Tekko

Language

Get in Touch

Usually respond within 24 hours

Back to BlogWeb Development

Local-First React: Syncing Postgres with ElectricSQL and PGlite

8 min read
ReactPostgreSQLLocal-FirstElectricSQLWebAssembly
Local-First React: Syncing Postgres with ElectricSQL and PGlite

For the last decade, the standard architecture for web applications has been the thin-client model. We build a React frontend, a REST or GraphQL API in the middle, and a heavy database at the back. Every user interaction—every click, every form submission—requires a round-trip to the server. If the user is on a spotty 5G connection or in a Wi-Fi dead zone, the application grinds to a halt, replaced by the ubiquitous loading spinner.

As engineers, we’ve tried to paper over these cracks with optimistic UI updates and complex caching layers like TanStack Query or SWR. But these are essentially band-aids. To truly solve the latency and offline problem, we need to rethink the architecture. We need to move the data closer to the user. This is the core tenet of the 'Local-First' movement.

In this article, we’ll explore a powerful new stack for building local-first React applications: PGlite (Postgres in the browser) and ElectricSQL (the synchronization engine). Together, they allow you to run a reactive, relational database inside your user’s browser and keep it perfectly in sync with your server-side Postgres.

The Local-First Paradigm Shift

Local-first software isn't just about 'offline mode.' It's about a fundamental change in how data is owned and accessed. In a local-first app, the primary source of truth for the UI is a local database residing in the client's memory or persistent storage (like IndexedDB).

When the user performs an action, the app writes to the local database immediately. The UI updates instantly because there is zero network latency. In the background, a synchronization engine ensures that these local changes are propagated to the server and that changes from other users are pulled down.

This approach offers three major advantages:

  1. Zero Latency: The UI responds at the speed of the local CPU/Disk.
  2. Offline Resilience: The app remains fully functional without an internet connection.
  3. Simplified State Management: Instead of managing complex caches, your UI simply 'observes' the local database.

PGlite: Bringing Postgres to the Browser

Until recently, if you wanted a database in the browser, you were largely stuck with IndexedDB (which has a clunky API) or SQLite via WebAssembly (WASM). While SQLite is excellent, it creates a 'dialect gap' if your backend is running Postgres. You end up writing different migrations, different queries, and dealing with different data types.

PGlite changes the game. It is a build of the actual Postgres source code compiled to WASM, packaged as a lightweight TypeScript library. It allows you to run a full Postgres engine inside a browser tab, a Web Worker, or Node.js, with no external dependencies.

Why PGlite?

  • Small Footprint: It’s roughly 3MB compressed, making it feasible for web delivery.
  • Full Relational Power: You get JSONB support, Common Table Expressions (CTEs), and complex joins right in the browser.
  • Persistence: It can persist data to IndexedDB, ensuring the local state survives page reloads.

ElectricSQL: The Sync Engine

Having a database in the browser is only half the battle. The real challenge is synchronization. How do you move data between the client-side PGlite and your central Postgres server without writing thousands of lines of custom 'sync' logic?

ElectricSQL is the bridge. It provides a sync service that sits in front of your Postgres database and streams data to clients. Recently, ElectricSQL introduced a 'Shape-based' sync protocol that is particularly revolutionary.

Instead of trying to sync the entire database, you define a Shape—a subset of your data (e.g., "all projects for User A and their associated tasks"). ElectricSQL handles the heavy lifting of tracking changes in the master database and streaming those updates to the client-side PGlite instance via an efficient HTTP-based protocol.

Building the Architecture

Let’s look at how these pieces fit together in a React application. The architecture follows a simple flow:

  1. The Backend: A standard Postgres database.
  2. The Sync Layer: ElectricSQL monitors Postgres and exposes 'Shapes'.
  3. The Client: A React app using PGlite as its local store.

Step 1: Initializing PGlite

First, we initialize the PGlite instance. In a real-world app, you would likely do this in a React Context or a dedicated singleton module.

import { PGlite } from '@electric-sql/pglite'; // Initialize PGlite with IndexedDB persistence const db = new PGlite('idb://my-app-db'); export default db;

