Tekko

Language

Get in Touch

Usually respond within 24 hours

Back to BlogWeb Development

Local-First React: Building Offline-Ready Apps with ElectricSQL & PGlite

7 min read
ReactPostgresLocal-FirstElectricSQLPGlite
Local-First React: Building Offline-Ready Apps with ElectricSQL & PGlite

The architecture of web applications is undergoing a fundamental shift. For years, we have optimized the 'Request-Response' cycle, shaving milliseconds off REST APIs and GraphQL resolvers. Yet, the most significant bottleneck remains the network itself. Latency, intermittent connectivity, and the 'loading spinner' UX continue to plague modern web apps.

Local-first software represents a departure from this model. Instead of treating the server as the primary source of truth that the client must query, we treat the local device as the primary database. The server transitions into a synchronization and persistence layer. This article explores how to implement this paradigm using two powerful tools: ElectricSQL and PGlite.

The Philosophy of Local-First

Local-first is not just 'offline mode.' It is a design philosophy where the application's core logic and data reside on the user's device. This offers several immediate advantages:

  1. Zero Latency: UI updates happen instantly because they interact with a local database, not a remote API.
  2. Offline Resilience: The app works perfectly on a plane, in a tunnel, or with a spotty cellular connection.
  3. Simplified State Management: By using a real database (SQL) in the browser, we move away from complex Redux/Zustand stores and treat the database as the single source of truth for the UI.
  4. Multi-Device Sync: Changes propagate across devices seamlessly when a connection is available.

Introducing the Stack: PGlite and ElectricSQL

To build a local-first app, we need a robust local database and a reliable sync engine.

PGlite: Postgres in the Browser

Until recently, using SQL in the browser meant using SQLite via WASM or the now-deprecated WebSQL. While SQLite is excellent, it creates a 'dialect gap' between your browser-side SQLite and your server-side Postgres.

PGlite changes this. It is a WASM build of Postgres bundled into a client-side library. It allows you to run a full Postgres instance inside your browser tab, worker, or Node.js environment. It supports transactions, triggers, and extensions, all while maintaining a tiny footprint (~3MB gzipped).

ElectricSQL: The Sync Engine

ElectricSQL is the glue that connects your local PGlite instances to a central Postgres database. It uses Postgres's logical replication features to stream data changes between the server and the client. Unlike traditional sync solutions that require custom conflict-resolution code, ElectricSQL handles the heavy lifting of ensuring data consistency across distributed clients.

Architecture Overview

In a standard ElectricSQL + PGlite setup, the architecture looks like this:

  1. Central Postgres: Your cloud-hosted or on-premise Postgres database.
  2. Electric Sync Service: A middleware service that monitors the central Postgres WAL (Write Ahead Log) and manages 'Shapes'—subsets of data to be synced to clients.
  3. PGlite (Client): The local database running in the user's browser.
  4. React Hooks: A set of reactive hooks that re-render the UI whenever the local PGlite data changes.

Implementing the Solution

Let's walk through the implementation of a collaborative task management application.

1. Defining the Schema

You define your schema in standard SQL on your central Postgres instance. ElectricSQL works by 'electrifying' specific tables.

