GenerateSaaS

Schema changes

Generate, review, and apply Drizzle migrations - the scripts, the db:deploy step every deploy runs, and the checks to make before narrowing a money column.

The schema files under packages/database/src/db/ are the source of truth. Local databases are pushed to match them; shared and production databases are shaped by migration files in packages/database/drizzle/, committed alongside the schema change that produced them.

Scripts

Run each with pnpm --filter @repo/database <script>.

ScriptWhat it doesUse it for
setupPushes the schema straight to the databaseLocal development
generateDiffs the schema files and writes a migration into packages/database/drizzle/Every change bound for a shared or production database
migrateApplies pending migrationsShared and production databases
pushThe same sync as setup, without the rest of itLocal iteration only
studioOpens Drizzle Studio to browse and edit tablesInspecting data
deployThe production apply step - see belowDeploys (already wired in)
resetDESTRUCTIVE: drops and recreates the public schemaLocal resets only

Changing the schema

Edit the table in packages/database/src/db/schema.ts.
Generate the migration: pnpm --filter @repo/database generate
Review the SQL it wrote under packages/database/drizzle/, then commit it alongside the schema change.
Apply it: pnpm --filter @repo/database migrate

Locally, pnpm db:setup is enough - it pushes without writing a migration. Anything a teammate or a production database will see needs the file, so generate it before you ship.

The GenerateSaaS boilerplate repo itself is push-only: its own migration history never ships, so it iterates with db:setup. Your generated project is the one that keeps migrations.

What db:deploy runs

init prepends pnpm -F @repo/database run deploy to the production start script (or to the Vercel build), so it runs on every deploy.

ModeSteps
DefaultReconcile the baseline, drizzle-kit migrate, then verify
--push (demo deploys)drizzle-kit push --force, then verify

Verify is the last step either way. It re-reads the live database and fails the boot naming any table or column that differs from the schema files - so an apply that reported success having changed nothing never reaches runtime.

Baseline reconciliation. When drizzle.__drizzle_migrations is empty and every table the baseline migration creates already exists, db:deploy records that baseline as applied instead of executing it - so a baseline generated against an already-pushed database never fails with relation already exists.

Projects generated on 3.3.0-3.4.1

Those releases shipped no drizzle/ directory, and a push shaped the database.

SituationWhat db:deploy doesWhat you do
No drizzle/ directoryWarns once, falls back to push + verifyRun pnpm --filter @repo/database generate once and commit the baseline
drizzle/ present but holds no .sqlAborts: No migrations found in packages/database/drizzleRun generate
Baseline present, no migration recorded yet, every table it creates already existsRecords the baseline as applied, runs only later migrationsNothing
Baseline present, only some of its tables existAborts and names the missing tablesGenerate the baseline from the schema the database was pushed with, then generate again for the difference

Generate the baseline before you merge a schema change from an update, so it matches the pushed database.

Before narrowing a money column

Postgres refuses a rewrite when a value does not fit the new type - it never truncates one silently, so no balance is ever quietly clipped. What you get is a bare error that names nothing:

ERROR: numeric field overflow (SQLSTATE 22003)
DETAIL: A field with precision 16, scale 6 must round to an absolute value less than 10^10.

Run this first to find the offending rows yourself, adjusting the columns and the ceiling to the change you are making:

SELECT 'billing_logs' AS table_name, 'amount' AS column_name, count(*)
FROM billing_logs WHERE abs(amount) >= 10000000000
UNION ALL
SELECT 'users', 'credits', count(*) FROM users WHERE abs(credits) >= 10000000000;

Settle, correct or write down every row it returns before deploying. Do not "fix" it by widening the ceiling back - a balance clipped to fit is money the ledger stops accounting for.

packages/database/src/db/auth.ts is generated by Better Auth - never edit it by hand. Add a column by declaring it in additionalFields inside packages/auth/src/config.ts, then run pnpm --filter @repo/database auth:generate. The CUSTOM-marked numeric credits columns and extra indexes are hand-applied tweaks - re-apply them after each regeneration. Never run reset against a production database.

On this page