Litestore · Developer guide
Database

Migrations

This page shows you how Litestore creates, applies and resets database migrations, and what the seed script puts in a new database.

Litestore builds and changes its database through committed migrations. Read this page before you change the schema, set up a new database, or deploy to production.

prisma/migrations/ contains two committed migrations. The first is a baseline that creates the whole schema and installs the money, refund and stock guarantees. The second, added later, creates three indexes.

Litestore relies on migrations for a concrete reason. The money guarantees are CHECK constraints, triggers and views, and a push that only compares schemas would never create them. They exist only because a migration installs them.

If you used prisma db push instead, you could reproduce every table and column in this repository and still end up with a database that accepts a refund larger than the order it refunds.

Order #1042Paid $120.00
Refunded $80.00Requested $60.00
Write rejectedTotal refunded can't exceed the order total. Nothing was saved.
With the baseline migration applied, Postgres rejects a refund that would take back more than the order was paid.

What is committed

The baseline

20260726143000_baseline_with_db_guarantees

This migration is 3,121 lines long.

It first creates the schema from scratch, including 19 enum types and all tables with their indexes. Then it installs the parts that a schema diff can't produce:

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

These are the six views:

OrderMoneyTotals
OrderMoneyDrift
CustomerMoneyTotals
CustomerMoneyDrift
InventoryStockTotals
InventoryLedgerDrift

The performance indexes

20260730090000_performance_indexes

This migration has three statements, and each one is a 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".

The prisma/sql/ folder exists but is empty, because Litestore doesn't use Prisma TypedSQL.

The commands

bun run db:migrate

This runs prisma migrate dev. Use it when you change the schema locally. It writes a new migration into prisma/migrations/, applies it, and regenerates the client in the same step.

bun run db:deploy

This runs prisma migrate deploy. Use it in production and CI. It only applies pending migrations. It never creates a migration, never asks you a question, and never resets the database.

bun run db:push

This also runs prisma migrate dev, with a script body identical to db:migrate. The name is left over from an older setup. db:push does not run prisma db push.

bun run db:reset

This runs prisma migrate reset, which you use to rebuild a local database. It drops the database, replays every migration, and then runs the seed.

bun run db:seed

This runs tsx prisma/seed.ts. Use it to seed the database without dropping anything.

bun run db:generate

This runs prisma generate. Use it to regenerate the client after a schema change that you didn't migrate yourself.

db:reset drops the database

prisma migrate reset deletes every row before it replays the migrations. Only use it on a local development database. If you point it at a DATABASE_URL that contains real orders, it destroys them.

What keeps the scripts honest

Two harness assertions stop these scripts from drifting. scripts/check-guarantees-contract.ts requires the following:

  1. db:migrate contains prisma migrate dev.
  2. db:deploy contains prisma migrate deploy.

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

The check runs inside bun run verify. You find out about drift at that gate, before a production database turns out to be missing a guarantee.

Seeding

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

The script doesn't use ~/services/db, so it runs without the soft-delete extension.

Every write is an upsert keyed on a natural key. You can run the script again without creating duplicate rows.

According to the summary the script prints at the end, it creates the following:

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

What the seed deliberately skips

The seed doesn't create orders. It prints this message instead:

Skipping order seeding (orders created through checkout)

For the same reason, it also skips refund requests, refund request items, returns, coupon usage and shipping. Those rows come from the flows that create them, and the money constraints apply to them. If the seed wrote an order straight into the table, that order would sit under constraints that assume a checkout produced it, when no checkout did.

Signing in

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

You sign in with a magic link that Resend delivers. To get into /admin, you need a working RESEND_API_KEY.

Two entry points, one file

Two seed entry points run the same file with different runners.

prisma.config.ts sets migrations.seed to bun prisma/seed.ts. This is the command that prisma migrate reset runs.

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

package.json also still contains the legacy block below, which Prisma 7 no longer reads:

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

README.md is out of date on this

In two places, README.md says 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 out of date. The migration history is committed, and it is the only place where the database guarantees are defined. No script runs prisma db push, and a harness check fails the build if one starts to.

The README's setup instructions still work. They tell you to run bun run db:push and then bun run db:seed, and since db:push runs prisma migrate dev, you end up applying the committed migrations instead of pushing a schema diff.

When the README and the scripts disagree, follow the scripts.

Changing the schema

  1. Edit prisma/schema.prisma.

  2. Create the migration. Run this command:

    bun run db:migrate
  3. Review the generated SQL before you commit it.

If your change touches money, refunds or stock, run two more checks. Each one answers a different question.

bun run lint:guarantees

This checks the migration SQL and doesn't need a database. It tells you the migration still says the right thing.

bun run db:guarantees:audit

This checks the database you applied the migration to. It tells you a real database agrees.

Which command to use

When you change the schema locally, use db:migrate. It is the only command that writes a migration.

When you apply someone else's committed migrations in CI or production, use db:deploy.

When a local database has drifted or broken and you want to rebuild it, use db:reset. Only run it against a database whose contents you are willing to lose.

When you pull a schema change that you didn't migrate yourself, use db:generate.

All four follow from the rule that the baseline migration exists to enforce. If a guarantee isn't in a migration, the database isn't enforcing it.

On this page