-- On the server-side Postgres CREATE TABLE projects ( id UUID PRIMARY KEY, name TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE TABLE tasks ( id UUID PRIMARY KEY, project_id UUID REFERENCES projects(id), title TEXT NOT NULL, completed BOOLEAN DEFAULT FALSE, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Enable replication for these tables ALTER TABLE projects REPLICA IDENTITY FULL; ALTER TABLE tasks REPLICA IDENTITY FULL;

2. Setting up the React Client

First, we initialize PGlite and the Electric client. The beauty of this approach is that the client-side code feels like standard database interaction.

import { PGlite } from '@electric-sql/pglite'; import { initElectric } from '@electric-sql/client'; const db = new PGlite(); const electric = await initElectric(db, { url: 'https://your-electric-service-url.com', });

3. Syncing Data with 'Shapes'

ElectricSQL uses a concept called Shapes. A Shape is a subscription to a subset of the global database. This is crucial because you rarely want to sync the entire database to every user's phone.

// Sync all tasks for a specific project const shape = await electric.sync({ tables: ['tasks'], where: `project_id = '${currentProjectId}'` }); // Wait for the initial sync to complete await shape.isReady();

4. Reactive UI with SQL

Instead of fetching data into a state variable, we use reactive hooks. When the local PGlite database is updated (either by the user or via sync from another device), the hook triggers a re-render.

import { useQuery } from '@electric-sql/pglite-react'; const TaskList = ({ projectId }) => { const { data: tasks, loading } = useQuery( 'SELECT * FROM tasks WHERE project_id = $1 ORDER BY created_at DESC', [projectId] ); if (loading) return <Spinner />; return ( <ul> {tasks.map(task => ( <TaskItem key={task.id} task={task} /> ))} </ul> ); };

Handling Conflicts

In a distributed system, two users will eventually edit the same record while offline. ElectricSQL handles this using Casual Ordering and Last Write Wins (LWW) at the column level by default.

Because ElectricSQL integrates deeply with the Postgres replication stream, it can maintain a high degree of consistency. For more complex scenarios, you can implement custom conflict resolution logic, but for 90% of web applications, the built-in LWW mechanism on a per-field basis provides the expected user experience.

Performance and Developer Experience

The 'SQL-First' Benefit

One of the most underrated benefits of this stack is the elimination of the API layer for data fetching. In a traditional app, you write:

  1. SQL on the server.
  2. A REST endpoint or GraphQL resolver.
  3. A TypeScript interface for the API response.
  4. A fetcher/query hook on the client.

With ElectricSQL and PGlite, you write SQL on the server and SQL on the client. Your data types are derived directly from the database schema. This reduces boilerplate significantly and makes the developer experience feel 'integrated.'

Bundle Size and WASM

While PGlite is remarkably small for a full Postgres build, it still involves WASM. For performance-sensitive applications, you should initialize the database in a Web Worker. This ensures that the heavy lifting of SQL execution doesn't block the main UI thread, keeping your animations and interactions at a smooth 60fps.

Security and Authorization

Local-first doesn't mean 'no security.' ElectricSQL integrates with Postgres Row Level Security (RLS). When a user connects to the sync service, they provide an auth token (e.g., JWT). The Electric service then uses the RLS policies defined in your Postgres database to determine which rows the user is allowed to see and sync.

-- Example RLS Policy ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; CREATE POLICY task_access_policy ON tasks FOR ALL USING (project_id IN ( SELECT id FROM projects WHERE user_id = current_setting('app.user_id')::uuid ));

Practical Considerations for Decision Makers

If you are evaluating this stack for a production project, consider the following:

  • Data Volume: PGlite stores data in IndexedDB. While IndexedDB can handle gigabytes, keep in mind the storage limits of mobile browsers. Local-first is best for 'operational data' rather than 'analytical archives.'
  • Migration Path: If you have an existing Postgres app, ElectricSQL is relatively non-invasive. You can electrify tables one by one without rewriting your entire backend.
  • Tooling Maturity: PGlite is a newer entrant compared to SQLite-WASM. However, its parity with Postgres makes it a compelling choice for teams already invested in the Postgres ecosystem.

Actionable Conclusion

Implementing local-first with ElectricSQL and PGlite removes the complexity of manual state synchronization and provides a superior user experience by default. To get started:

  1. Audit your 'Loading' states: Identify parts of your app where network latency frustrates users.
  2. Prototype with PGlite: Drop PGlite into a React project to manage local state using SQL instead of useState or Redux.
  3. Electrify your Schema: Use the ElectricSQL CLI to generate a client and start syncing a single table from your existing Postgres instance.
  4. Move Logic to the Client: Transition your data-fetching logic from REST/GraphQL to reactive SQL queries directly in your components.

The future of web development is one where the network is an implementation detail, not a constraint. By leveraging PGlite and ElectricSQL, you can build applications that are faster, more reliable, and significantly easier to maintain.