Migrations & Seeding
Migration workflow
cd packages/database
# 1. edit prisma/schema.prisma
# 2. create and apply the migration
pnpm db:migrate # prisma migrate dev — prompts for a name
# 3. rebuild so consuming services see the new types
cd ../.. && pnpm build --filter=@playpals/dbprisma migrate dev does three things: diffs the schema against the database, writes SQL into prisma/migrations/<timestamp>_<name>/migration.sql, applies it, and regenerates the client. Commit the generated migration directory.
The commands
| Command | Runs | When |
|---|---|---|
pnpm db:generate | prisma generate | After pulling a schema change without a new migration |
pnpm db:migrate | prisma migrate dev | Local development — creates and applies |
pnpm db:deploy | prisma migrate deploy | Production — applies existing migrations, never creates |
pnpm db:push | prisma db push | Throwaway local experiments only — no migration file |
pnpm db:seed | prisma/seed.ts | Populate demo data |
Never run db:push against a shared database
It mutates the schema without recording a migration, so the migration history no longer describes reality and the next migrate deploy fails or silently diverges. It is fine on your own laptop for a quick experiment you intend to throw away.
Migration rules
Never edit an applied migration. Once a migration has run anywhere other than your laptop it is immutable — Prisma records a checksum, and changing the file makes migrate deploy fail. Fix mistakes with a new migration.
Plan destructive changes in two steps. Renaming a column or dropping a table in a single migration means downtime, because the old code is still running while the new schema is live. Use expand/contract:
- Expand — add the new column, deploy code that writes both and reads the new one.
- Contract — a later migration drops the old column, once nothing reads it.
Watch for locks. Adding a column with a non-null default rewrites the table on older Postgres. Adding an index locks writes unless you use CREATE INDEX CONCURRENTLY, which Prisma will not generate — write it by hand in the migration SQL when the table is large.
Applying migrations in production
The API image does not run migrations at startup. Apply them deliberately:
kubectl run prisma-migrate --rm -it \
--image=registry.digitalocean.com/playpalzproduction/api:latest \
--env="DATABASE_URL=$DATABASE_URL" \
--restart=Never \
-- npx prisma migrate deploy --schema packages/database/prisma/schema.prismaOrder matters: apply an additive migration before deploying the code that needs it; apply a destructive migration after the code that stopped using the old shape is fully rolled out.
Always take a snapshot of the managed database first. See Runbooks.
Checking state
cd packages/database
pnpm exec prisma migrate status # applied vs pending
pnpm exec prisma validate # schema is valid
pnpm exec prisma format # canonical formattingPrisma configuration
packages/database/prisma.config.ts supplies the datasource URL and the seed command:
export default defineConfig({
datasource: { url: env("DATABASE_URL") },
migrations: {
seed: "ts-node --project ./prisma/tsconfig.seed.json ./prisma/seed.ts",
},
});schema.prisma itself declares only provider = "postgresql" — no url. The connection comes from this config for CLI operations, and from the driver adapter in src/client.ts at runtime.
Seeding
cd packages/database
pnpm db:seedThe seed produces a full demo environment rather than a handful of rows — users of each persona, posts of every type, channels, DMs, sessions, subscriptions, shop items, wallets and ledger entries, notifications, trending, and sponsors. Enough that every screen in the app has something to render.
What it does with media
Avatars and post images are fetched from pravatar and picsum, processed with Sharp, and uploaded to Spaces. Clip videos from assets/ are uploaded to Mux. The upload helpers fall back to source URLs when credentials are missing, so seeding works without Spaces or Mux access — you get remote URLs instead of your own CDN.
Safety properties
- Additive. Seeded accounts live in a dedicated
@playpalz.ggemail space so they cannot collide with existing rows. - Idempotent. Re-running aborts early if the seed data is already present.
- Games are not seeded. It queries the existing
Gamerows (populated byigdb-heartbeat) and links to them.
Seeded credentials
Every seeded account uses the password Password123!. Local demo data only — never reuse it anywhere real.
Related files
| File | Purpose |
|---|---|
prisma/seed.ts | The active seed |
prisma/seed/env.ts | Env loading for the seed |
prisma/seed/spaces.ts | Image processing and upload |
prisma/seed/mux.ts | Video upload |
prisma/seedStore.ts | Store products |
prisma/seedOLD.ts | Superseded — do not use |
Resetting locally
docker compose down -v # deletes the volume and all local data
docker compose up -d db redis
cd packages/database
pnpm db:migrate
pnpm db:seedPrisma also offers pnpm exec prisma migrate reset, which drops, re-migrates, and re-seeds in one step. Both are local-only operations.
