Drizzle ORM + Supabase — A Production-Safe DB Workflow

Drizzle ORM + Supabase — A Production-Safe DB Workflow

The Problem: ORM Migration Risk

You add a column to a production database. The auto-migration tool executes unexpected SQL. The service goes down.

Schema changes are the most nerve-wracking moment in any deployment. Storida combines Drizzle ORM’s developer experience with Supabase CLI’s production safety to solve this.

Workflow Overview

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ schema.ts    │────▶│   dev DB     │────▶│ Migration    │
│ (Drizzle)    │ push│  (Supabase)  │ diff│    .sql      │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │ git push
                                           ┌──────▼───────┐
                                           │  prod DB     │
                                           │ (Supabase)   │
                                           └──────────────┘

In development, Drizzle applies schemas directly. In production, only reviewed SQL migrations execute.

Environment Separation

Environment Git Branch Supabase Branch Purpose
Development dev Preview Schema experiments, fast iteration
Production main Production Reviewed migrations only

Supabase supports Git branch linking. Push to dev and the Preview DB updates automatically. Merge to main and Production DB applies the migration.

Three-Step Schema Change Process

Step 1: Define Schema in Drizzle

All schemas are written in TypeScript. This enables type inference and autocompletion.

The contents table tracks each piece of generated content: a unique ID, the owning user (FK to users with cascade delete), a title, a status field (defaulting to PENDING), total page count, generation phase and progress indicators, and a creation timestamp. Each field is defined in TypeScript with full type inference.

Drizzle schema definitions map 1:1 to SQL DDL. No proprietary DSL like Prisma. You can predict the generated SQL.

Step 2: Push to Dev DB

cd packages/backend
npm run db:push   # Drizzle → dev DB direct apply

db:push calculates the diff between the current schema and the database, then applies it immediately. No migration files generated. Fast iteration. Development environment only.

Step 3: Generate Migration SQL

After testing on the dev DB, extract migration SQL with Supabase CLI.

supabase db diff -f add_content_type
# → supabase/migrations/20260319_add_content_type.sql

This SQL file becomes the subject of code review. A human reviews the auto-generated SQL before it touches production. Unsafe DDL like DROP COLUMN gets caught.

Database Schema: 12+ Tables

Storida has 12+ tables. The core relationships look like this.

users ─────┬── contents ──── content_pages
           │      └───── content_assets ── characters
           ├── subscriptions
           ├── payments
           ├── credit_logs
           └── credit_purchases

prompt_templates (writing styles)
styles (illustration styles)
ai_models (AI models)
announcements ── announcement_reads
policies (terms of service)
jobs (Job Queue)
usage_records (token usage logs)
app_config (system configuration)

ID Type Decision: UUID vs Text

users.id and managers.id use text type, not uuid. Firebase UIDs are 28-character alphanumeric strings. The original design used uuid. When migrating to Firebase Auth, every FK column referencing users.id had to change type simultaneously.

The lesson: tables referencing external auth system IDs need a flexible type. Use text.

Forbidden Rules

Three rules prevent production incidents.

Forbidden Reason
Modify tables via Supabase Dashboard Breaks sync with Drizzle schema
Run db:push on production DB Applies changes without migration history
Manually edit applied migration files Conflicts with already-applied migrations

Break any of these and the schema source of truth fractures. Recovery is painful.

Seed Data Management

Initial data (plans, default characters, system settings) lives in src/db/seed.ts. Run with npm run db:seed. Idempotency is guaranteed via ON CONFLICT DO NOTHING.

The seed script inserts initial plan tiers (free through premium), default characters, and system settings. Each insert uses an ON CONFLICT DO NOTHING clause for idempotency.

Run it once. Run it ten times. Same result. No duplicate records. No errors.

Summary

Drizzle ORM is SQL-first. You never wonder “what SQL is this ORM generating?” Combine it with Supabase CLI’s db diff and branch-based auto-deployment. Development speed and production safety coexist.

Next: the dynamic prompt system for Claude and Gemini.