Use when working in a project that depends on @prisma/orm-postgres, @prisma/orm-sqlite, or @prisma/orm-mongo (Prisma 8, formerly Prisma Next): editing contract.prisma or a contract.ts builder, running `prisma contract emit`, planning or applying migrations, editing migration.ts, writing db.orm / db.sql / db.query queries, wiring db.ts or middleware, integrating a build tool, using the Supabase extension or RLS, or reading a dotted error code such as MIGRATION.HASH_MISMATCH. Use when the user asks "what is Prisma 8", "where do I start", or compares it to another ORM. Use when the user asks to upgrade or bump Prisma 8 in an app or an extension package. Use when you see @internal/* or @prisma/orm-* imports, prisma.config.ts with definePrismaConfig, or contract.json / contract.d.ts. Do not use for Prisma ORM 7 or earlier (schema.prisma + @prisma/client).
Load this guide when db.ts imports from @internal/mongo/runtime.
Shared concepts (result consumption, script teardown, cross-target pitfalls, capability gaps) live in queries.md.
Key Concepts
Mongo (mongo<Contract>(...) from @internal/mongo/runtime):
db.orm.<root> — ORM, lowercased plural contract root (db.orm.users, db.orm.posts). Same fluent chaining; .where({ field: value }) object equality is the idiomatic filter form.
db.query — typed aggregation-pipeline builder. Start with db.query.from('<root>'), chain .match(...) / .project(...) / .group(...) / .lookup(...), terminal with .build(). Run via (await db.runtime()).query(plan) for anything that returns documents; execute(plan) is only for a write you want an affected count from, and it throws RUNTIME.MONGO_STATISTICS_UNSUPPORTED on a find or aggregate.
Reach for the ORM first; drop to db.query when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app.
Filtered writes after .match(...); plans execute through the runtime.
Workflow — ORM reads
The concept matches Postgres — db.orm.<root> returns a collection you compose method-by-method — but roots are lowercased plurals from the emitted contract (users, posts, not User / Post), and filters are usually object equality:
// src/queries/users.ts — adjust the relative import to match file depth.
import { db } from '../prisma/db';
// All users.
const users = await db.orm.users.all();
// Single row by equality filter.
const alice = await db.orm.users.where({ email: 'alice@example.com' }).first();
// Projection, sort, pagination — same chaining as Postgres.
const recent = await db.orm.posts
.select('title', 'authorId', 'createdAt')
.orderBy({ createdAt: -1 })
.limit(10)
.all();
.where(...) accepts a plain object whose keys are model field names and values are compared with equality (codec-aware — ObjectId fields accept string ids from the contract). Chain multiple .where({ ... }) calls to AND-compose filters.
For operators the object form doesn't cover (.in([...]), range comparisons, nested logic), pass a MongoFilterExpr — today that means importing filter helpers from @prisma/orm-mongo/query-ast/execution (a façade-completeness gap; see What Prisma 8 doesn't do yet in queries.md). Prefer the object form whenever equality suffices.
Polymorphic roots. When the contract declares variants on a model, narrow before querying:
Sorting and pagination..orderBy({ field: 1 | -1 }) (Mongo sort directions). .limit(n) maps to $limit; .offset(n) maps to $skip.
.first() vs .all()..first() issues a limit-1 read; .all() returns every matching document. There is no .first({ pk }) shorthand on Mongo — filter on _id explicitly: .where({ _id: id }).first().
Mongo .all() returns the same AsyncIterableResult shape as Postgres — await db.orm.users.all() yields an array; see Consuming the result in queries.md.
Workflow — Eager-loading relations (.include)
Mongo reference relations eager-load through the same .include('<relation>') surface; the ORM lowers to $lookup:
Relation names match the contract's @relation field names. Nested includes follow the same chaining rules as the parent collection.
Workflow — ORM writes
Mongo mutations require a preceding .where(...) filter (except .create / .createAll). Updates accept either a partial document or a field-accessor callback for Mongo operators:
// Create — returns the row with server-assigned `_id`.
const user = await db.orm.users.create({
name: 'Alice',
email: 'alice@example.com',
bio: null,
address: null,
});
// Update one — plain object replaces top-level fields.
await db.orm.users.where({ _id: user._id }).update({ bio: 'Writer' });
// Update one — field operations ($push, $inc, dot-path $set).
await db.orm.users
.where({ _id: user._id })
.update((u) => [u.tags.push('admin'), u.loginCount.inc(1)]);
// Update many / delete many — iterate or count.
const updated = await db.orm.users
.where({ bio: null })
.updateAll({ bio: 'filled' });
for await (const row of updated) { /* each modified doc */ }
await db.orm.users.where({ _id: user._id }).delete();
// Upsert — filter via .where(), split create vs update branches.
await db.orm.users.where({ email: 'alice@example.com' }).upsert({
create: { name: 'Alice', email: 'alice@example.com', bio: null, address: null },
update: { bio: 'Editor' },
});
Count-only terminals..createAndCount(...), .updateAndCount(...), .deleteAndCount() return numbers without re-reading full documents — useful for bulk operations where you only need the modified count.
Upsert + dot-path. The upsert update callback cannot use dot-path field operations — use top-level field replacement in the upsert branch or a separate .update((u) => [...]) call.
Workflow — Aggregates
The Mongo ORM does not expose .aggregate(...) / .groupBy(...). Express aggregations through db.query — the pipeline builder — with .group(...) and accumulator helpers:
Import acc and expression helpers (fn) from @prisma/orm-mongo/query-builder when building computed pipeline stages (as examples/mongo-demo/src/server.ts does).
Workflow — Query builder (db.query)
The concept: db.query.from('<root>') starts a typed aggregation-pipeline chain. Terminal methods produce a MongoQueryPlan; run it through the runtime with query(plan) (an AsyncIterableResult of documents — await it for an array):
Filters — .match(...). Callback form: .match((f) => f.status.eq('active')). Filters AND-compose across chained .match(...) calls. Field accessors support property access (f.email), callable dot paths (f('address.city').eq('NYC')), and f.rawPath('path') for migration/backfill paths outside the current contract.
Write terminals on the builder. After .from('users') or .from('users').match(...), use insert/update/delete terminals. Write plans run through query(plan) too (one result row carrying the driver's response — the inserted ids, or the document findOneAndUpdate returns); reach for execute(plan) only on an update* / delete* plan when all you want is the affected count (any other command kind throws RUNTIME.MONGO_STATISTICS_UNSUPPORTED):
Update callbacks return arrays of field operations (.set, .inc, .push, .pull, …). Pipeline-style updates use f.stage.set(...) inside an aggregation chain, then .updateMany() with no callback.
Plans vs ORM. The ORM's .create / .update / .all issue queries directly. Don't pass ORM collections to runtime.query / runtime.execute — those entry points are for db.query plans (and migration/runtime internals).
Common Pitfalls (Mongo)
Reaching for the lower-level lane when the ORM would have done. Default to the ORM; drop to db.query only for shapes the ORM can't express.
Using .all() when you wanted one row. Use .where({ ... }).first() — not .all().
Calling .update() / .delete() without .where(). Mutations other than .create / .createAll require a filter — the compiler enforces this at the type level where possible.
Using PascalCase model names on ORM. Roots are lowercased plurals from the contract (db.orm.users, not db.orm.User).
Expecting Postgres-style lambda .where((u) => u.email.eq(...)) on ORM. Prefer object equality .where({ email: '...' }); richer operators need MongoFilterExpr helpers (façade gap today).
Expecting db.transaction(...). The Mongo façade does not expose it today. Multi-document atomicity requires MongoDB transactions on a replica set via the driver — not yet wrapped in the Prisma 8 façade. Route to What Prisma 8 doesn't do yet / references/feedback.md if the user needs this.
Trying to use db.sql. There is no db.sql on Mongo.
Trying to db.execute(plan) directly, or reading documents with execute. Run query-builder plans via (await db.runtime()).query(plan). execute(plan) resolves statistics only and throws RUNTIME.MONGO_STATISTICS_UNSUPPORTED for a find or aggregate.
Expecting ORM .aggregate(...) / .groupBy(...). Use db.query.from(...).group(...).build() instead.