Claude Code for ElectricSQL: Local-First Sync for SQLite and PostgreSQL — Claude Skills 360 Blog
Blog / Backend / Claude Code for ElectricSQL: Local-First Sync for SQLite and PostgreSQL
Backend

Claude Code for ElectricSQL: Local-First Sync for SQLite and PostgreSQL

Published: March 9, 2027
Read time: 8 min read
By: Claude Skills 360

ElectricSQL syncs PostgreSQL data to SQLite on the client — shapes define what data syncs, and the client always reads from a local SQLite database that stays current via streaming replication. Shape subscriptions stream partial datasets: shape({ url, params: { table, where } }) fetches rows matching a filter. useShape(shape) returns the current rows and updates reactively when sync delivers changes from Postgres. Local writes flow through the server API; Electric handles the sync back to all clients, delivering a consistent eventually-consistent view. PGlite runs a full PostgreSQL instance in WebAssembly for in-browser development without a server. Electric Cloud hosts the sync layer; self-hosted Electric connects to your own PostgreSQL. Claude Code generates Electric shape definitions, React hook integrations, local write patterns, and the deployment configuration for local-first web applications.

CLAUDE.md for ElectricSQL

## ElectricSQL Stack
- Version: @electric-sql/react >= 1.0, @electric-sql/client >= 1.0
- Shape: shape({ url: ELECTRIC_URL, params: { table, where, columns } })
- React: const { data, isLoading, error } = useShape(orderShape) — reactive sync
- Write: local writes via server API → Electric syncs change to all clients
- PGlite: browser SQLite-compatible Postgres WA — for dev and offline-capable apps
- Auth: Electric headers option with JWT for row-level shape access control
- Partial sync: where: `status != 'archived'` — sync only relevant rows to client
- Deploy: ELECTRIC_URL env pointing to Electric Cloud or self-hosted instance

Shape Definitions

// lib/electric-shapes.ts — define sync shapes
import { Shape, ShapeStream } from "@electric-sql/client"

const ELECTRIC_URL = process.env.NEXT_PUBLIC_ELECTRIC_URL!

// Helper to get auth token for Electric requests
async function getElectricToken(): Promise<string> {
  const response = await fetch("/api/electric-token")
  const { token } = await response.json()
  return token
}

// Shape: all orders for the current user
export function createOrdersShape(customerId: string) {
  return new Shape({
    url: `${ELECTRIC_URL}/v1/shape`,
    params: {
      table: "orders",
      where: `customer_id = '${customerId}'`,
      columns: ["id", "status", "total_cents", "created_at", "tracking_number"],
    },
    headers: {
      Authorization: async () => `Bearer ${await getElectricToken()}`,
    },
  })
}

// Shape: all products (shared data)
export const productsShape = new Shape({
  url: `${ELECTRIC_URL}/v1/shape`,
  params: {
    table: "products",
    where: "active = true",
    columns: ["id", "slug", "name", "price_cents", "stock"],
  },
})

// Shape: single order with its items
export function createOrderDetailShape(orderId: string) {
  return new Shape({
    url: `${ELECTRIC_URL}/v1/shape`,
    params: {
      table: "order_items",
      where: `order_id = '${orderId}'`,
    },
  })
}

// ShapeStream: lower-level streaming for manual control
export function watchOrderUpdates(
  orderId: string,
  onUpdate: (rows: any[]) => void
) {
  const stream = new ShapeStream({
    url: `${ELECTRIC_URL}/v1/shape`,
    params: {
      table: "orders",
      where: `id = '${orderId}'`,
    },
  })

  const unsubscribe = stream.subscribe(messages => {
    const rows = messages
      .filter(m => m.headers.action !== "delete")
      .map(m => m.value)
    onUpdate(rows)
  })

  return unsubscribe
}

React Hooks

// hooks/useOrders.ts — reactive order data from ElectricSQL
import { useShape } from "@electric-sql/react"
import { useMemo } from "react"
import { createOrdersShape } from "@/lib/electric-shapes"
import { useAuth } from "@/lib/auth"

interface Order {
  id: string
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
  total_cents: number
  created_at: string
  tracking_number: string | null
}

