skills/electric-sql/electric/electric-shapes

electric-shapes

SKILL.md

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

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.

Weekly Installs
4
GitHub Stars
10.0K
First Seen
8 days ago
Installed on
opencode4
gemini-cli4
github-copilot4
codex4
kimi-cli4
amp4