# Real-time Sync

For Postgres databases, you can enable real-time data synchronization. This enables building 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 where your backend proxies requests after handling authentication and authorization.

## Enabling sync

Reference `sync.url` and `sync.secret` in your service env:

```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 starts a sync engine connected to your Postgres database during `specific dev`.

## Available sync attributes

- `sync.url` - Sync engine HTTP endpoint URL (e.g., `http://127.0.0.1:5133`)
- `sync.secret` - Secret for authenticating requests to the sync engine. This should be passed as the `secret` query parameter from the 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 (handles 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:

- Authentication and proxying: https://electric-sql.com/docs/guides/auth
- Shapes and client usage: https://electric-sql.com/docs/guides/shapes
- Handling writes: https://electric-sql.com/docs/guides/writes
- Troubleshooting: https://electric-sql.com/docs/guides/troubleshooting

## Proxy configuration

When proxying Electric requests from your backend, you must handle headers and query parameters correctly. 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`. Make sure your proxy does not 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 CDN level. During development, this can cause browsers to serve stale cached responses — for example, responses cached before CORS headers were configured correctly will keep failing even after the proxy is fixed. Override with `Cache-Control: no-store` on your proxy response to prevent this.

### Control query parameters

Set these server-side — 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

When using `ShapeStream` from the Electric TypeScript SDK, a full URL is required (not a relative URL). In browsers, derive the full URL from `window.location.origin` instead of using a relative one:

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

const stream = new ShapeStream({
  url: `${window.location.origin}/api/sync/items`,
});
```

This ensures the client works correctly regardless of the deployment environment.

---

Run `specific docs postgres` for database configuration.
