Menu
Blog Documentation Community Pricing Demo Call Sign Up
Sign Up

ElectricSQL: How Postgres Real-Time Sync Works

How ElectricSQL syncs PostgreSQL data to client apps in real time — shapes, HTTP streaming, WAL integration, and practical implementation patterns.

Postgres

This post was written by an engineer at QueryPlane. QueryPlane is an app builder for your database: bring your own postgres db and you can create interactive applications to share with other developers, coworkers or even your customers. If you’re interested in trying it out, get started here.


Most applications fetch data from a server on every interaction. The user clicks, the app sends a request, waits for a response, and renders the result. When the network is fast, this is fine. When it isn’t, the experience degrades—loading spinners, stale data, failed requests. ElectricSQL offers a different approach: sync data from PostgreSQL directly to client applications and keep it up to date in real time.

In this post, we’ll cover:

  • What ElectricSQL is - A read-path sync engine for Postgres
  • Shapes - The core primitive for partial replication
  • Architecture - How data flows from Postgres WAL to clients over HTTP
  • Client libraries - TypeScript, React, Elixir, and PGlite integrations
  • Deployment - Self-hosted Docker and Electric Cloud
  • Tradeoffs - Where ElectricSQL fits and where it doesn’t

What ElectricSQL is

ElectricSQL is an open-source sync engine that streams data out of PostgreSQL into client applications. It’s licensed under Apache 2.0 and developed by the team at Electric, who have deep roots in CRDT research and distributed systems.

The key design decision is that Electric is a read-path sync engine. It handles getting data from Postgres to clients efficiently—partial replication, real-time updates, fan-out to many concurrent users. Writes still go through your existing backend API. You don’t need to rewrite your mutation logic or adopt a new write protocol. This makes Electric practical to adopt incrementally: you keep your existing Postgres schema, your existing backend, and layer sync on top for the read path.

This stands in contrast to earlier versions of the project. ElectricSQL underwent a significant architectural rewrite in mid-2024, moving from a more complex local-first framework with CRDTs to the current, simpler read-path sync approach. The pivot made it easier to integrate with existing stacks.

Shapes: partial replication

The core primitive in ElectricSQL is the Shape. A Shape defines a subset of data from a Postgres table, specified by a WHERE clause, that should be synced to a client. Instead of replicating entire tables—which would be impractical for most applications—clients subscribe to Shapes that match the data they actually need.

import { ShapeStream } from "@electric-sql/client";

const stream = new ShapeStream({
  url: "http://localhost:3000/v1/shape",
  params: {
    table: "projects",
    where: "org_id = 'acme'",
  },
});

When a client subscribes to a Shape, Electric sends the initial data matching the WHERE clause, then streams incremental updates as the underlying Postgres rows change. If a row is updated and no longer matches the Shape’s WHERE clause, the client receives a delete message. If a new row is inserted that matches, the client receives an insert.

Shapes are composable. A client can subscribe to multiple Shapes simultaneously—one for the user’s projects, another for their notifications, another for shared team settings. Each Shape syncs independently, and the client can manage them separately.

How data flows: Postgres to client

ElectricSQL connects to PostgreSQL using logical replication, the same mechanism used by CDC tools like Debezium. The database writes changes to its write-ahead log (WAL), and Electric consumes that stream in real time.

The data flow works in four stages:

  1. WAL consumption. Electric connects to Postgres via a replication slot and reads the logical WAL stream. This captures every insert, update, and delete on the tables you’re syncing.

  2. Shape filtering. Incoming changes are matched against active Shapes. If a change affects a row that belongs to one or more Shapes, it’s routed to those Shapes’ logs.

  3. Shape cache. Filtered changes are stored in an append-only log on disk. The v1.1 storage engine maintains two files per Shape: a log of raw data and an offset index for fast lookups. This design achieves 102x faster writes and 73x faster reads on SSD compared to the previous engine, by doing all heavy work at write time so the read path stays fast.

  4. HTTP serving. Clients fetch Shape data over standard HTTP. Electric uses long polling—clients make a request, and the server holds it open until new data is available or a 20-second timeout elapses. Because it’s plain HTTP, Shape data can be cached by CDNs, which is useful for fan-out to many clients requesting the same data.

This HTTP-based approach is a deliberate choice over WebSockets. It means Electric works behind standard load balancers, CDN edge nodes, and existing HTTP infrastructure without special configuration.

Client libraries

Electric provides client libraries at two levels of abstraction.

