Architecture & Lifecycle
The migration pipeline follows an immutable, append-only lifecycle:- Source of Truth: Database tables, columns, relations, and enums are defined in
backend/src/database/schema/. - Diff Engine: Drizzle Kit compares the TypeScript definitions against the current migration snapshot and produces a versioned SQL file.
- Execution: The programmatic migration runner
backend/src/database/migrations/migrate.tsapplies unexecuted migrations inside a transaction and updates the tracking table.
Tooling Configuration
Drizzle Kit is configured viabackend/src/config/drizzle.config.ts:
Configuration Options
CLI Commands & Workflows
All database scripts are executed from thebackend/ directory:
1. Generate SQL Migrations (db:gen)
- Analyzes changes in
src/database/schema/*.ts. - Prompts for disambiguation if a column was renamed or dropped.
- Creates a new timestamped file in
src/database/migrations/generated/(e.g.0035_smooth_thanos.sql). - Appends the new migration metadata to
src/database/migrations/generated/meta/_journal.json.
2. Apply Migrations Programmatically (db:migrate)
- Executes
backend/src/database/migrations/migrate.tsviatsx. - Connects to the database and reads the internal
__drizzle_migrationstable. - Applies all pending migration files in chronological order within a transaction.
- Logs elapsed execution time upon completion.
3. Direct Schema Push (Development Only)
4. Drizzle Studio
https://local.drizzle.studio to inspect tables, view foreign-key relations, run ad-hoc queries, and modify seed data during development.
Programmatic Migration Runner
In production or automated deployment pipelines, migrations run via the standalone runnerbackend/src/database/migrations/migrate.ts:
Key Implementation Details
- Migration Tracking Table: Drizzle automatically provisions a
__drizzle_migrationstable in PostgreSQL that stores migration IDs, hashes, and timestamps. - Atomic Operations: Each migration file executes inside a database transaction. If any statement fails, the entire migration aborts and rolls back, preventing partial schema corruption.
- Connection Teardown:
db.$client.end()explicitly closes open pool sockets so the Node process terminates cleanly in CI/CD runners.
Migration Journal & File Structure
All generated migrations reside inbackend/src/database/migrations/generated/:
Journal Metadata (_journal.json)
The journal tracks the sequential dependency of migration files:
Commit both the
.sql files and the meta/ directory to Git. The journal ensures that team members and deployment pipelines execute identical migrations in the exact same sequence.Schema Invariants & Postgres Features
When authoring schema modifications, observe these PostgreSQL-specific patterns used across the GoBetter database:1. Generated Always As Columns
In theusage table, the total_cost column is generated at the database level:
- Rule: Do not insert or update
total_costdirectly in application code or INSERT statements. PostgreSQL computes it automatically upon row write.
2. PostgreSQL Custom Enums
Enums are defined withpgEnum:
- Drizzle Generation Behavior: Drizzle Kit automatically emits safe type creation blocks in generated SQL:
3. Foreign Key Cascades
Tables referencing parent records use explicit cascade behavior:users.idcascading tosessions,pull_requests,byok, andworkspace_settings.- When modifying relation keys, ensure
onDelete: 'cascade'is declared so orphaned child rows do not block deletion.
4. Native UUID Generation
Primary keys utilize PostgreSQLβs native random UUID generator:gen_random_uuid() in Postgres 13+, avoiding client-side UUID generation overhead.
Production Deployment Checklist
- Never edit generated SQL files manually unless addressing custom data migrations (backfills). If you modify an existing migration file, you must update the corresponding snapshot in
meta/or regenerate the migration. - Release Phase Migrations: In containerized hosting environments (Fly.io, Railway, AWS ECS, Kubernetes), execute
pnpm run db:migratein the deployment release hook before starting new container instances. - Connection Pooling:
- For transactional web traffic, the backend connects through the pooled connection string (
pooler.neon.tech). - For DDL migrations (
db:migrate), ensure session timeouts are sufficient to accommodate table alterations and index builds.
- For transactional web traffic, the backend connects through the pooled connection string (
- Non-Destructive Changes: Use the Expand and Contract pattern for schema changes:
- Step 1 (Expand): Add new nullable columns or tables. Deploy application code that writes to both old and new columns.
- Step 2 (Backfill): Run data migration script to populate new columns.
- Step 3 (Contract): Drop old unused columns in a subsequent release.