Skip to content
Back to skills

Electric Schema Shapes

ASecurity

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.

  • 65 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added May 30, 2026
developmentsqlapidatabasebackend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 30, 2026

npx -y skills add ComeOnOliver/skillshub --skill electric-schema-shapes --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Electric Schema Shapes?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Electric Schema Shapes
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/comeonoliver-electric-schema-shapes/badge)](https://www.skillsdirectory.com/skills/comeonoliver-electric-schema-shapes)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: electric-schema-shapes
description: >
  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.
type: core
library: electric
library_version: '1.5.10'
requires:
  - electric-shapes
sources:
  - 'electric-sql/electric:AGENTS.md'
  - 'electric-sql/electric:website/docs/guides/shapes.md'
---

This skill builds on electric-shapes. Read it first for ShapeStream configuration.

# Electric — Schema and Shapes

## Setup

Design tables knowing each shape syncs one table. For cross-table data, use multiple shapes with client-side joins.

```sql
-- Schema designed for Electric shapes
CREATE TABLE todos (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL,
  text TEXT NOT NULL,
  completed BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE todos REPLICA IDENTITY FULL;
```

```ts
import { ShapeStream } from '@electric-sql/client'

const todoStream = new ShapeStream({
  url: '/api/todos', // Proxy sets: table=todos, where=org_id=$1
})
```

## Core Patterns

### Cross-table data with multiple shapes

```ts
// Each shape syncs one table — join client-side
const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })

// With TanStack DB, use .join() in live queries:
// q.from({ todo: todoCollection })
//   .join({ user: userCollection }, ({ todo, user }) => eq(todo.userId, user.id))
```

### Choose replica mode

```ts
// Default: only changed columns sent on update
const stream = new ShapeStream({ url: '/api/todos' })

// Full: all columns + old_value on updates (more bandwidth, needed for diffs)
const stream = new ShapeStream({
  url: '/api/todos',
  params: { replica: 'full' },
})
```

### Backend txid handshake for optimistic writes

Call `pg_current_xact_id()::xid::text` inside the same transaction as your mutation. If you query it outside the transaction, you get a different txid and the client will never reconcile.

```ts
// API endpoint — txid MUST be in the same transaction as the INSERT
app.post('/api/todos', async (req, res) => {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await client.query(
      'INSERT INTO todos (id, text, org_id) VALUES ($1, $2, $3) RETURNING id',
      [crypto.randomUUID(), req.body.text, req.body.orgId]
    )
    const txResult = await client.query(
      'SELECT pg_current_xact_id()::xid::text AS txid'
    )
    await client.query('COMMIT')
    // txid accepts number | bigint | `${bigint}`
    res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
  } finally {
    client.release()
  }
})
```

```ts
// Client awaits txid before dropping optimistic state
await todoCollection.utils.awaitTxId(txid)
```

## Common Mistakes

### HIGH Designing shapes that span multiple tables

Wrong:

```ts
const stream = new ShapeStream({
  url: '/api/data',
  params: {
    table: 'todos JOIN users ON todos.user_id = users.id',
  },
})
```

Correct:

```ts
const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })
```

Shapes are single-table only. Cross-table data requires multiple shapes joined client-side via TanStack DB live queries.

Source: `AGENTS.md:104-105`

### MEDIUM Using enum columns without casting to text in WHERE

Wrong:

```ts
// Proxy route
originUrl.searchParams.set('where', "status IN ('active', 'done')")
```

Correct:

```ts
originUrl.searchParams.set('where', "status::text IN ('active', 'done')")
```

Enum types in WHERE clauses require explicit `::text` cast. Without it, the query may fail or return unexpected results.

Source: `packages/sync-service/lib/electric/replication/eval/env/known_functions.ex`

### HIGH Not setting up txid handshake for optimistic writes

Wrong:

```ts
// Backend: just INSERT, return id
app.post('/api/todos', async (req, res) => {
  const result = await db.query(
    'INSERT INTO todos (text) VALUES ($1) RETURNING id',
    [req.body.text]
  )
  res.json({ id: result.rows[0].id })
})
```

Correct:

```ts
// Backend: INSERT and return txid in same transaction
app.post('/api/todos', async (req, res) => {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await client.query(
      'INSERT INTO todos (text) VALUES ($1) RETURNING id',
      [req.body.text]
    )
    const txResult = await client.query(
      'SELECT pg_current_xact_id()::xid::text AS txid'
    )
    await client.query('COMMIT')
    res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
  } finally {
    client.release()
  }
})
```

Without txid, the UI flickers when optimistic state is dropped before the synced version arrives from Electric. The client uses `awaitTxId(txid)` to hold optimistic state until the real data syncs.

Source: `AGENTS.md:116-119`

See also: electric-shapes/SKILL.md — Shapes are immutable; dynamic filters require new ShapeStream instances.
See also: electric-orm/SKILL.md — Schema design affects both shapes (read) and ORM queries (write).

## Version

Targets @electric-sql/client v1.5.10.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.