Skip to main content
GoBetter manages all database schema migrations through Drizzle ORM and Drizzle Kit. The system relies on declarative TypeScript definitions as the single source of truth, compiling schema diffs into version-controlled SQL migration scripts before applying them to PostgreSQL.

Architecture & Lifecycle

The migration pipeline follows an immutable, append-only lifecycle:
  1. Source of Truth: Database tables, columns, relations, and enums are defined in backend/src/database/schema/.
  2. Diff Engine: Drizzle Kit compares the TypeScript definitions against the current migration snapshot and produces a versioned SQL file.
  3. Execution: The programmatic migration runner backend/src/database/migrations/migrate.ts applies unexecuted migrations inside a transaction and updates the tracking table.

Tooling Configuration

Drizzle Kit is configured via backend/src/config/drizzle.config.ts:

Configuration Options


CLI Commands & Workflows

All database scripts are executed from the backend/ 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.ts via tsx.
  • Connects to the database and reads the internal __drizzle_migrations table.
  • Applies all pending migration files in chronological order within a transaction.
  • Logs elapsed execution time upon completion.

3. Direct Schema Push (Development Only)

Directly synchronizes the database schema with TypeScript files without generating migration scripts.
Never use db:push in staging or production environments. It bypasses versioned SQL files and migration tracking history, making it impossible to perform automated rollback or audit database state.

4. Drizzle Studio

Launches a local web UI at 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 runner backend/src/database/migrations/migrate.ts:

Key Implementation Details

  • Migration Tracking Table: Drizzle automatically provisions a __drizzle_migrations table 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 in backend/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 the usage table, the total_cost column is generated at the database level:
  • Rule: Do not insert or update total_cost directly in application code or INSERT statements. PostgreSQL computes it automatically upon row write.

2. PostgreSQL Custom Enums

Enums are defined with pgEnum:
  • 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.id cascading to sessions, pull_requests, byok, and workspace_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:
This maps to gen_random_uuid() in Postgres 13+, avoiding client-side UUID generation overhead.

Production Deployment Checklist

  1. 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.
  2. Release Phase Migrations: In containerized hosting environments (Fly.io, Railway, AWS ECS, Kubernetes), execute pnpm run db:migrate in the deployment release hook before starting new container instances.
  3. 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.
  4. 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.