Migration Guide
How to change the Prisma schema safely, both locally and in production.
Convention
All migrations under prisma/migrations/<timestamp>_<name>/migration.sql are idempotent (IF NOT EXISTS, ADD COLUMN IF NOT EXISTS) so re-runs are safe. We do NOT use prisma migrate deploy against production — too risky for our scale + the Prisma Cloud is overkill. We write SQL by hand.
Workflow
1. Edit prisma/schema.prisma
Add the field, model, or relation.
2. Regenerate the client
npx prisma generate
This updates the TypeScript types so the rest of the codebase compiles.
3. Write the migration SQL by hand
Create a directory + file:
mkdir -p prisma/migrations/$(date +%Y%m%d)_<name>
$EDITOR prisma/migrations/$(date +%Y%m%d)_<name>/migration.sql
Use idempotent statements:
-- Good
ALTER TABLE "StoreOrder" ADD COLUMN IF NOT EXISTS "cancellationReason" TEXT;
CREATE INDEX IF NOT EXISTS "StoreOrder_status_idx" ON "StoreOrder"("status");
CREATE TABLE IF NOT EXISTS "EmailLog" ( ... );
-- Bad (non-idempotent)
ALTER TABLE "StoreOrder" ADD COLUMN "cancellationReason" TEXT;
CREATE INDEX "StoreOrder_status_idx" ON "StoreOrder"("status");
Idempotent migrations let you re-run safely if a deploy gets interrupted, and let you apply the same SQL across local + staging + prod.
4. Apply locally
PGPASSWORD=<local-password> psql -h localhost -U ka26user -d ka26 -f prisma/migrations/<dir>/migration.sql
5. Test the change
- Run unit tests
- Spot-check via Prisma Studio
- Run any tests that touch the changed tables
6. Apply to production
PGPASSWORD=Ka26Mkt2026 psql -h 34.123.40.64 -U ka26user -d ka26 -f prisma/migrations/<dir>/migration.sql
Verify:
PGPASSWORD=Ka26Mkt2026 psql -h 34.123.40.64 -U ka26user -d ka26 -c '\d "StoreOrder"'
7. Deploy code
Push the schema + code changes via normal CI/CD. The deploy contains the new types; the DB already has the new columns from step 6.
Order matters: DB before code
Always apply the migration BEFORE deploying code that reads from new columns. The reverse causes a brief window where the code expects fields that don't exist yet.
For column ADDITIONS this is forgiving — the old code just ignores the new column. For RENAMES or REMOVALS, you need a 2-deploy dance:
- Deploy code that reads BOTH old + new column (writes new)
- Backfill old → new in DB
- Deploy code that reads new only
- Drop old column
Examples (today's migrations)
| Migration | What | Pattern |
|---|---|---|
20260417_add_email_log/migration.sql | New EmailLog table | CREATE TABLE + indexes |
20260417_doctor_password_reset/migration.sql | Add resetToken + resetTokenExp to Doctor | ALTER TABLE ... ADD COLUMN IF NOT EXISTS |
20260417_store_order_cancellation_reason/migration.sql | Add cancellationReason to StoreOrder | ALTER TABLE ... ADD COLUMN IF NOT EXISTS |
Rollback strategy
Postgres doesn't have transactional DDL across statements + Cloud SQL doesn't expose a "previous state" snapshot beyond backups.
For ADDITIONS — there's nothing to rollback (the new column is just unused).
For DROPS or RENAMES — rollback means restoring from backup OR running an inverse migration (which means having written one upfront — recommended for any destructive migration).
Tests for migrations
Add a test that asserts the migration SQL exists + has the expected statements:
it("StoreOrder has cancellationReason field", () => {
const schema = read("prisma/schema.prisma");
const block = schema.match(/model\s+StoreOrder\s*\{[\s\S]*?^\}/m)?.[0] || "";
expect(block).toMatch(/cancellationReason\s+String\?/);
});
it("migration SQL exists", () => {
const rel = "prisma/migrations/20260417_store_order_cancellation_reason/migration.sql";
expect(fs.existsSync(path.join(ROOT, rel))).toBe(true);
expect(read(rel)).toMatch(/ADD COLUMN\s+IF NOT EXISTS\s+"cancellationReason"/);
});
This way if someone reverts the migration accidentally, the test catches it.