electric-new-feature について
Electric の機能をエンドツーエンドで実装する開発者向けスキルです。Postgres のテーブルからプロキシルート、コレクション、ライブクエリまでを扱い、よくある実装上の問題も説明します。
主な機能
- 01Electric のローカル環境構築
- 02Postgres テーブルとプロキシルートの作成
- 03スキーマとコレクションの定義
- 04ライブクエリと楽観的ミューテーションの構築
- 05同期実装でよくある誤りの回避
活用シーン
- 01同期対応の ToDo 機能の構築
- 02Electric データ用プロキシルートの追加
- 03TanStack DB の型や書き込み経路の問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ electric-sql/ electric --skill electric-new-featureインストール後、Claude Code で/electric-new-featureを入力して読み込まれたか確認します。
詳細
このスキルは、electric-shapes、electric-proxy-auth、electric-schema-shapesを基盤としています。まず、これらをお読みください。
Electric — 新機能のエンドツーエンド実装
セットアップ
0. Electricをローカルで起動する
# 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. Postgresのテーブルを作成する
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;
2. プロキシルートを作成する
プロキシはElectricプロトコルのパラメーターを転送し、サーバー側のシークレットを追加します。使用しているフレームワークのサーバールートのパターン(TanStack Start、Next.js APIルート、Expressなど)を使用してください。
// 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,
},
},
})
3. スキーマを定義する
// 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>
Drizzleを使用する場合は、drizzle-zodのcreateSelectSchema(todosTable)を使って、テーブル定義からスキーマを生成してください。
4. ミューテーションエンドポイントを作成する
フレームワークのサーバー関数またはAPIルートを使って、書き込みエンドポイントを実装してください。エンドポイントは、ミューテーションと同じトランザクションから{ txid }を返す必要があります。
// 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()
}
}
5. TanStack DBコレクションを作成する
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 }
},
})
)
6. ライブクエリを構築する
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>
)
}
7. 楽観的ミューテーション
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)
よくある間違い
HIGH TanStack DBのスキーマが型を処理するため、パーサーを削除する
誤り:
// "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!
})
正しい例:
electricCollectionOptions({
schema: z.object({ created_at: z.coerce.date() }),
shapeOptions: {
url: '/api/todos',
parser: { timestamptz: (date: string) => new Date(date) },
},
})
Electricの同期経路では、データがTanStack DBのスキーマを経由せず、コレクションストアに直接渡されます。shapeOptionsのparserは同期経路での型変換を処理し、スキーマはミューテーション経路を処理します。両方が必要です。パーサーがないと、timestamptzは文字列として渡されるため、getTime()などのDateメソッドは実行時に失敗します。
CRITICAL 古いelectrify()双方向同期APIを使用する
誤り:
const { db } = await electrify(conn, schema)
await db.todos.create({ text: 'New todo' })
正しい例:
todoCollection.insert({ id: crypto.randomUUID(), text: 'New todo' })
// Write path: collection.insert() → onInsert → API → Postgres → txid → awaitTxId
旧ElectricSQL(v0.x)には、双方向のSQLite同期がありました。現在のElectricは読み取り専用です。書き込みはAPIエンドポイント経由で行い、txidハンドシェイクを通じて調整します。
出典:AGENTS.md:386-392
HIGH パスベースのテーブルURLパターンを使用する
誤り:
const stream = new ShapeStream({
url: 'http://localhost:3000/v1/shape/todos?offset=-1',
})
正しい例:
const stream = new ShapeStream({
url: 'http://localhost:3000/v1/shape?table=todos&offset=-1',
})
テーブルをパスセグメントとして指定するパターン(/v1/shape/todos)は、v0.8.0で削除されました。テーブルはクエリパラメーターで指定するようになりました。
出典:packages/sync-service/CHANGELOG.md:1124
MEDIUM handleの代わりにshape_idを使用する
誤り:
const stream = new ShapeStream({
url: '/api/todos',
params: { shape_id: '12345' },
})
正しい例:
const stream = new ShapeStream({
url: '/api/todos',
handle: '12345',
})
v0.8.0でshape_idからhandleに名称が変更されました。
出典:packages/sync-service/CHANGELOG.md:1123
関連項目:electric-orm/SKILL.md — ORMトランザクションからtxidを取得する方法。 関連項目:electric-proxy-auth/SKILL.md — エンドツーエンドの機能実装手順には、プロキシルートの設定が含まれています。
バージョン
@electric-sql/client v1.5.10、@tanstack/react-dbの最新バージョンを対象としています。