packages/typescript-client/skills/electric-orm/SKILL.md
Use Electric with Drizzle ORM or Prisma for the write path. Covers getting pg_current_xact_id() from ORM transactions using Drizzle tx.execute(sql) and Prisma $queryRaw, running migrations that preserve REPLICA IDENTITY FULL, and schema management patterns compatible with Electric shapes. Load when using Drizzle or Prisma alongside Electric for writes.
npx skillsauth add electric-sql/electric electric-ormInstall 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.
This skill builds on electric-shapes and electric-schema-shapes. Read those first.
import { drizzle } from 'drizzle-orm/node-postgres'
import { sql } from 'drizzle-orm'
import { todos } from './schema'
const db = drizzle(pool)
// Write with txid for Electric reconciliation
async function createTodo(text: string, userId: string) {
return await db.transaction(async (tx) => {
const [row] = await tx
.insert(todos)
.values({
id: crypto.randomUUID(),
text,
userId,
})
.returning()
const [{ txid }] = await tx.execute<{ txid: string }>(
sql`SELECT pg_current_xact_id()::xid::text AS txid`
)
return { id: row.id, txid: parseInt(txid) }
})
}
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function createTodo(text: string, userId: string) {
return await prisma.$transaction(async (tx) => {
const todo = await tx.todo.create({
data: { id: crypto.randomUUID(), text, userId },
})
const [{ txid }] = await tx.$queryRaw<[{ txid: string }]>`
SELECT pg_current_xact_id()::xid::text AS txid
`
return { id: todo.id, txid: parseInt(txid) }
})
}
// In migration file
import { sql } from 'drizzle-orm'
export async function up(db) {
await db.execute(sql`
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
text TEXT NOT NULL,
completed BOOLEAN DEFAULT false
)
`)
await db.execute(sql`ALTER TABLE todos REPLICA IDENTITY FULL`)
}
-- prisma/migrations/001_init/migration.sql
CREATE TABLE "todos" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"text" TEXT NOT NULL,
"completed" BOOLEAN DEFAULT false
);
ALTER TABLE "todos" REPLICA IDENTITY FULL;
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
export const todoCollection = createCollection(
electricCollectionOptions({
id: 'todos',
schema: todoSchema,
getKey: (row) => row.id,
shapeOptions: { url: '/api/todos' },
onInsert: async ({ transaction }) => {
const newTodo = transaction.mutations[0].modified
const { txid } = await createTodo(newTodo.text, newTodo.userId)
return { txid }
},
})
)
Wrong:
// Drizzle — no txid returned
const [todo] = await db.insert(todos).values({ text: 'New' }).returning()
return { id: todo.id }
Correct:
// Drizzle — txid in same transaction
const result = await db.transaction(async (tx) => {
const [row] = await tx.insert(todos).values({ text: 'New' }).returning()
const [{ txid }] = await tx.execute<{ txid: string }>(
sql`SELECT pg_current_xact_id()::xid::text AS txid`
)
return { id: row.id, txid: parseInt(txid) }
})
ORMs do not return pg_current_xact_id() by default. Add a raw SQL query for txid within the same transaction. Without it, optimistic state may drop before the synced version arrives, causing UI flicker.
Source: AGENTS.md:116-119
Wrong:
// ORM migration recreates table without REPLICA IDENTITY
await db.execute(sql`DROP TABLE todos`)
await db.execute(sql`CREATE TABLE todos (...)`)
// Missing: ALTER TABLE todos REPLICA IDENTITY FULL
Correct:
await db.execute(sql`DROP TABLE todos`)
await db.execute(sql`CREATE TABLE todos (...)`)
await db.execute(sql`ALTER TABLE todos REPLICA IDENTITY FULL`)
Some migration tools reset table properties. Always ensure REPLICA IDENTITY FULL is set after table recreation. Without it, Electric cannot stream updates and deletes correctly.
Source: website/docs/guides/troubleshooting.md:373
See also: electric-new-feature/SKILL.md — Full write-path journey including txid handshake. See also: electric-schema-shapes/SKILL.md — Schema design affects both shapes and ORM queries.
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.
tools
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.
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.