Data · We run it ourselves
Prisma is a schema definition language for relational databases that generates a typed client and emits versioned SQL migration files.
A schema file describes what a database looks like today. A migrations folder describes how it came to look that way, in the exact statements that were executed, in the order they were executed — which is the difference between reconstructing a decision and asking whoever was on the project whether they remember it. Data Nexus uses Prisma for that history and for the compile-time contract the generated client gives a monorepo, and drops to hand-written SQL wherever the query builder would either lie about PostgreSQL or produce a plan worse than the one a person would write.
01
A migration directory is an audit trail that survives staff turnover. The mozg platform carries one initial migration of 2,223 lines of SQL and nine dated ones after it — a foreign key and snapshot column added to clan challenges, password reset tokens, profile visibility, leaderboard indexes, a nullable CV job artefact, achievement rank indexes. Each is a directory holding the statement that actually ran against production. Reading git blame on a schema file yields the last person to touch a line; reading the migration folder yields the date, the statement and the change it belonged to.
02
The migration_lock.toml file pins the provider to postgresql and the emitted files are PostgreSQL SQL, not a portable dialect. This is treated as the point rather than a compromise: the datasource declares extensions = [ltree] under the postgresqlExtensions preview feature, GIST indexes are declared in the schema itself as @@index([ontologyLtree], type: Gist), and no part of the model layer maintains the fiction that the database could be swapped for MySQL next quarter. A schema layer that is honest about its target produces SQL a database administrator can read before it is applied.
03
The generated client is a build artefact, never a committed dependency to be trusted. The mozg CI gate runs prisma generate in contracts/pg-schema before ESLint and again before the type check, so a renamed column fails the build of every application in the turborepo that reads it, at type-check time, rather than at the first request that touches the row. In a monorepo where an API, several front ends and a seeding package share one database, that is the only mechanism that makes a schema edit visibly expensive at the moment it is made.
04
Migration runs as its own container with its own exit code. infra/docker/db-migrate/entrypoint.sh calls prisma migrate deploy; the API process does not migrate on boot. An application that migrates itself at startup will, under a rolling deploy, run several copies of the same migration against the same database and rely on advisory locking to sort it out. Separating the step means a failed migration halts the release before any new application container serves a request, and the failure has a log of its own to read.
05
Seventy-nine models are split across ten schema files by domain — base, auth, content, learning, essay, gamification, ai, geo, social, analytics — using Prisma's schema folder support rather than a single file every branch edits. The practical effect is on merges: two people working on gamification and on content collide inside their own domain, and do not collide with each other over line 1,400 of a monolithic file.
06
Where the query builder would be worse than a person, we write SQL and say so. There are 45 raw call sites in mozg, and they cluster exactly where the work is arithmetic over history — the achievement trigger service counting corrected errors and recovery candidates, leaderboard statistics, the admin dashboard aggregating error rates by topic, the knowledge map counting tasks per topic. The rule that stops this becoming a general escape hatch is that structure has exactly one definition: the schema owns tables, columns, indexes and relations, raw SQL is used for reads with a hand-declared return type, and writes stay in the typed client so that migrations remain the only thing changing shape.
07
Prisma is at its most useful in the places it refuses to model. The ontology in mozg is a PostgreSQL ltree column on subjects and topics, GIST-indexed, and the schema declares it as Unsupported("ltree"). That declaration carries the column through every migration and every index without ever claiming to understand it — the tree stays in the database where the ltree operators live, and the client offers no imitation typed accessor that would quietly do the wrong thing. An ORM that admits a gap is more useful than one that papers over it.
An entry that cannot point at something you can open, run or read does not compile. That is a property of the type, not a promise in a paragraph.
Invariant
Prisma migrate is forward-only: there are no down migrations, so reversing a change means writing the inverse SQL by hand as a new forward migration, under exactly the time pressure that makes hand-written SQL a bad idea. Each migration file executes inside a single transaction, which rules out CREATE INDEX CONCURRENTLY — PostgreSQL rejects it inside a transaction block with error 25001 — so building an index on a large, hot table without locking writes has to be run outside Prisma with psql and then registered using prisma migrate resolve --applied, which means the migration history and the database are briefly out of step by hand. The type safety also stops precisely where PostgreSQL becomes interesting: the ltree columns in mozg are invisible to the generated client, so every ontology traversal is raw SQL with a return type asserted by a person, and the compiler will check that assertion for internal consistency and never for truth. On a project of a dozen tables with no PostgreSQL-specific features, the schema DSL, the generate step and the migration ceremony cost more than they return — which is why carfleet-main and status-kvo-app run on Supabase with no Prisma in them at all.