- name
- prisma-next-runtime
- description
- 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-telemetry`; lints and budgets), `DATABASE_URL` config, per-environment branching, switching between Postgres, SQLite, and Mongo façades. Use for db.ts, postgres(), sqlite(), mongo(), middleware, telemetry, lints, budgets, DATABASE_URL, .env, connection pool, poolOptions, dev vs prod config, transactions, db.transaction, read replicas, multi-database, script won't exit, hangs, close connection, db.end, db.close, pool.end, [Symbol.asyncDispose], await using.
# 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`):
```typescript
// src/prisma/db.ts
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
});
```
(`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:
```typescript
// src/scripts/hello.ts
import { db } from '../prisma/db';
const created = await db.orm.User.create({ email: 'alice@example.com', name: 'Alice' });
const read = await db.orm.User.first();
console.log({ created, read });
await db.close();
```
**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:
```typescript
// src/scripts/hello.ts — top-level await in a script module
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from '../prisma/contract.d';
import contractJson from '../prisma/contract.json' with { type: 'json' };
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
const user = await db.orm.User.first();
console.log(user);
// db.close() runs automatically when the script 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.
```typescript
// DO NOT do this — closes the pool after every request.
app.get('/users', async (req, res) => {
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
const users = await db.orm.User.all();
res.json(users);
});
```
The right server pattern is a **module-level singleton** in `db.ts`, imported by handlers, never closed during the process lifetime:
```typescript
// src/prisma/db.ts — constructed once, lives for the process
export const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL });
// src/routes/users.ts
import { db } from '../prisma/db';
app.get('/users', async (req, res) => {
const users = await db.orm.User.all();
res.json(users);
});
```
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.
```typescript
import postgres from '@prisma-next/postgres/runtime';
import { createTelemetryMiddleware } from '@prisma-next/middleware-telemetry';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
middleware: [
createTelemetryMiddleware({
onEvent: (event) => {
// forward to your collector, log, etc.
},
}),
],
});
```
`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.
```typescript
import postgres from '@prisma-next/postgres/runtime';
import { budgets, lints } from '@prisma-next/sql-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
middleware: [
lints({
severities: {
selectStar: 'warn',
noLimit: 'error',
deleteWithoutWhere: 'error',
updateWithoutWhere: 'error',
readOnlyMutation: 'error',
},
}),
budgets({
maxRows: 10_000,
defaultTableRows: 10_000,
tableRows: { user: 10_000, post: 50_000 },
maxLatencyMs: 1_000,
severities: { rowCount: 'error', latency: 'warn' },
}),
],
});
```
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
```typescript
middleware: [
createTelemetryMiddleware({ onEvent }), // outermost — sees all sub-failures as inner errors
lints({ severities: { noLimit: 'error' } }),
budgets({ maxLatencyMs: 5_000 }), // innermost — runs closest to the driver
],
```
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
Voir sur GitHub