Database
PostgreSQL schema, Drizzle ORM, and migrations - the foundation for Zero sync
Takeout uses PostgreSQL as the source of truth, with Drizzle ORM for schema management. Zero replicates from Postgres to provide real-time sync to clients. This page covers the database layer—for client-side queries and mutations, see the Zero page.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ schema-private │ │ schema-public │ │
│ │ (auth tables) │ │ (synced to Zero clients) │ │
│ └─────────────────┘ └──────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────────────┐ │
│ │ │ Zero Replication │ │
│ │ └──────────────────────┘ │
└───────────│─────────────────────────│───────────────────────┘
│ │
▼ ▼
Server-side only Client devices
(auth, sessions) (offline-first sync)
Schema Files
The database schema is split into two files in src/database/:
schema-public.ts
Tables that sync to Zero clients. This is your application data:
schema-private.ts
Authentication and sensitive data that stays server-side only:
Both schemas feed into Drizzle migrations—“private” means the data doesn’t sync to clients, not that it’s excluded from migrations.
Migrations
Takeout has a unified migration system that combines Drizzle schema migrations with custom TypeScript migrations.
Workflow
- Edit
src/database/schema-public.tsorschema-private.ts - Run migrations:
Terminal
That’s it. You don’t need to run Drizzle commands directly.
Custom Migrations
For data migrations or changes that can’t be expressed in schema:
Terminal
This creates a TypeScript migration file:
We don’t do down migrations—only forward.
How It Works
bun migrate build does three things:
- Runs
drizzle-kit generateto create SQL from schema changes - Wraps SQL migrations in TypeScript (using
?rawimports) - Bundles everything into a single deployable file
The migration runner tracks applied migrations in a migrations table and runs
them in a transaction.
Server-Side Queries
For server-side code (API routes, server actions), use Drizzle ORM:
See the Drizzle ORM docs for the full query API.
For client-side queries that sync in real-time, use Zero’s ZQL—see the Zero page and Zero’s Reading Data docs.
Aggregates and Triggers
Zero doesn’t support aggregates, so we use PostgreSQL triggers to maintain
denormalized counts. For example, post.commentCount is updated automatically
when comments are added or removed.
Simple Counter Pattern
Key patterns:
- Recalculate the count from scratch (safer than incrementing)
- Check parent exists before updating (prevents cascade delete errors)
- Use AFTER triggers to ensure the source data is committed
For complex aggregates like time-windowed stats or multi-table relationships, create dedicated stats tables with their own triggers.
Local Development
Starting the Database
Terminal
Connecting
Terminal
Or use a GUI like TablePlus with the connection string from ZERO_UPSTREAM_DB
(default: postgresql://user:password@127.0.0.1:5433/postgres).
Troubleshooting
Migration file is blank/empty
Drizzle found no schema changes. Check that:
- Your changes are saved
- You’re editing the right file (
schema-public.tsvsschema-private.ts) - The schema doesn’t already match the database
Check which migrations have run
Terminal
Migration failed
Migrations run in a transaction, so failures should rollback automatically. If you need to manually fix:
- Check what migrations have run (query above)
- If a bad migration was applied, write a new migration to fix it
- If stuck in a partial state, you may need to manually delete from the migrations table:
Then fix your schema and run migrations again.
Schema changes not detected
- Both
schema-public.tsandschema-private.tsgo to Drizzle - “Private” means it doesn’t sync to Zero clients, not “excluded from migrations”
- Run
bun migrate runwhich regenerates migrations fresh
Zero not seeing changes
After schema changes:
- Run
bun migrate runto update Postgres - Run
bun tko zero/generateto regenerate Zero types - Restart the Zero server
Zero replicates from Postgres, so the database must be updated first.
Production
Production uses AWS Aurora Serverless v2 with PostgreSQL 16. The connection pooling handles Aurora’s connection limits with:
- Retry logic with exponential backoff
- Automatic pool recycling when approaching limits
- Configurable timeouts for serverless environments
Environment variables:
ZERO_UPSTREAM_DB- Main database connection stringZERO_CVR_DB- Zero client view records databaseZERO_CHANGE_DB- Zero change tracking database
Learn More
- Zero page - Client-side queries and mutations
- Drizzle ORM docs - Schema and query reference
- PostgreSQL docs - SQL and triggers
Edit this page on GitHub.