Electric — Shape Streaming
Setup
import { ShapeStream, Shape } from '@electric-sql/client'
const stream = new ShapeStream({ url: '/api/todos', // Your proxy route, NOT direct Electric URL // Built-in parsers auto-handle: bool, int2, int4, float4, float8, json, jsonb // Add custom parsers for other types (see references/type-parsers.md) parser: { timestamptz: (date: string) => new Date(date), }, })
const shape = new Shape(stream)
shape.subscribe(({ rows }) => { console.log('synced rows:', rows) })
// Wait for initial sync const rows = await shape.rows
Core Patterns
Filter rows with WHERE clause and positional params
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', where: 'user_id = $1 AND status = $2', params: { '1': userId, '2': 'active' }, }, })
Select specific columns (must include primary key)
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', columns: ['id', 'title', 'status'], // PK required }, })
Map column names between snake_case and camelCase
import { ShapeStream, snakeCamelMapper } from '@electric-sql/client'
const stream = new ShapeStream({ url: '/api/todos', columnMapper: snakeCamelMapper(), }) // DB column "created_at" arrives as "createdAt" in client // WHERE clauses auto-translate: "createdAt" → "created_at"
Handle errors with retry
const stream = new ShapeStream({ url: '/api/todos', onError: (error) => { console.error('sync error', error) return {} // Return {} to retry; returning void stops the stream }, })
For auth token refresh on 401 errors, see electric-proxy-auth/SKILL.md.
Resume from stored offset
const stream = new ShapeStream({ url: '/api/todos', offset: storedOffset, // Both offset AND handle required handle: storedHandle, })
Get replica with old values on update
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', replica: 'full', // Sends unchanged columns + old_value on updates }, })
Common Mistakes
CRITICAL Returning void from onError stops sync permanently
Wrong:
const stream = new ShapeStream({ url: '/api/todos', onError: (error) => { console.error('sync error', error) // Returning nothing = stream stops forever }, })
Correct:
const stream = new ShapeStream({ url: '/api/todos', onError: (error) => { console.error('sync error', error) return {} // Return {} to retry }, })
onError returning undefined signals the stream to permanently stop. Return at least {} to retry, or return { headers, params } to retry with updated values.
Source: packages/typescript-client/src/client.ts:409-418
HIGH Using columns without including primary key
Wrong:
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', columns: ['title', 'status'], }, })
Correct:
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', columns: ['id', 'title', 'status'], }, })
Server returns 400 error. The columns list must always include the primary key column(s).
Source: website/docs/guides/shapes.md
HIGH Setting offset without handle for resumption
Wrong:
new ShapeStream({ url: '/api/todos', offset: storedOffset, })
Correct:
new ShapeStream({ url: '/api/todos', offset: storedOffset, handle: storedHandle, })
Throws MissingShapeHandleError . Both offset AND handle are required to resume a stream from a stored position.
Source: packages/typescript-client/src/client.ts:1997-2003
HIGH Using non-deterministic functions in WHERE clause
Wrong:
const stream = new ShapeStream({ url: '/api/events', params: { table: 'events', where: 'start_time > now()', }, })
Correct:
const stream = new ShapeStream({ url: '/api/events', params: { table: 'events', where: 'start_time > $1', params: { '1': new Date().toISOString() }, }, })
Server rejects WHERE clauses with non-deterministic functions like now() , random() , count() . Use static values or positional params.
Source: packages/sync-service/lib/electric/replication/eval/env/known_functions.ex
HIGH Not parsing custom Postgres types
Wrong:
const stream = new ShapeStream({ url: '/api/events', }) // createdAt will be string "2024-01-15T10:30:00.000Z", not a Date
Correct:
const stream = new ShapeStream({ url: '/api/events', parser: { timestamptz: (date: string) => new Date(date), timestamp: (date: string) => new Date(date), }, })
Electric auto-parses bool , int2 , int4 , float4 , float8 , json , jsonb , and int8 (→ BigInt). All other types arrive as strings — add custom parsers for timestamptz , date , numeric , etc. See references/type-parsers.md for the full list.
Source: AGENTS.md:300-308
MEDIUM Using reserved parameter names in params
Wrong:
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', cursor: 'abc', // Reserved! offset: '0', // Reserved! }, })
Correct:
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', page_cursor: 'abc', page_offset: '0', }, })
Throws ReservedParamError . Names cursor , handle , live , offset , cache-buster , and all subset__* prefixed params are reserved by the Electric protocol.
Source: packages/typescript-client/src/client.ts:1984-1985
MEDIUM Mutating shape options on a running stream
Wrong:
const stream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', where: "status = 'active'" }, }) // Later... stream.options.params.where = "status = 'done'" // No effect!
Correct:
// Create a new stream with different params const newStream = new ShapeStream({ url: '/api/todos', params: { table: 'todos', where: "status = 'done'" }, })
Shapes are immutable per subscription. Changing params on a running stream has no effect. Create a new ShapeStream instance for different filters.
Source: AGENTS.md:106
References
-
WHERE clause supported types and functions
-
Built-in type parsers
See also: electric-proxy-auth/SKILL.md — Shape URLs must point to proxy routes, not directly to Electric. See also: electric-debugging/SKILL.md — onError semantics and backoff are essential for diagnosing sync problems.
Version
Targets @electric-sql/client v1.5.10.