Database environments and schema migration system. Covers dev/prod PostgreSQL, Drizzle ORM, dual migration approach (Drizzle SQL + TypeScript startup), naming conventions, orchestration order, idempotency, and dev workflow.
Database environments and schema migration system for the hospitality financial model. Covers dev/prod PostgreSQL, Drizzle ORM patterns, the dual migration approach (Drizzle SQL migrations + TypeScript startup migrations), naming conventions, orchestration order, idempotency requirements, and the dev workflow. Use this skill when adding database columns, creating tables, modifying the schema, syncing environments, or debugging database issues.
DATABASE_URL env var)shared/schema.tsserver/storage.ts (IStorage interface). Routes call storage methods — never raw SQL.DATABASE_URL (auto-set), PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASEnpm run db:push # Push schema changes to dev database
npx drizzle-kit push # Alternative direct push
npx drizzle-kit studio # Open Drizzle Studio for visual DB management
All Drizzle table definitions live in shared/schema.ts. When adding tables:
pgTable()createInsertSchema from drizzle-zodz.infer<typeof insertSchema>typeof table.$inferSelectnpm run db:push to applymigrations/)Generated by Drizzle Kit from the schema definition. These are the canonical DDL migrations.
shared/schema.ts (Drizzle ORM table definitions)drizzle.config.ts (output dir: ./migrations, dialect: PostgreSQL)0000_brainy_mother_askani.sqlmigrations/meta/ contains Drizzle's migration journalDev workflow:
npx drizzle-kit push # Push schema changes directly to dev DB (no migration file)
npx drizzle-kit generate # Generate a migration SQL file from schema diff
Use db:push during development for rapid iteration. Generate migration files before committing for production deployments.
server/migrations/)Runtime migrations that execute on server startup. Used for:
ALTER TABLE ... ADD COLUMN IF NOT EXISTSEach migration is a standalone .ts file exporting an async function.
Current files:
server/migrations/
├── auto-research-refresh-001.ts
├── companies-theme-001.ts
├── composite-indexes-001.ts
├── db-hygiene-001.ts
├── documents-001.ts
├── drop-plaid-001.ts
├── fix-shared-ownership.ts
├── fk-indexes-001.ts
├── funding-interest-001.ts
├── google-id-001.ts
├── icp-config-001.ts
├── inflation-per-entity-001.ts
├── marcela-voice-001.ts
├── notification-logs-001.ts
├── property-photos-001.ts
└── research-config-001.ts
TypeScript migrations: {feature}-{sequence}.ts
001research-config-001.ts, funding-interest-001.tsSpecial migrations: Descriptive names without sequence numbers for one-off fixes:
fix-shared-ownership.ts, drop-plaid-001.tsExport convention: Each file exports a named function (not default):
// Standard pattern
export async function runMigration(): Promise<void> { ... }
// Or descriptive name
export async function runDbHygiene001() { ... }
export async function fixLegacyOwnership() { ... }
runMigrationsAndSeeds()Located in server/index.ts (called after HTTP server starts listening). Follows a strict ordering:
Migrations that don't depend on each other run in parallel for faster startup:
const [
{ runMigration: runResearchConfig001 },
{ runMigration: runInflationPerEntity001 },
// ... 9 more independent migrations
] = await Promise.all([
import("./migrations/research-config-001"),
import("./migrations/inflation-per-entity-001"),
// ...
]);
await Promise.all([
runResearchConfig001(),
runInflationPerEntity001(),
// ...
]);
Migrations that depend on Phase 1 results run one at a time:
// Must run after table-creating migrations
const { runNotificationLogs001 } = await import("./migrations/notification-logs-001");
await runNotificationLogs001();
// FK indexes must run after all tables exist
const { runFkIndexes001 } = await import("./migrations/fk-indexes-001");
await runFkIndexes001();
Data hygiene and legacy fixes:
const { runDbHygiene001 } = await import("./migrations/db-hygiene-001");
await runDbHygiene001();
const { fixLegacyOwnership } = await import("./migrations/fix-shared-ownership");
await fixLegacyOwnership();
After all migrations complete:
await seedAdminUser(); // Users first (FK dependency)
// Independent seeds run in parallel
await Promise.all([
seedMissingMarketResearch(),
seedMarketRates(),
seedDefaultLogos(),
seedUserGroups(),
seedFeeCategories(),
seedServiceTemplates(),
seedPropertyPhotos(),
seedGlobalAssumptions(),
]);
// Dependent seeds run sequentially
await seedCompanies();
await seedUserCompanyAssignments();
// Post-seed cleanup
await cleanOrphanedLogos();
Every TypeScript migration must be safe to run multiple times:
// CORRECT — idempotent
await db.execute(sql`
ALTER TABLE global_assumptions
ADD COLUMN IF NOT EXISTS research_config jsonb DEFAULT '{}'::jsonb
`);
// CORRECT — check before dropping
const result = await db.execute(sql`
SELECT constraint_name FROM information_schema.table_constraints
WHERE table_name = ${table} AND constraint_name IN (${drop}, ${keep})
`);
if (names.has(drop) && names.has(keep)) {
await db.execute(sql.raw(`ALTER TABLE "${table}" DROP CONSTRAINT "${drop}"`));
}
// WRONG — will fail on second run
await db.execute(sql`ALTER TABLE foo ADD COLUMN bar text`);
shared/schema.ts first (Drizzle schema is the source of truth)server/migrations/{feature}-{sequence}.ts:
import { db } from "../db";
import { sql } from "drizzle-orm";
const TAG = "{feature}-{sequence}";
export async function runMigration(): Promise<void> {
try {
await db.execute(sql`
ALTER TABLE {table}
ADD COLUMN IF NOT EXISTS {column} {type} DEFAULT {default}
`);
console.info(`[INFO] [${TAG}] Column added (or already existed)`);
} catch (error: any) {
console.error(`[ERROR] [${TAG}] Migration failed: ${error.message}`);
throw error;
}
}
runMigrationsAndSeeds() in server/index.ts:
npx drizzle-kit push before committingmigrations/ — they are immutable historyIF NOT EXISTS, check before drop)shared/schema.ts is always the source of truth for table structure| File | Purpose |
|---|---|
shared/schema.ts | Drizzle ORM schema definitions (source of truth) |
drizzle.config.ts | Drizzle Kit configuration |
migrations/ | Generated SQL migrations (immutable) |
server/migrations/ | TypeScript startup migrations |
server/index.ts:runMigrationsAndSeeds() | Migration orchestrator |