export function useOrders(filters?: { status?: string }) {
  const { userId } = useAuth()
  const shape = useMemo(() => createOrdersShape(userId!), [userId])

  const {
    data: rawData,
    isLoading,
    error,
  } = useShape<Order>(shape)

  const orders = useMemo(() => {
    if (!rawData) return []

    let result = rawData

    if (filters?.status) {
      result = result.filter(o => o.status === filters.status)
    }

    return result.sort(
      (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
    )
  }, [rawData, filters?.status])

  return { orders, isLoading, error }
}

// Component using the hook
export function OrdersList() {
  const [statusFilter, setStatusFilter] = useState<string | undefined>()
  const { orders, isLoading } = useOrders({ status: statusFilter })

  if (isLoading) return <OrdersListSkeleton />

  return (
    <div>
      <div className="flex gap-2 mb-4">
        {["all", "pending", "shipped", "delivered"].map(status => (
          <button
            key={status}
            onClick={() => setStatusFilter(status === "all" ? undefined : status)}
            className={cn(
              "px-3 py-1 rounded-full text-sm",
              (status === "all" && !statusFilter) || statusFilter === status
                ? "bg-primary text-primary-foreground"
                : "bg-muted"
            )}
          >
            {status}
          </button>
        ))}
      </div>

      {orders.map(order => (
        <OrderCard key={order.id} order={order} />
      ))}

      {orders.length === 0 && (
        <p className="text-center text-muted-foreground py-12">No orders found</p>
      )}
    </div>
  )
}

Local Writes with Electric Sync

// lib/order-mutations.ts — write to server, Electric syncs to clients
export async function createOrder(input: CreateOrderInput): Promise<Order> {
  // Write goes to your API server → database → Electric replicates to all clients
  const response = await fetch("/api/orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(input),
  })

  if (!response.ok) {
    const error = await response.json()
    throw new Error(error.message ?? "Failed to create order")
  }

  const order = await response.json()

  // No need to update local state — Electric sync delivers the change
  // automatically to all components using useShape for the orders table

  return order
}

export async function cancelOrder(orderId: string): Promise<void> {
  const response = await fetch(`/api/orders/${orderId}/cancel`, {
    method: "POST",
  })

  if (!response.ok) throw new Error("Failed to cancel order")

  // Electric delivers the status change to all subscribed clients
}

PGlite In-Browser

// lib/pglite-dev.ts — in-browser PostgreSQL for development/offline
import { PGlite } from "@electric-sql/pglite"
import { electricSync } from "@electric-sql/pglite-sync"

// Create browser-based Postgres instance (persisted to OPFS)
export async function createLocalDB() {
  const db = await PGlite.create("idb://my-app-db", {
    extensions: { electric: electricSync() },
  })

  // Create local schema matching server
  await db.exec(`
    CREATE TABLE IF NOT EXISTS orders (
      id UUID PRIMARY KEY,
      customer_id UUID NOT NULL,
      status TEXT NOT NULL DEFAULT 'pending',
      total_cents INTEGER NOT NULL,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    )
  `)

  // Sync from Electric into local PGlite
  await db.electric.syncShapeToTable({
    shape: {
      url: `${process.env.NEXT_PUBLIC_ELECTRIC_URL}/v1/shape`,
      params: { table: "orders" },
    },
    table: "orders",
    primaryKey: ["id"],
  })

  return db
}

// Query local database directly (works offline)
export async function queryLocalOrders(db: PGlite, customerId: string) {
  const result = await db.query<Order>(
    "SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC",
    [customerId]
  )
  return result.rows
}

Electric Token API

// app/api/electric-token/route.ts — issue scoped Electric JWT
import { NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { SignJWT } from "jose"

const ELECTRIC_SECRET = new TextEncoder().encode(process.env.ELECTRIC_SECRET!)

export async function GET() {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  // Issue JWT scoped to the user's ID for Electric RLS
  const token = await new SignJWT({
    sub: session.user.id,
    // Electric uses these claims to apply row-level security in shapes
    "electric.user_id": session.user.id,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("1h")
    .sign(ELECTRIC_SECRET)

  return NextResponse.json({ token })
}

For the Convex real-time backend alternative that colocates server functions alongside reactive queries in a TypeScript-first backend without requiring a separate sync layer, see the Convex backend guide for reactive query patterns. For the Supabase Realtime alternative that provides PostgreSQL-backed real-time subscriptions through database change listeners rather than full offline sync — better for primarily online apps needing live updates without full local-first architecture, see the Supabase Advanced guide for realtime channels. The Claude Skills 360 bundle includes ElectricSQL skill sets covering shape definitions, PGlite, and offline patterns. Start with the free tier to try ElectricSQL sync configuration generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free