packages/typescript-client/skills/electric-shapes/SKILL.md
Configure ShapeStream and Shape to sync a Postgres table to the client. Covers ShapeStreamOptions (url, table, where, columns, replica, offset, handle), custom type parsers (timestamptz, jsonb, int8), column mappers (snakeCamelMapper, createColumnMapper), onError retry semantics, backoff options, log modes (full, changes_only), requestSnapshot, fetchSnapshot, subscribe/unsubscribe, and Shape materialized view. Load when setting up sync, configuring shapes, parsing types, or handling sync errors.
npx skillsauth add electric-sql/electric electric-shapesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
const stream = new ShapeStream({
url: '/api/todos',
params: {
table: 'todos',
where: 'user_id = $1 AND status = $2',
params: { '1': userId, '2': 'active' },
},
})
const stream = new ShapeStream({
url: '/api/todos',
params: {
table: 'todos',
columns: ['id', 'title', 'status'], // PK required
},
})
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"
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.
const stream = new ShapeStream({
url: '/api/todos',
offset: storedOffset, // Both offset AND handle required
handle: storedHandle,
})
const stream = new ShapeStream({
url: '/api/todos',
params: {
table: 'todos',
replica: 'full', // Sends unchanged columns + old_value on updates
},
})
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
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
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
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
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
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
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
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.
Targets @electric-sql/client v1.5.10.
development
Interactive blog post authoring. Produces a draft blog post file with structured outline, inline guidance comments, and meta briefs that the author proses up in place. Supports pyramid principle, best sales deck, and release post formats.
development
Set up ElectricProvider for real-time collaborative editing with Yjs via Electric shapes. Covers ElectricProvider configuration, document updates shape with BYTEA parser (parseToDecoder), awareness shape at offset='now', LocalStorageResumeStateProvider for reconnection with stableStateVector diff, debounceMs for batching writes, sendUrl PUT endpoint, required Postgres schema (ydoc_update and ydoc_awareness tables), CORS header exposure, and sendErrorRetryHandler. Load when implementing collaborative editing with Yjs and Electric.
documentation
Design Postgres schema and Electric shape definitions together for a new feature. Covers single-table shape constraint, cross-table joins using multiple shapes, WHERE clause design for tenant isolation, column selection for bandwidth optimization, replica mode choice (default vs full for old_value), enum casting in WHERE clauses, and txid handshake setup with pg_current_xact_id() for optimistic writes. Load when designing database tables for use with Electric shapes.
testing
Set up a server-side proxy to forward Electric shape requests securely. Covers ELECTRIC_PROTOCOL_QUERY_PARAMS forwarding, server-side shape definition (table, where, params), content-encoding/content-length header cleanup, CORS configuration for electric-offset/electric-handle/ electric-schema/electric-cursor headers, auth token injection, ELECTRIC_SECRET/SOURCE_SECRET server-side only, tenant isolation via WHERE positional params, onError 401 token refresh, and subset security (AND semantics). Load when creating proxy routes, adding auth, or configuring CORS for Electric.