mcpField
E
electric-shapes

electric-shapes

github.com/electric-sqlData & databases10.4k51 installsUpdated 3w ago
Repository

About electric-shapes

A skill for developers using Electric, explaining how to sync and process data streams with @electric-sql/client's ShapeStream. It covers filtering, column selection, field mapping, retry behavior, and resuming streams.

Key features

  1. 01Filter rows by conditions
  2. 02Select columns with primary keys
  3. 03Map database column names
  4. 04Configure error retries
  5. 05Resume from a stored position
  6. 06Parse custom types

Use cases

  1. 01Sync data for selected users or statuses
  2. 02Map database fields to client naming
  3. 03Resume synchronization from a saved offset

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/electric-sql/electric --skill electric-shapes

After installing, type/electric-shapesin Claude Code to confirm it loaded.

Bundled filesreferences/type-parsers.mdreferences/where-clause.md

Details

From the project README

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.

Comments

Markdown supported