Litestore · Developer guide

Migrations

Learn what the two committed migrations under prisma/migrations install, how db:migrate, db:deploy and db:reset differ, what prisma/seed.ts creates, and where README.md contradicts the scripts.

Litestore is migration-driven. prisma/migrations/ holds two committed migrations: a baseline that creates the whole schema and installs the money, refund and stock guarantees, and a later one that adds three indexes.

That choice is load-bearing rather than stylistic. The money guarantees exist only because they are in a migration — they are CHECK constraints, triggers and views that no schema-diffing push would ever create.

A prisma db push workflow can reproduce every table and column in this repository and still produce a database that accepts a refund larger than the order it refunds.

What is committed

The baseline

20260726143000_baseline_with_db_guarantees

3,121 lines.

It creates the schema from scratch — 19 enum types, the tables and their indexes — and then installs the parts that a schema diff cannot produce:

  • 20 CHECK constraints
  • four litestore_guard_* trigger functions, with their triggers
  • the case-insensitive unique indexes — user_email_lower_unique, product_variant_sku_upper_unique, product_slug_lower_unique and the rest
  • six views

The six views are:

OrderMoneyTotals
OrderMoneyDrift
CustomerMoneyTotals
CustomerMoneyDrift
InventoryStockTotals
InventoryLedgerDrift

The performance indexes

20260730090000_performance_indexes

Three statements, all CREATE INDEX IF NOT EXISTS:

OrderItem_variantId_idx
Customer_createdAt_idx
Media_url_idx

The rest of the directory

prisma/migrations/migration_lock.toml records provider = "postgresql".

prisma/sql/ exists but is empty — Litestore does not use Prisma TypedSQL.

The commands

bun run db:migrate

Runs prisma migrate dev. This is the command for a local schema change: it writes a new migration into prisma/migrations/, applies it, and regenerates the client in the same step.

bun run db:deploy

Runs prisma migrate deploy. This is the production and CI command. It applies pending migrations only — it never generates a migration, never prompts, and never resets.

bun run db:push

Also runs prisma migrate dev. It does nothing that db:migrate does not already do; the name is a leftover and the script body is identical. db:push does not run prisma db push.

bun run db:reset

Runs prisma migrate reset, for rebuilding a local database. It drops the database, replays every migration, then runs the seed.

bun run db:seed

Runs tsx prisma/seed.ts, for seeding without dropping anything.

bun run db:generate

Runs prisma generate, for regenerating the client after a schema change you did not migrate yourself.

db:reset drops the database

prisma migrate reset deletes every row before it replays the migrations. It is a local development command. Pointing it at a DATABASE_URL that holds real orders destroys them.

What keeps the scripts honest

Two harness assertions keep this arrangement from drifting. scripts/check-guarantees-contract.ts requires:

  1. db:migrate to contain prisma migrate dev
  2. db:deploy to contain prisma migrate deploy

It also fails the build if the string prisma db push appears anywhere in package.json scripts.

It runs inside bun run verify, so the drift is caught at the gate rather than discovered when a guarantee turns out to be missing in production.

Seeding

prisma/seed.ts is a 2,482-line script that builds its own PrismaClient over PrismaPg from process.env.DATABASE_URL.

It does not use ~/services/db, so it runs without the soft-delete extension.

Every write is an upsert keyed on a natural key, so re-running it does not duplicate rows.

Its own closing summary states what it produces:

  • Two rows for each of 41 base models
  • 20 additional products with 80+ variants
  • 12 email templates

What the seed deliberately skips

Orders are not seeded. The script prints:

Skipping order seeding (orders created through checkout)

It skips refund requests, refund request items, returns, coupon usage and shipping for the same reason: those rows are produced by the flows that create them, and are subject to the money constraints. A seeded order written straight to the table would be an order that no checkout produced, sitting under constraints that assume one did.

Signing in

The seed creates an admin account for admin@example.com.

Sign-in is a magic link delivered by Resend, so a working RESEND_API_KEY is what actually gets you into /admin.

Two entry points, one file

There are two seed entry points naming the same file with different runners.

prisma.config.ts sets migrations.seed to bun prisma/seed.ts, which is what prisma migrate reset invokes.

package.json maps db:seed to tsx prisma/seed.ts.

package.json also still carries the legacy block that Prisma 7 no longer reads:

"prisma": { "seed": "bun prisma/seed.ts" }

README.md is out of date on this

README.md states, in two places, that the repository is not migration-driven:

Prisma schema is current, but local setup uses prisma db push rather than a committed migration history

There is no committed Prisma migration history that should be treated as authoritative local bootstrap state. Do not document this repo as migration-driven unless that changes in the codebase.

Both statements are stale:

  • The migration history is committed
  • It is the only place the database guarantees are defined
  • prisma db push is not in any script
  • A harness check fails the build if it reappears

Its setup instructions — bun run db:push then bun run db:seed — still work, because db:push runs prisma migrate dev. But they apply the committed migrations rather than pushing a schema diff.

Follow the scripts, not the README prose.

Changing the schema

Edit prisma/schema.prisma, then run:

bun run db:migrate

Review the generated SQL before committing it.

If the change touches money, refunds or stock, there are two checks and they answer different questions.

bun run lint:guarantees

Checks the migration SQL, without a database.

bun run db:guarantees:audit

Checks the database you actually applied it to.

The first tells you the migration still says the right thing. The second tells you a real database agrees.

Which command to reach for

Changing the schema locally is db:migrate — and that is the only command that writes a migration.

Applying someone else's committed migrations, in CI or production, is db:deploy.

Rebuilding a local database that has drifted or broken is db:reset, and only ever against a database whose contents you are willing to lose.

Pulling a schema change you did not migrate yourself is db:generate.

The rule underneath all four is the one the baseline migration exists to enforce:

A guarantee that is not in a migration is not a guarantee the database is making.

On this page