electric-new-feature

This skill builds on electric-shapes, electric-proxy-auth, and electric-schema-shapes. Read those first.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "electric-new-feature" with this command: npx skills add electric-sql/electric/electric-sql-electric-electric-new-feature

This skill builds on electric-shapes, electric-proxy-auth, and electric-schema-shapes. Read those first.

Electric — New Feature End-to-End

Setup

  1. Start Electric locally

docker-compose.yml

services: postgres: image: postgres:17-alpine environment: POSTGRES_DB: electric POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - '54321:5432' tmpfs: - /tmp command: - -c - listen_addresses=* - -c - wal_level=logical

electric: image: electricsql/electric:latest environment: DATABASE_URL: postgresql://postgres:password@postgres:5432/electric?sslmode=disable ELECTRIC_INSECURE: true # Dev only — use ELECTRIC_SECRET in production ports: - '3000:3000' depends_on: - postgres

docker compose up -d

  1. Create Postgres table

CREATE TABLE todos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, text TEXT NOT NULL, completed BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT now() );

ALTER TABLE todos REPLICA IDENTITY FULL;

  1. Create proxy route

The proxy forwards Electric protocol params and injects server-side secrets. Use your framework's server route pattern (TanStack Start, Next.js API route, Express, etc.).

// Example: TanStack Start — src/routes/api/todos.ts import { createFileRoute } from '@tanstack/react-router' import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'

const serve = async ({ request }: { request: Request }) => { const url = new URL(request.url) const electricUrl = process.env.ELECTRIC_URL || 'http://localhost:3000' const origin = new URL(${electricUrl}/v1/shape)

url.searchParams.forEach((v, k) => { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(k)) origin.searchParams.set(k, v) })

origin.searchParams.set('table', 'todos')

// Add auth if using Electric Cloud if (process.env.ELECTRIC_SOURCE_ID && process.env.ELECTRIC_SECRET) { origin.searchParams.set('source_id', process.env.ELECTRIC_SOURCE_ID) origin.searchParams.set('secret', process.env.ELECTRIC_SECRET) }

const res = await fetch(origin) const headers = new Headers(res.headers) headers.delete('content-encoding') headers.delete('content-length') return new Response(res.body, { status: res.status, statusText: res.statusText, headers, }) }

export const Route = createFileRoute('/api/todos')({ server: { handlers: { GET: serve, }, }, })

  1. Define schema

// db/schema.ts — Zod schema matching your Postgres table import { z } from 'zod'

export const todoSchema = z.object({ id: z.string().uuid(), user_id: z.string().uuid(), text: z.string(), completed: z.boolean(), created_at: z.date(), })

export type Todo = z.infer<typeof todoSchema>

If using Drizzle, generate schemas from your table definitions with createSelectSchema(todosTable) from drizzle-zod .

  1. Create mutation endpoint

Implement your write endpoint using your framework's server function or API route. The endpoint must return { txid } from the same transaction as the mutation.

// Example: server function that inserts and returns txid async function createTodo(todo: { text: string; user_id: string }) { const client = await pool.connect() try { await client.query('BEGIN') const result = await client.query( 'INSERT INTO todos (text, user_id) VALUES ($1, $2) RETURNING id', [todo.text, todo.user_id] ) const txResult = await client.query( 'SELECT pg_current_xact_id()::xid::text AS txid' ) await client.query('COMMIT') return { id: result.rows[0].id, txid: Number(txResult.rows[0].txid) } } finally { client.release() } }

  1. Create TanStack DB collection

import { createCollection } from '@tanstack/react-db' import { electricCollectionOptions } from '@tanstack/electric-db-collection' import { todoSchema } from './db/schema'