Step 2: Consuming Shapes with ElectricSQL

ElectricSQL provides a React hook (or a vanilla JS client) to sync a shape into your local database. When the shape changes on the server, ElectricSQL pushes those changes to the client, and we apply them to PGlite.

import { useShape } from '@electric-sql/react'; import db from './db'; function TaskList() { // Define the shape we want to sync const { data: tasks, status } = useShape({ url: `http://api.electric-sql.com/v1/shape/tasks`, selector: (data) => data, // Optional transformation }); if (status === 'loading') return <div>Initial sync in progress...</div>; return ( <ul> {tasks.map(task => ( <li key={task.id}>{task.title}</li> ))} </ul> ); }

Step 3: Reactive Queries

One of the most powerful features of PGlite combined with a sync engine is reactivity. You can set up 'live queries' against your local Postgres. When the sync engine updates the local data, your UI components re-render automatically.

import { useLiveQuery } from '@electric-sql/pglite-react'; function CompletedTasksCount() { const res = useLiveQuery( "SELECT COUNT(*) as count FROM tasks WHERE status = 'completed'", [] ); return <p>Completed tasks: {res?.rows[0]?.count ?? 0}</p>; }

Handling Conflicts and Integrity

In a distributed system where multiple clients can write to their local databases simultaneously, conflicts are inevitable. ElectricSQL handles this using Causal Integrity.

By default, ElectricSQL ensures that updates are applied in a consistent order across all clients. Because it uses a "last-write-wins" approach (or more sophisticated CRDT-based logic depending on the configuration), you don't have to worry about your database ending up in an inconsistent state. However, as a developer, you should still design your schema to be additive where possible, using UUIDs for primary keys to avoid collision during offline creation.

Real-World Example: A Field Service App

Imagine an application for utility workers inspecting equipment in remote areas.

  • Without Local-First: The worker tries to load the equipment list. The spinner spins. They lose signal. The app crashes or shows a 404. They have to take notes on paper and sync manually later.
  • With PGlite + ElectricSQL: Before leaving the office, the app syncs the 'Inspection' shape for that day. In the field, the worker has full access to the Postgres database. They record inspections via SQL INSERT statements into PGlite. The UI is instant. When they regain signal, ElectricSQL automatically streams those rows back to the central Postgres server.

Performance Considerations

While running Postgres in the browser sounds heavy, PGlite is remarkably efficient. Since it runs in the same process (or a Worker), there is no TCP overhead for queries. A simple SELECT from a local PGlite instance typically takes less than 1 millisecond.

However, you should be mindful of:

  1. Storage Limits: Browsers impose limits on IndexedDB (usually a percentage of free disk space). Don't try to sync a multi-terabyte table.
  2. Initial Sync: The first time a user opens the app, they must download the initial 'Shape' data. Use fine-grained shapes to minimize this payload.
  3. Worker Offloading: For complex computations, run PGlite in a Web Worker to keep the main UI thread buttery smooth.

Why This Matters for Technical Decision Makers

Choosing a local-first architecture with PGlite and ElectricSQL isn't just a technical flex; it's a strategic advantage.

  • Reduced Server Costs: Much of the query load is offloaded to the client's device.
  • Improved User Retention: Users stick with apps that feel fast and 'just work' regardless of their connection.
  • Developer Productivity: You stop writing complex retry logic, cache invalidation strategies, and state synchronization code. You just write SQL.

Conclusion

The transition to local-first development represents the next major evolution in web architecture. By combining the robustness of PostgreSQL with the portability of WASM and the seamless synchronization of ElectricSQL, we can finally build web applications that feel as snappy and reliable as native desktop software.

Actionable Next Steps:

  1. Audit your 'offline' needs: Identify which parts of your app would benefit most from zero-latency interactions.
  2. Experiment with PGlite: Replace a small piece of local state (like a complex filter configuration) with a PGlite instance to get a feel for the API.
  3. Explore ElectricSQL Shapes: Look at the ElectricSQL documentation to see how easily you can expose your existing Postgres tables as syncable shapes.

It's time to stop building around the limitations of the network and start building for the user.