Skillshub electric-shapes

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/electric-sql/electric/electric-shapes" ~/.claude/skills/comeonoliver-skillshub-electric-shapes && rm -rf "$T"
manifest: skills/electric-sql/electric/electric-shapes/SKILL.md
source content

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.