export const todoCollection = createCollection( electricCollectionOptions({ id: 'todos', schema: todoSchema, getKey: (row) => row.id, shapeOptions: { url: new URL( '/api/todos', typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173' ).toString(), // Electric auto-parses: bool, int2, int4, float4, float8, json, jsonb // You only need custom parsers for types like timestamptz, date, numeric // See electric-shapes/references/type-parsers.md for the full list parser: { timestamptz: (date: string) => new Date(date), }, }, onInsert: async ({ transaction }) => { const { modified: newTodo } = transaction.mutations[0] const result = await createTodo({ text: newTodo.text, user_id: newTodo.user_id, }) return { txid: result.txid } }, onUpdate: async ({ transaction }) => { const { modified: updated } = transaction.mutations[0] const result = await updateTodo(updated.id, { text: updated.text, completed: updated.completed, }) return { txid: result.txid } }, onDelete: async ({ transaction }) => { const { original: deleted } = transaction.mutations[0] const result = await deleteTodo(deleted.id) return { txid: result.txid } }, }) )

  1. Build live queries

import { useLiveQuery, eq } from '@tanstack/react-db'

export function TodoList() { const { data: todos } = useLiveQuery((q) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.completed, false)) .orderBy(({ todo }) => todo.created_at, 'desc') .limit(50) )

return ( <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ) }

  1. Optimistic mutations

const handleAdd = () => { todoCollection.insert({ id: crypto.randomUUID(), text: 'New todo', completed: false, created_at: new Date(), }) }

const handleToggle = (todo) => { todoCollection.update(todo.id, (draft) => { draft.completed = !draft.completed }) }

const handleDelete = (todoId) => todoCollection.delete(todoId)

Common Mistakes

HIGH Removing parsers because the TanStack DB schema handles types

Wrong:

// "My Zod schema has z.coerce.date() so I don't need a parser" electricCollectionOptions({ schema: z.object({ created_at: z.coerce.date() }), shapeOptions: { url: '/api/todos' }, // No parser! })

Correct:

electricCollectionOptions({ schema: z.object({ created_at: z.coerce.date() }), shapeOptions: { url: '/api/todos', parser: { timestamptz: (date: string) => new Date(date) }, }, })

Electric's sync path delivers data directly into the collection store, bypassing the TanStack DB schema. The parser in shapeOptions handles type coercion on the sync path; the schema handles the mutation path. You need both. Without the parser, timestamptz arrives as a string and getTime() or other Date methods will fail at runtime.

CRITICAL Using old electrify() bidirectional sync API

Wrong:

const { db } = await electrify(conn, schema) await db.todos.create({ text: 'New todo' })

Correct:

todoCollection.insert({ id: crypto.randomUUID(), text: 'New todo' }) // Write path: collection.insert() → onInsert → API → Postgres → txid → awaitTxId

Old ElectricSQL (v0.x) had bidirectional SQLite sync. Current Electric is read-only. Writes go through your API endpoint and are reconciled via txid handshake.

Source: AGENTS.md:386-392

HIGH Using path-based table URL pattern

Wrong:

const stream = new ShapeStream({ url: 'http://localhost:3000/v1/shape/todos?offset=-1', })

Correct:

const stream = new ShapeStream({ url: 'http://localhost:3000/v1/shape?table=todos&#x26;offset=-1', })

The table-as-path-segment pattern (/v1/shape/todos ) was removed in v0.8.0. Table is now a query parameter.

Source: packages/sync-service/CHANGELOG.md:1124

MEDIUM Using shape_id instead of handle

Wrong:

const stream = new ShapeStream({ url: '/api/todos', params: { shape_id: '12345' }, })

Correct:

const stream = new ShapeStream({ url: '/api/todos', handle: '12345', })

Renamed from shape_id to handle in v0.8.0.

Source: packages/sync-service/CHANGELOG.md:1123

See also: electric-orm/SKILL.md — Getting txid from ORM transactions. See also: electric-proxy-auth/SKILL.md — E2E feature journey includes setting up proxy routes.

Version

Targets @electric-sql/client v1.5.10, @tanstack/react-db latest.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

blog-planner

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-proxy-auth

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-shapes

No summary provided by upstream source.

Repository SourceNeeds Review
General

electric-debugging

No summary provided by upstream source.

Repository SourceNeeds Review