Litestore · Developer docs
Database

Migrations

Create, apply and reset database migrations, and seed 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.

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.

Migrations and guarantees

prisma/migrations/ contains the committed migration history. The first migration is a baseline that creates the whole schema and installs the money, refund and stock guarantees, and every later migration changes the schema from there.

Litestore needs migrations because the money guarantees are CHECK constraints, triggers and views, and a push that only compares schemas would never create them. The baseline migration is the only thing that installs them.

If you used prisma db push instead, you'd get every table and column in this repository, but the database would accept a refund larger than the order it refunds.

Migration files

migration_lock.toml

Baseline migration

The 20260726143000_baseline_with_db_guarantees migration is 3,121 lines long.

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

What it installsDetails
CHECK constraintsThe migration adds 20 of them.
Trigger functionsThe migration adds four litestore_guard_* functions, with their triggers.
Case-insensitive unique indexesExamples are user_email_lower_unique, product_variant_sku_upper_unique and product_slug_lower_unique.
ViewsThe migration creates six views, listed below.
OrderMoneyTotals
OrderMoneyDrift
CustomerMoneyTotals
CustomerMoneyDrift
InventoryStockTotals
InventoryLedgerDrift

Performance indexes

The 20260730090000_performance_indexes migration has three statements, and each one is a CREATE INDEX IF NOT EXISTS:

OrderItem_variantId_idx
Customer_createdAt_idx
Media_url_idx

Later migrations

Every migration after these two is a small, named step, and the names say what changed: for example 20261012000000_pages_content_drop_blocks_ads_social moved storefront pages to HTML content and dropped the block, ad and social tables, 20261013000000_order_cancelled_at added the cancellation timestamp, and 20261014000000_settings_to_code removed the settings that became constants in platform/config/. Some of them also backfill or correct data, so apply them in order with db:deploy.

Partial unique indexes

The baseline creates partial unique indexes that Prisma's schema language can't express, such as channel_one_default_active_unique (one active default channel). Prisma therefore always sees them as drift, and db:migrate proposes DROP INDEX lines for them. Delete those lines from every new migration before you apply it. The comment at the top of 20261011000000_catch_up_indexes/migration.sql explains this. (The second index it names, on EmailTemplate, went away with that table.)

Other files

prisma/migrations/migration_lock.toml records provider = "postgresql". Litestore doesn't use Prisma TypedSQL, so there is no prisma/sql/ folder.

Commands

CommandWhat it runsWhen you use it
bun run db:migrateprisma migrate dev --create-onlyYou changed the schema locally. It writes a new migration into prisma/migrations/ but doesn't apply it, so the SQL is there to read and edit first.
bun run db:deployprisma migrate deployYou apply migrations locally after db:migrate, and in production. It only applies pending migrations, without creating migrations, prompting you or resetting the database.
bun run db:resetprisma migrate resetYou rebuild a local database. It drops the database, replays every migration, and then runs the seed.
bun run db:seedbun prisma/seed.tsYou seed the database without dropping anything.
bun run db:generateprisma generateYou regenerate the client after a schema change that you didn't migrate yourself.

No script runs prisma db push, and you shouldn't run it yourself, because it skips the constraints, triggers and views that only the migrations install.

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.

Schema changes

Schema edit

Make your change in prisma/schema.prisma.

Migration file

bun run db:migrate

SQL review

Read the SQL before you commit it, and delete any DROP INDEX lines for the partial unique indexes.

Apply and test

bun run db:deploy
bun run test:integration

db:deploy applies the new migration to your local database. If the migration touches money, refunds or stock, run the integration tests too. test/integration/db-guarantees.test.ts checks that Postgres still rejects the writes the guarantees forbid. The test database needs the migration as well, so apply it there before you run the tests.

Seeding

prisma/seed.ts is a 2,211-line script. It builds its own PrismaClient over PrismaPg using process.env.DATABASE_URL instead of importing ~/platform/db/client, so it runs without the soft-delete extension. It refuses to run when NODE_ENV is production unless you set SEED_ALLOW_PRODUCTION=1.

Almost every write is an upsert keyed on a natural key, so running the script again doesn't create duplicate rows. According to the summary the script prints at the end, it creates two rows for each of 41 base models, plus 20 additional products with 80+ variants.

Skipped data

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

Skipping order seeding (orders created through checkout)

It also skips refund requests, refund request items, returns, coupon usage, shipments and manual orders. These rows should only be created by the flows that normally produce them, because the money constraints apply to them. The constraints assume an order came from a checkout, so an order inserted directly by the seed wouldn't match the data they require.

Admin account

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

You sign in with a magic link. Outside production the email, including the link, is printed in the terminal running bun run dev instead of being sent; in production Resend delivers it.

Inbox
Sign in to Your StoreTo ava@example.com
now
YYour sign-in linkClick the button below to sign in. No password needed.Sign inDidn't ask for this? You can ignore this email.
You sign in to the seeded admin account with a magic link sent to admin@example.com.

Seed command

bun run db:seed runs bun prisma/seed.ts. prisma.config.ts sets migrations.seed to the same command, which is what prisma migrate reset runs after it replays the migrations.

Quick reference

All four answers come from the same rule: the database only enforces guarantees that a migration has installed.

On this page