Skip to main content
For Postgres databases, you can enable real-time data synchronization to build real-time and local-first applications. Sync is powered by Electric, a sync engine that streams changes from Postgres to clients using a plain HTTP API. It uses a proxy-based architecture: your backend proxies requests to the sync engine after handling authentication and authorization.

Enabling sync

Reference sync.url and sync.secret on a Postgres database in your service env:
specific.hcl
postgres "main" {}

service "api" {
  command = "node index.js"

  endpoint {
    public = true
  }

  env = {
    DATABASE_URL         = postgres.main.url
    DATABASE_SYNC_URL    = postgres.main.sync.url
    DATABASE_SYNC_SECRET = postgres.main.sync.secret
  }
}
When these references are present, Specific automatically runs a sync engine connected to your Postgres database, locally during specific dev and managed in production. Sync attributes:
AttributeDescription
sync.urlSync engine HTTP endpoint URL (for example, http://127.0.0.1:5133 in dev).
sync.secretSecret for authenticating requests to the sync engine. Pass it as the secret query parameter from your backend to the Electric API.

How sync works

The sync engine streams “shapes” of data from Postgres to clients:
  1. Your backend proxies shape requests to the sync engine (handling auth/authz).
  2. The sync engine streams the initial data and subsequent changes.
  3. Clients receive real-time updates over HTTP.
For implementation details, see the Electric documentation:

Proxy configuration

When proxying Electric requests from your backend, handle headers and query parameters carefully. Getting these wrong causes silent failures: browsers block responses or serve stale cached data without clear error messages.

Expose Electric response headers

Electric returns custom electric-* headers that the client SDK needs to follow the shape log. Browsers block JavaScript from reading non-standard response headers unless they are listed in Access-Control-Expose-Headers. Don’t strip any electric-* headers from the response, and expose them all:
Access-Control-Expose-Headers: electric-handle, electric-offset, electric-schema, electric-cursor, electric-up-to-date, electric-chunk-last-offset

Strip upstream headers

Electric’s response includes its own CORS and caching headers. If your proxy copies these through and your CORS middleware also adds its own, the browser receives duplicate Access-Control-Allow-Origin values and rejects the response. Before setting your own headers, strip these from Electric’s response:
  • Access-Control-Allow-Origin and other Access-Control-* headers
  • Content-Encoding and Content-Length (fetch decompresses the body but doesn’t update these headers, which breaks browser decoding)
  • Cache-Control (see below)

Override caching

Electric returns Cache-Control with max-age and stale-age for request collapsing at the CDN level. During development, this can cause browsers to serve stale cached responses. For example, responses cached before CORS headers were configured correctly keep failing even after the proxy is fixed. Override with Cache-Control: no-store on your proxy response.

Control query parameters

Set these server-side and never pass them through from the client:
  • table - letting clients specify the table allows access to any table
  • where - this is your authorization filter
  • columns - clients could request sensitive columns
  • secret - never expose the API secret to clients
Only forward Electric protocol parameters from the client: offset, handle, live, live_sse, cursor, expired_handle, replica, log.

Using the Electric TypeScript SDK

ShapeStream requires a full URL, not a relative one. In browsers, derive it from window.location.origin:
import { ShapeStream } from "@electric-sql/client";

const stream = new ShapeStream({
  url: `${window.location.origin}/api/sync/items`,
});
This works correctly regardless of the deployment environment.