prisma-next-runtime
Wire the Prisma Next runtime — `db.ts` setup using `postgres<Contract>(...)` from `@prisma-next/postgres/runtime`, `sqlite<Contract>(...)` from `@prisma-next/sqlite/runtime`, or `mongo<Contract>(...)` from `@prisma-next/mongo/runtime`; middleware composition (telemetry from `@prisma-next/middleware-
By prisma · 1,312 installs
npx skills add prisma/prisma-next --skill prisma-next-runtime
Source repository · Upstream listing
Prisma Next — Runtime ( db.ts Wiring)
Edit your data contract. Prisma handles the rest.
This skill covers the runtime entry point — db.ts — and how to compose the database client with extensions, middleware, and environment configuration.
When to Use
User is wiring up db.ts for the first time (post init).
User wants to add middleware (telemetry, lints, budgets, custom).
User wants per environment config (dev vs prod, multi region).
User wants to switch between the Postgres, SQLite, and Mongo façades.
User wants to wrap operations in db.transaction(...) (Postgres and SQLite).
User is running a one off script ( tsx my script.ts , Node CLI, CI task) and the process won't exit after queries finish, or they need script teardown ( db.close() , await using ).
User mentions: db.ts, postgres(), mongo(), middleware, telemetry, lints, budgets, DATABASE URL, .env, connection pool, poolOptions, dev vs prod, transactions, read replicas, multi database, script won't exit, hangs, db.close, db.end, close connection, pool.end, await using .
When Not to Use
User wants to write queries → prisma next queries .
User is on Supabase — the supabase() role first factory, asUser(jwt) / asAnon() / asServiceRole() , JWT config, RLS → prisma next supabase .
User wants to edit the contract → prisma next contract .
User wants to wire Prisma Next into a build tool (Vite plugin, Next.js, …) → prisma next build .
User wants to debug a connection / runtime error → prisma next debug .
User wants to file a bug or feature request → prisma next feedback .
Key Concepts
db.ts is the runtime entry point. Imports the runtime factory from the @prisma next/<target façade ( @prisma next/postgres/runtime , @prisma next/sqlite/runtime , or @prisma next/mongo/runtime ), the contract artefacts ( contract.json + the Contract type from contract.d.ts ), and any middleware. Exports a db value the rest of your app imports.
The façade's runtime factory is the only surface user authored db.ts imports from. Each factory is a default export. For Postgres: import postgres from '@prisma next/postgres/runtime' ; SQLite: import sqlite from '@prisma next/sqlite/runtime' ; Mongo: import mongo from '@prisma next/mongo/runtime' . The factory signature is <Target <Contract (options) — a single type parameter (the Contract type from contract.d.ts ), and one options object.
Lazy connect. The factory does not connect to the database synchronously. Static query surfaces ( db.sql , db.orm ) are available immediately; the driver / pool is instantiated on the first call that needs a runtime (or when you explicitly call await db.connect({ url }) ). This is why db.ts can be imported in modules that load before the env is ready.
Middleware composes in order. The first middleware in the middleware: [...] array runs outermost — it sees the operation first on the way in and last on the way out. Telemetry first means budget / lint failures show up inside telemetry spans.
prisma next.config.ts vs .env . The config ( defineConfig({ contract, db, extensions, migrations }) ) is for static project shape: contract path, installed extensions, migrations directory, default connection string. .env is for per environment values ( DATABASE URL , secrets). The config reads .env automatically via dotenv/config . Hardcoding DATABASE URL in the config file leaks credentials and bypasses per env overrides.
Build system / dev server integration is a separate skill. vite dev auto emit lives in prisma next build . The runtime side (this skill) reads contract.json / contract.d.ts regardless of how they got onto disk, so the two skills compose cleanly.
Workflow — Basic db.ts
The concept: db.ts is the seam between the emitted contract artefacts (target shaped) and the runtime that executes queries against them. Three imports are load bearing — the runtime factory, the Contract type (so the static query surfaces are typed), and the JSON artefact (so the runtime validates the structure at construct time).
init scaffolds something like this (for target postgres ):
( init currently scaffolds at prisma/db.ts instead — see TML 2532 in prisma next quickstart . The canonical path is src/prisma/db.ts ; the rest of src/ imports from ./prisma/db or ../prisma/db depending on depth.)
Three things to know:
<Contract type parameter is load bearing. Without it, the static surfaces collapse to a generic shape and you lose autocomplete on model names. Always import Contract from the emitted ./contract.d.ts .
with { type: 'json' } is required. Node's ESM JSON import attribute spec. Without it, the import errors.
url is optional at construct time. If DATABASE URL is not set when db.ts loads, the factory still returns a client; you can call await db.connect({ url }) later. The factory throws lazily — only when a runtime is actually needed.
The Mongo façade has the same construction shape — import mongo from '@prisma next/mongo/runtime' — and the same db.connect(...) / db.close() lifecycle methods. The Mongo façade does not expose db.transaction(...) . See What Prisma Next doesn't do yet for the workaround. The ORM surface differs in one place: keys. On Mongo, db.orm is keyed by the collection's storage name (from @@map(...) , or the lowercased model name if no @@map is set), not by the PSL model name — so model User { … @@map("users") } is reached at db.orm.users , not db.orm.User . The SQL builder lane ( db.sql.<table ) doesn't exist on Mongo at all ( db.sql is undefined ). See prisma next queries § MongoDB ORM addressing for the full rule and a rewrite recipe for SQL target examples.
Workflow — Running as a script (teardown)
The concept: short scripts that connect, query, then expect the process to exit will hang on Postgres because the façade owned pg.Pool keeps Node's event loop alive. The data round trip succeeds; the script never exits. Call await db.close() before the script returns (or use await using at the top of a script module so teardown runs when the module exits — see the block scope warning below for why this matters).
Plain shape — export db from db.ts , import it in the script, close at the end:
TS 5.2+ idiomatic shape — construct the client at the top of a script module and let [Symbol.asyncDispose] call close() when the module exits:
await using is block scoped — do not put it inside a request handler
This is the most important rule in this section. await using db = postgres(...) disposes when the enclosing block exits. In a script module, that block is the module body and disposal fires at process exit — fine. In a request handler, the enclosing block is the handler function, so disposal fires after every request — a fresh pg.Pool per call, TCP connect storm, hot loop tearing connections up and down.
The right server pattern is a module level singleton in db.ts , imported by handlers, never closed during the process lifetime:
Servers (HTTP handlers, workers in a request loop) do not call db.close() at all in steady state. The pool stays open for the process lifetime. db.close() and await using are for short lived scripts — tsx my script.ts , Node CLI commands, CI tasks, one off seed runs — not for code that runs inside a request loop.
Semantics:
close() is idempotent. Calling it twice is a no op.
close() is terminal. There is no reconnect on a closed db — construct a new client if you need another connection. After close, db.runtime() , db.connect(...) , db.transaction(...) , and db.prepare(...) reject with Error('<target client is closed') (e.g. 'Postgres client is closed' , 'SQLite client is closed' , 'Mongo client is closed' ).
close() does not abort in flight queries. await outstanding work before calling close() . Async iterators from db.runtime().execute(plan) and PreparedStatement handles held after close() fail on their next call.
Ownership. close() releases only what the façade constructed ( pg.Pool from { url } , MongoClient from { url } / { uri, dbName } , SQLite handle from { path } ). If you supplied your own pg.Pool / pg.Client (Postgres pg: option), mongodb.MongoClient (Mongo mongoClient: option), or a pre built binding , db.close() does not touch those — you own their lifecycle.
db.end() does not exist. The universal node postgres name is pool.end() on a pg.Pool ; the Prisma Next runtime client is not a pg.Pool . The right call is await db.close() .
Workflow — Telemetry middleware
The concept: telemetry middleware sees every operation and emits a structured event for each (start, success, error). Pair the events with your observability stack's collector.
createTelemetryMiddleware is shipped as a separate user installable package ( @prisma next/middleware telemetry ), not as a /middleware subpath of the postgres façade. Install it directly. Run pnpm ls @prisma next/middleware telemetry to confirm it's on the lockfile.
Workflow — Lints and budgets middleware
The concept: lints catch authoring mistakes that survive type check (e.g. DELETE without a WHERE , SELECT without a LIMIT on a large table); budgets enforce row count and latency ceilings at runtime. Both surface findings through the structured error envelope so an agent can branch on the code.
These ship in the underlying SQL runtime package ( @prisma next/sql runtime ) and are not yet re exported from the postgres façade — see What Prisma Next doesn't do yet . The example apps under examples/prisma next demo/src/prisma/db.ts show the canonical import.
For the full option surface, read the source: packages/2 sql/5 runtime/src/middleware/lints.ts and .../budgets.ts . The severities keys ( selectStar , noLimit , deleteWithoutWhere , updateWithoutWhere , readOnlyMutation for lints; rowCount , latency for budgets) are the source of truth; do not extrapolate to a key that ripgrep can't find.
Workflow — Compose multiple middleware
Order matters: outermost wraps. Telemetry first means budget / lint failures are captured as spans (the agent can correlate the lint code with the operation in the same trace).
Workflow — Configure the connection
The concept: the runtime takes one of three binding shapes — url , pg (a pre constructed pg.Pool or pg.Client ), or binding (an explicit kind tag). They're mutually exclusive. The pg form is for projects that already manage their own pool (e.g. a Lambda layer); url is the default. Pool tuning is poolOptions.connectionTimeoutMillis / poolOptions.idleTimeoutMillis — not driverOptions .
The url and pg keys are mutually exclusive at the type level; passing both errors.
DATABASE URL lives in .env . The CLI reads it for emit / verify / migration commands; the runtime reads it through process.env at db.ts load time.
Workflow — Per environment config (dev vs prod)
The concept: one DATABASE URL per environment; the rest of the db.ts shape is the same. For middleware divergence (e.g. strict lints in dev only), branch in db.ts on process.env['NODE ENV'] .
.env for local; the deploy platform's secrets for prod. Never commit .env .
Workflow — Transactions
The concept applies to Postgres and SQLite . db.transaction(fn) opens a transaction, gives the callback a tx context with the same sql / orm surfaces as db , and commits on successful return / rolls back on any thrown error. Inside the callback, use tx.sql and tx.orm instead of db.sql / db.orm so the writes ride the transaction. The Mongo façade does not expose db.transaction(...) .
The callback returns whatever you return from it — the transaction wrapper passes it through. The tx object exposes execute(plan) for SQL builder