ShapeStream is the low-level API. It gives you a stream of change messages (inserts, updates, deletes) with offset management, retry logic, and message parsing. You consume raw events and manage state yourself.

Shape is the higher-level API. It maintains an in-memory materialized view—a Map<key, row>—that stays in sync with the server. You read from the Map and subscribe to change notifications.

import { Shape } from "@electric-sql/client";

const shape = new Shape({
  url: "http://localhost:3000/v1/shape",
  params: {
    table: "tasks",
    where: "user_id = '123'",
  },
});

shape.subscribe((data) => {
  console.log("Current tasks:", [...data.values()]);
});

For React applications, Electric provides hooks that integrate with React’s rendering lifecycle. The useShape hook subscribes to a Shape and triggers re-renders when data changes.

Beyond the core client, the Electric ecosystem includes two complementary projects. PGlite is an embeddable, WASM-based Postgres that runs in the browser—useful for local-first applications that want a full SQL engine on the client. TanStack DB is a reactive client-side store designed for building local-first applications with Electric as the sync layer.

See what QueryPlane can build for you

Connect to your database, write SQL with AI, and build shareable apps — all from your browser.

Deployment

An Electric deployment has three components: your Postgres database, the Electric sync service, and your application.

The sync service is an Elixir application packaged as a Docker container. It needs a DATABASE_URL pointing to your Postgres instance (directly, not through a connection pooler, since most poolers don’t support logical replication), a filesystem for the Shape cache, and an exposed HTTP port.

docker run -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  -p 3000:3000 \
  electricsql/electric

Your Postgres database needs wal_level = logical enabled. If you’re running on a managed provider like Supabase, Neon, or any of the major cloud Postgres providers, logical replication is typically available—check your provider’s documentation for how to enable it.

For managed hosting, Electric Cloud entered public beta in April 2025. It provides turnkey hosting with a global data delivery network (DDN) and regional deployment options. Pricing is usage-based with a free tier. At the time of writing, the beta is still free for all users.

Write path: your existing API

Because Electric only handles the read path, writes go through whatever backend you already have. If you’re using a REST API, GraphQL, or RPC—nothing changes. Your backend validates input, enforces authorization, applies business logic, and writes to Postgres. Electric picks up the changes from the WAL and syncs them to subscribed clients.

This separation is intentional. Authorization logic, validation rules, and business constraints stay in your backend code where they belong, rather than being encoded into database-level sync rules. If you already have a working backend for a Postgres-backed application, adopting Electric doesn’t require rearchitecting your write path.

For applications that need optimistic updates on the client side, you handle that in your application code—apply the change locally, send the mutation to your API, and let Electric’s sync confirm or override the local state when the server-side write hits the WAL.

Tradeoffs

ElectricSQL is well-suited for applications that read frequently and can tolerate a slight delay between a server-side write and the client seeing the update (typically sub-second). Real-time dashboards, collaborative views, live feeds, and any UI that polls an API today are good candidates.

The read-only sync model is both a strength and a limitation. It simplifies the architecture dramatically—no conflict resolution, no CRDT complexity, no bidirectional sync bugs—but it means Electric doesn’t solve offline writes. If your application needs users to make changes while disconnected and sync those changes back later, you’ll need to combine Electric with something else for the write path, or look at a tool like PowerSync that handles bidirectional sync.

Electric connects directly to Postgres via logical replication, which means it consumes a replication slot. On managed Postgres providers, you may have a limited number of replication slots available. It also means your Postgres WAL retention grows if Electric falls behind on processing—the same operational consideration that applies to any CDC tool.

Finally, Shapes currently support single-table queries with WHERE clause filtering. If you need to sync joined data from multiple tables, you’ll either need multiple Shapes on the client that you join application-side, or denormalize the data on the server. This is an area the team is actively working on.

Wrapping up

ElectricSQL takes a focused approach to the sync problem: stream data from Postgres to clients over HTTP, use Shapes for partial replication, and leave writes to your existing backend. The architecture is simple enough to reason about—it’s essentially a real-time, filtered read replica that speaks HTTP—and the read-path-only design avoids the complexity of bidirectional sync and conflict resolution.

If you’re building on Postgres and find yourself writing polling logic, building WebSocket layers for real-time updates, or managing client-side caches that go stale, Electric is worth evaluating. For more on the broader local-first ecosystem, see our post on local-first databases. If you’re evaluating your Postgres setup, our guides on choosing a primary key strategy and top PostgreSQL extensions cover related decisions.