@prisma/orm-mongo 8.0.0-rc.5 → 8.0.0-rc.5-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -10
- package/skills/prisma-8/SKILL.md +84 -0
- package/skills/prisma-8/references/build.md +142 -0
- package/skills/prisma-8/references/contract.md +417 -0
- package/skills/prisma-8/references/debug.md +141 -0
- package/skills/prisma-8/references/feedback.md +251 -0
- package/skills/prisma-8/references/migration-review.md +224 -0
- package/skills/prisma-8/references/migrations.md +519 -0
- package/skills/prisma-8/references/queries-mongo.md +236 -0
- package/skills/prisma-8/references/queries-postgres.md +415 -0
- package/skills/prisma-8/references/queries.md +168 -0
- package/skills/prisma-8/references/quickstart.md +326 -0
- package/skills/prisma-8/references/runtime.md +344 -0
- package/skills/prisma-8/references/supabase.md +244 -0
- package/skills/prisma-8/references/upgrade-app.md +101 -0
- package/skills/prisma-8/references/upgrade-extension.md +105 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.10-to-0.11/instructions.md +56 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/instructions.md +381 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-closed-mongo-contracts.ts +202 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-domain-namespaced-contracts.ts +201 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-postgres-public-default.ts +198 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/instructions.md +339 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/re-emit-mti-variant-link-columns.ts +229 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/instructions.md +543 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.14-to-0.15/instructions.md +359 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.15-to-0.16/instructions.md +173 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/instructions.md +805 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.17-to-8.0.0-rc.1/instructions.md +72 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/instructions.md +36 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/instructions.md +86 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +588 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +158 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +42 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.10-to-0.11/instructions.md +276 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/instructions.md +738 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/migrate-contract-testing-imports.ts +97 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/regenerate-extension-public-baseline.ts +223 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.12-to-0.13/instructions.md +266 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/instructions.md +522 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.14-to-0.15/instructions.md +803 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.15-to-0.16/instructions.md +219 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/instructions.md +731 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.17-to-8.0.0-rc.1/instructions.md +194 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/instructions.md +57 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/instructions.md +150 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +746 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +137 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +129 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
|
|
2
|
+
# Prisma Next — Runtime (`db.ts` Wiring)
|
|
3
|
+
|
|
4
|
+
> **Edit your data contract. Prisma handles the rest.**
|
|
5
|
+
|
|
6
|
+
This skill covers the **runtime entry point** — `db.ts` — and how to compose the database client with extensions, middleware, and environment configuration.
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- User is wiring up `db.ts` for the first time (post-init).
|
|
11
|
+
- User wants to add middleware (telemetry, lints, budgets, custom).
|
|
12
|
+
- User wants per-environment config (dev vs prod, multi-region).
|
|
13
|
+
- User wants to switch between the Postgres, SQLite, and Mongo façades.
|
|
14
|
+
- User wants to wrap operations in `db.transaction(...)` (Postgres and SQLite).
|
|
15
|
+
- 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`).
|
|
16
|
+
- 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*.
|
|
17
|
+
|
|
18
|
+
## When Not to Use
|
|
19
|
+
|
|
20
|
+
- User wants to write queries → `references/queries.md`.
|
|
21
|
+
- User is on Supabase — the `supabase()` role-first factory, `asUser(jwt)` / `asAnon()` / `asServiceRole()`, JWT config, RLS → `references/supabase.md`.
|
|
22
|
+
- User wants to edit the contract → `references/contract.md`.
|
|
23
|
+
- User wants to wire Prisma Next into a build tool (Vite plugin, Next.js, …) → `references/build.md`.
|
|
24
|
+
- User wants to debug a connection / runtime error → `references/debug.md`.
|
|
25
|
+
- User wants to file a bug or feature request → `references/feedback.md`.
|
|
26
|
+
|
|
27
|
+
## Key Concepts
|
|
28
|
+
|
|
29
|
+
- **`db.ts` is the runtime entry point.** Imports the runtime factory from the `@internal/<target>` façade (`@internal/postgres/runtime`, `@internal/sqlite/runtime`, or `@internal/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.
|
|
30
|
+
- **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 '@internal/postgres/runtime'`; SQLite: `import sqlite from '@internal/sqlite/runtime'`; Mongo: `import mongo from '@internal/mongo/runtime'`. The factory signature is `<Target><Contract>(options)` — a single type parameter (the `Contract` type from `contract.d.ts`), and one options object.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **`prisma.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.
|
|
34
|
+
- **Build-system / dev-server integration is a separate skill.** `vite dev` auto-emit lives in `references/build.md`. The runtime side (this skill) reads `contract.json` / `contract.d.ts` regardless of how they got onto disk, so the two skills compose cleanly.
|
|
35
|
+
|
|
36
|
+
## Workflow — Basic `db.ts`
|
|
37
|
+
|
|
38
|
+
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).
|
|
39
|
+
|
|
40
|
+
`init` scaffolds something like this (for `--target postgres`):
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/prisma/db.ts
|
|
44
|
+
import postgres from '@internal/postgres/runtime';
|
|
45
|
+
import type { Contract } from './contract.d';
|
|
46
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
47
|
+
|
|
48
|
+
export const db = postgres<Contract>({
|
|
49
|
+
contractJson,
|
|
50
|
+
url: process.env['DATABASE_URL'],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
(`init` currently scaffolds at `prisma/db.ts` instead — see TML-2532 in `references/quickstart.md`. The canonical path is `src/prisma/db.ts`; the rest of `src/` imports from `./prisma/db` or `../prisma/db` depending on depth.)
|
|
55
|
+
|
|
56
|
+
Three things to know:
|
|
57
|
+
|
|
58
|
+
- **`<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`.
|
|
59
|
+
- **`with { type: 'json' }` is required.** Node's ESM JSON-import-attribute spec. Without it, the import errors.
|
|
60
|
+
- **`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.
|
|
61
|
+
|
|
62
|
+
The Mongo façade has the same construction shape — `import mongo from '@internal/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 `references/queries.md` § *MongoDB ORM addressing* for the full rule and a rewrite recipe for SQL-target examples.
|
|
63
|
+
|
|
64
|
+
## Workflow — Running as a script (teardown)
|
|
65
|
+
|
|
66
|
+
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).
|
|
67
|
+
|
|
68
|
+
**Plain shape** — export `db` from `db.ts`, import it in the script, close at the end:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/scripts/hello.ts
|
|
72
|
+
import { db } from '../prisma/db';
|
|
73
|
+
|
|
74
|
+
const created = await db.orm.User.create({ email: 'alice@example.com', name: 'Alice' });
|
|
75
|
+
const read = await db.orm.User.first();
|
|
76
|
+
console.log({ created, read });
|
|
77
|
+
|
|
78
|
+
await db.close();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**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:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// src/scripts/hello.ts — top-level await in a script module
|
|
85
|
+
import postgres from '@internal/postgres/runtime';
|
|
86
|
+
import type { Contract } from '../prisma/contract.d';
|
|
87
|
+
import contractJson from '../prisma/contract.json' with { type: 'json' };
|
|
88
|
+
|
|
89
|
+
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
|
|
90
|
+
|
|
91
|
+
const user = await db.orm.User.first();
|
|
92
|
+
console.log(user);
|
|
93
|
+
// db.close() runs automatically when the script module exits.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `await using` is **block-scoped** — do not put it inside a request handler
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// DO NOT do this — closes the pool after every request.
|
|
102
|
+
app.get('/users', async (req, res) => {
|
|
103
|
+
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
|
|
104
|
+
const users = await db.orm.User.all();
|
|
105
|
+
res.json(users);
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The right server pattern is a **module-level singleton** in `db.ts`, imported by handlers, never closed during the process lifetime:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// src/prisma/db.ts — constructed once, lives for the process
|
|
113
|
+
export const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL });
|
|
114
|
+
|
|
115
|
+
// src/routes/users.ts
|
|
116
|
+
import { db } from '../prisma/db';
|
|
117
|
+
|
|
118
|
+
app.get('/users', async (req, res) => {
|
|
119
|
+
const users = await db.orm.User.all();
|
|
120
|
+
res.json(users);
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
**Semantics:**
|
|
127
|
+
|
|
128
|
+
- **`close()` is idempotent.** Calling it twice is a no-op.
|
|
129
|
+
- **`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'`).
|
|
130
|
+
- **`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.
|
|
131
|
+
- **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.
|
|
132
|
+
|
|
133
|
+
**`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()`.
|
|
134
|
+
|
|
135
|
+
## Workflow — Telemetry middleware
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import postgres from '@internal/postgres/runtime';
|
|
141
|
+
import { createTelemetryMiddleware } from '@internal/middleware-telemetry';
|
|
142
|
+
import type { Contract } from './contract.d';
|
|
143
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
144
|
+
|
|
145
|
+
export const db = postgres<Contract>({
|
|
146
|
+
contractJson,
|
|
147
|
+
url: process.env['DATABASE_URL'],
|
|
148
|
+
middleware: [
|
|
149
|
+
createTelemetryMiddleware({
|
|
150
|
+
onEvent: (event) => {
|
|
151
|
+
// forward to your collector, log, etc.
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`createTelemetryMiddleware` is shipped as a separate user-installable package (`@internal/middleware-telemetry`), not as a `/middleware` subpath of the postgres façade. Install it directly. Run `pnpm ls @internal/middleware-telemetry` to confirm it's on the lockfile.
|
|
159
|
+
|
|
160
|
+
## Workflow — Lints and budgets middleware
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
These ship in the underlying SQL runtime package (`@internal/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-8-demo/src/prisma/db.ts` show the canonical import.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import postgres from '@internal/postgres/runtime';
|
|
168
|
+
import { budgets, lints } from '@internal/sql-runtime';
|
|
169
|
+
import type { Contract } from './contract.d';
|
|
170
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
171
|
+
|
|
172
|
+
export const db = postgres<Contract>({
|
|
173
|
+
contractJson,
|
|
174
|
+
url: process.env['DATABASE_URL'],
|
|
175
|
+
middleware: [
|
|
176
|
+
lints({
|
|
177
|
+
severities: {
|
|
178
|
+
selectStar: 'warn',
|
|
179
|
+
noLimit: 'error',
|
|
180
|
+
deleteWithoutWhere: 'error',
|
|
181
|
+
updateWithoutWhere: 'error',
|
|
182
|
+
readOnlyMutation: 'error',
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
budgets({
|
|
186
|
+
maxRows: 10_000,
|
|
187
|
+
defaultTableRows: 10_000,
|
|
188
|
+
tableRows: { user: 10_000, post: 50_000 },
|
|
189
|
+
maxLatencyMs: 1_000,
|
|
190
|
+
severities: { rowCount: 'error', latency: 'warn' },
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
## Workflow — Compose multiple middleware
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
middleware: [
|
|
202
|
+
createTelemetryMiddleware({ onEvent }), // outermost — sees all sub-failures as inner errors
|
|
203
|
+
lints({ severities: { noLimit: 'error' } }),
|
|
204
|
+
budgets({ maxLatencyMs: 5_000 }), // innermost — runs closest to the driver
|
|
205
|
+
],
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
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).
|
|
209
|
+
|
|
210
|
+
## Workflow — Configure the connection
|
|
211
|
+
|
|
212
|
+
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`.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Default — URL string, factory constructs the pool.
|
|
216
|
+
postgres<Contract>({
|
|
217
|
+
contractJson,
|
|
218
|
+
url: process.env['DATABASE_URL'],
|
|
219
|
+
poolOptions: {
|
|
220
|
+
connectionTimeoutMillis: 20_000,
|
|
221
|
+
idleTimeoutMillis: 30_000,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// BYO pool — pass a pg.Pool you already created.
|
|
226
|
+
import { Pool } from 'pg';
|
|
227
|
+
const pool = new Pool({ connectionString: process.env['DATABASE_URL'] });
|
|
228
|
+
postgres<Contract>({ contractJson, pg: pool });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The `url` and `pg` keys are mutually exclusive at the type level; passing both errors.
|
|
232
|
+
|
|
233
|
+
`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.
|
|
234
|
+
|
|
235
|
+
## Workflow — Per-environment config (dev vs prod)
|
|
236
|
+
|
|
237
|
+
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']`.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const isProd = process.env['NODE_ENV'] === 'production';
|
|
241
|
+
|
|
242
|
+
export const db = postgres<Contract>({
|
|
243
|
+
contractJson,
|
|
244
|
+
url: process.env['DATABASE_URL'],
|
|
245
|
+
middleware: isProd
|
|
246
|
+
? [createTelemetryMiddleware({ onEvent })]
|
|
247
|
+
: [
|
|
248
|
+
createTelemetryMiddleware({ onEvent }),
|
|
249
|
+
lints({ severities: { noLimit: 'error', deleteWithoutWhere: 'error' } }),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`.env` for local; the deploy platform's secrets for prod. Never commit `.env`.
|
|
255
|
+
|
|
256
|
+
## Workflow — Transactions
|
|
257
|
+
|
|
258
|
+
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(...)`.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
await db.transaction(async (tx) => {
|
|
262
|
+
const user = await tx.orm.User.create({ email: 'alice@example.com' });
|
|
263
|
+
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
|
|
264
|
+
// If either call throws, both inserts roll back.
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The callback returns whatever you return from it — the transaction wrapper passes it through. The `tx` object exposes `execute(plan)` for SQL-builder plans inside the transaction.
|
|
269
|
+
|
|
270
|
+
## Workflow — Switch between Postgres, SQLite, and Mongo
|
|
271
|
+
|
|
272
|
+
The concept: the façade selection is baked into `db.ts` (`@internal/postgres` or `@internal/mongo`) and `prisma.config.ts` (which `defineConfig` you import from). To switch a project's target, re-run `prisma orm init` in the same directory and pick the other target — the init flow detects the existing scaffold and prompts to reinit (non-interactive runs grant the consent with `--confirm <directory name>`). PN re-scaffolds `prisma.config.ts` and `db.ts` for the new façade. The contract source needs to be re-authored for the new target's idioms (Mongo expresses nested documents; Postgres expresses relations).
|
|
273
|
+
|
|
274
|
+
After the switch (Mongo):
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// src/prisma/db.ts (Mongo)
|
|
278
|
+
import mongo from '@internal/mongo/runtime';
|
|
279
|
+
import type { Contract } from './contract.d';
|
|
280
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
281
|
+
|
|
282
|
+
export const db = mongo<Contract>({ contractJson, url: process.env['DATABASE_URL'] });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
SQLite:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// src/prisma/db.ts (SQLite)
|
|
289
|
+
import sqlite from '@internal/sqlite/runtime';
|
|
290
|
+
import type { Contract } from './contract.d';
|
|
291
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
292
|
+
|
|
293
|
+
export const db = sqlite<Contract>({ contractJson, path: 'app.db' });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`path` is optional at construct time (you can call `db.connect({ path })` later); omit it and the façade still returns a client. The SQLite façade exposes the same `db.sql`, `db.orm`, `db.transaction(...)`, `db.close()`, and `[Symbol.asyncDispose]` surfaces as Postgres. The Mongo façade shares `db.orm`, `db.close()`, and `[Symbol.asyncDispose]` but has no `db.sql` and no `db.transaction(...)`.
|
|
297
|
+
|
|
298
|
+
The `db.sql` / `db.orm` surfaces stay the same in name; the operators each surface exposes are target-shaped (Mongo has no `JOIN`).
|
|
299
|
+
|
|
300
|
+
## Workflow — Build-system / dev-server integration
|
|
301
|
+
|
|
302
|
+
If you want contract artefacts to re-emit automatically while the dev server is running (instead of running `prisma contract emit` by hand each time the contract source changes), reach for the build-tool plugin from `references/build.md`:
|
|
303
|
+
|
|
304
|
+
- **Vite**: install `@internal/vite-plugin-contract-emit` and register `prismaVitePlugin('prisma.config.ts')` in `vite.config.ts`.
|
|
305
|
+
- **Next.js, Webpack, esbuild, Rollup, Turbopack**: no first-party plugin yet — the workaround is a `prebuild` script that runs `prisma contract emit`. See `references/build.md` for the walkthrough.
|
|
306
|
+
|
|
307
|
+
The runtime side (this skill) is the same regardless: `db.ts` reads `contract.json` + `contract.d.ts` from disk. The build-system plugin's job is to keep those files current during development.
|
|
308
|
+
|
|
309
|
+
## Common Pitfalls
|
|
310
|
+
|
|
311
|
+
1. **Hardcoding `DATABASE_URL` in `prisma.config.ts`.** Leaks credentials; bypasses per-environment overrides. Use `.env`.
|
|
312
|
+
2. **Omitting the `<Contract>` type parameter** in `postgres<Contract>(...)`. Without it, static surfaces collapse to a generic shape and you lose autocomplete for models. There is no second type parameter — the older two-param signature (`postgres<Contract, TypeMaps>`) is gone.
|
|
313
|
+
3. **Forgetting `with { type: 'json' }` on the contract import.** Required by Node's ESM JSON-import-attribute spec.
|
|
314
|
+
4. **Middleware order matters.** Outermost wraps. Put telemetry first if you want it to capture inner-middleware errors.
|
|
315
|
+
5. **Importing middleware from a non-existent façade subpath.** `@internal/postgres/middleware` does *not* exist. Telemetry comes from `@internal/middleware-telemetry`; lints / budgets come from `@internal/sql-runtime` today (see *What Prisma Next doesn't do yet*).
|
|
316
|
+
6. **Confabulating lint / budget option names.** Lints take `severities` (with the five keys above), not `requireWhere` / `maxRowsWithoutLimit`. Budgets use `maxLatencyMs` (not `maxDurationMs`) plus `maxRows` / `defaultTableRows` / `tableRows`. When in doubt, read the source.
|
|
317
|
+
7. **Switching targets without re-emitting.** The contract artefacts are target-shaped; emit after the target change.
|
|
318
|
+
8. **Script hangs after queries finish on Postgres.** The `pg.Pool` keeps Node's event loop alive. Solution: `await db.close()` before the script returns, or `await using db = postgres<Contract>(...)` at the top of a script module. Do not put `await using db = postgres(...)` inside a request handler — it's block-scoped and would close the pool after every request. The right server pattern is a module-level singleton in `db.ts` that lives for the process lifetime.
|
|
319
|
+
|
|
320
|
+
## What Prisma Next doesn't do yet
|
|
321
|
+
|
|
322
|
+
- **`@internal/postgres/middleware` subpath.** The postgres façade re-exports the runtime factory (`./runtime`), config (`./config`), contract-builder (`./contract-builder`), control (`./control`), family (`./family`), target (`./target`), and serverless (`./serverless`) — but not middleware. Today's workaround: import `lints` and `budgets` from `@internal/sql-runtime`, and `createTelemetryMiddleware` from `@internal/middleware-telemetry`. File additional gaps you hit via `references/feedback.md`.
|
|
323
|
+
- **Multi-database routing / read replicas.** Prisma Next doesn't ship a built-in primary/replica router or shard-aware client. Workaround: configure separate `db.ts` instances per data store and call the right one in your application code. If you need first-class multi-database routing, file a feature request via the `references/feedback.md` skill.
|
|
324
|
+
- **Connection pooling as a first-class config field.** `poolOptions.connectionTimeoutMillis` and `poolOptions.idleTimeoutMillis` are wired through, but the rest of `pg.Pool`'s tuning surface (max connections, `allowExitOnIdle`, ssl options, …) is not exposed by name. Workaround: construct the `pg.Pool` yourself and pass it via `pg:`. If you need more pool fields surfaced on the façade, file a feature request via the `references/feedback.md` skill.
|
|
325
|
+
- **Query logger middleware as a built-in.** Prisma Next doesn't ship a "log every query" middleware. Workaround: write a small custom middleware that wraps each operation and logs; or use `createTelemetryMiddleware` and log inside the `onEvent` callback. If you need a built-in query log, file a feature request via the `references/feedback.md` skill.
|
|
326
|
+
|
|
327
|
+
## Reference Files
|
|
328
|
+
|
|
329
|
+
This skill is intentionally body-only; `prisma orm init --help`, the `defineConfig` factory in `packages/3-extensions/postgres/src/config/define-config.ts`, the `postgres()` factory in `packages/3-extensions/postgres/src/runtime/postgres.ts`, and the middleware sources in `packages/2-sql/5-runtime/src/middleware/{lints,budgets}.ts` are the authoritative surfaces for option-level detail. When in doubt, read the source.
|
|
330
|
+
|
|
331
|
+
## Checklist
|
|
332
|
+
|
|
333
|
+
- [ ] `db.ts` imports the runtime factory from `@internal/<target>/runtime` (`postgres`, `sqlite`, or `mongo`) and the `Contract` type from `./contract.d`.
|
|
334
|
+
- [ ] `with { type: 'json' }` on the contract JSON import.
|
|
335
|
+
- [ ] `<Contract>` is the single type parameter on `postgres<Contract>(...)` (no second parameter).
|
|
336
|
+
- [ ] `DATABASE_URL` lives in `.env`, not in `prisma.config.ts`.
|
|
337
|
+
- [ ] Middleware ordered intentionally (telemetry outermost typically).
|
|
338
|
+
- [ ] `lints` / `budgets` use the verified option keys (`severities`, `maxLatencyMs`, `maxRows`, `tableRows`).
|
|
339
|
+
- [ ] Per-env divergence (if any) gated by `NODE_ENV` or similar.
|
|
340
|
+
- [ ] Did NOT hardcode credentials in any committed file.
|
|
341
|
+
- [ ] Did NOT confabulate a `@internal/postgres/middleware` subpath, a `@internal/postgres-extension-audit` package, or a second type parameter on `postgres<...>`.
|
|
342
|
+
- [ ] Did NOT claim `db.transaction(...)` exists on the Mongo façade — only Postgres and SQLite expose it.
|
|
343
|
+
- [ ] Did NOT confabulate read-replica / multi-DB / extra pool config — pointed at *What Prisma Next doesn't do yet* and routed to `references/feedback.md`.
|
|
344
|
+
- [ ] For build-system / dev-server prompts (Vite plugin, Next.js plugin, …) routed to `references/build.md`.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
|
|
2
|
+
# Prisma Next — Supabase
|
|
3
|
+
|
|
4
|
+
> **Edit your data contract. Prisma handles the rest.**
|
|
5
|
+
|
|
6
|
+
This skill covers using Prisma Next against a **Supabase** project end-to-end: composing the Supabase extension pack, referencing Supabase-owned tables from your contract, authoring row-level-security (RLS) policies, and running role-bound queries through the `supabase()` runtime.
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- User has a Supabase project (or wants one) and is wiring Prisma Next into it.
|
|
11
|
+
- User wants RLS policies on their tables (`policy_select`, `@@rls`, `auth.uid()`).
|
|
12
|
+
- User wants per-request role binding (`asUser(jwt)`, `asAnon()`, `asServiceRole()`).
|
|
13
|
+
- User wants a foreign key into `auth.users` (cross-space FK).
|
|
14
|
+
- User wants to read Supabase-internal tables (`auth.*`, `storage.*`) as an admin.
|
|
15
|
+
- User mentions: *supabase, RLS, row level security, policy, anon, authenticated, service_role, auth.users, auth.uid(), JWT, jwtSecret, jwksUrl, SUPABASE.JWT_INVALID, RoleBoundDb, session pooler*.
|
|
16
|
+
|
|
17
|
+
## When Not to Use
|
|
18
|
+
|
|
19
|
+
- General contract editing (models, fields, relations) → `references/contract.md`.
|
|
20
|
+
- Non-Supabase `db.ts` wiring, middleware, teardown → `references/runtime.md`.
|
|
21
|
+
- General query shapes (filtering, includes, aggregates) → `references/queries.md` — everything there applies to a role-bound `db` too.
|
|
22
|
+
- Migration planning / applying → `references/migrations.md`.
|
|
23
|
+
|
|
24
|
+
## Key Concepts
|
|
25
|
+
|
|
26
|
+
- **The pack is an `external` contract space.** `@internal/extension-supabase/pack` ships a complete, introspection-generated contract of everything Supabase owns — the `auth` and `storage` schemas, their native enum types, and the platform roles (`anon`, `authenticated`, `service_role`) — all with control policy `external`. Composed via `extensions`, it means: the migration planner **emits no DDL** for those objects (Supabase manages them), and `db verify` **confirms they exist** in the live database. Your own tables stay `managed` as usual.
|
|
27
|
+
- **Roles come from the pack; you never declare them.** RLS `roles = [authenticated]` identifiers resolve against the composed contract. Pointing the runtime at a non-Supabase Postgres fails verify with a `not-found` issue naming the missing role — the common "wrong database" misconfiguration surfaces before queries run.
|
|
28
|
+
- **The runtime is role-first.** `supabase()` returns a `SupabaseDb` with **no top-level query surface** — there is no `db.sql` / `db.orm` until you bind a role. `await db.asUser(jwt)` / `db.asAnon()` / `db.asServiceRole()` each return a `RoleBoundDb` exposing `.sql`, `.orm`, `.raw`, `.execute(plan)`, and `.transaction(fn)`. This is deliberate: in a Supabase app there is no meaningful "no role" execution context, and defaulting to the connection's login role is a silent-RLS-bypass footgun.
|
|
29
|
+
- **Role binding is below middleware and cannot leak.** Each role-bound query runs on a connection that had `set_config('role', …)` and `set_config('request.jwt.claims', …)` applied beneath the user-middleware chain, with `RESET ALL` on release. Postgres-side `auth.uid()` / `auth.jwt()` read those session vars — RLS enforcement is Postgres's job; the runtime's job is binding the context.
|
|
30
|
+
- **RLS is enforced by policies *and* grants.** Policies filter *rows*; `GRANT` controls *table access*. Prisma Next authors and migrates the policies; it does not author grants (see *What Prisma Next doesn't do yet*). A role with policies but no `GRANT` gets a permission error, not filtered rows. On Supabase your `public` tables already carry the platform-role grants via default privileges — the grant that is actually missing out of the box is `service_role`'s on `auth.*` / `storage.*` (see *Workflow — Grants*).
|
|
31
|
+
- **JWT validation is eager and configurable — current Supabase projects need `jwksUrl`.** `asUser(jwt)` verifies the token (via `jose`) *before* any connection is acquired: signature + expiry against `jwksUrl` (asymmetric signing keys — **the default on current Supabase projects**, which sign ES256) **xor** `jwtSecret` (the symmetric HS256 secret — legacy projects only). Both or neither → a structured error with code `SUPABASE.CONFIG_INVALID`. Bad tokens throw a structured error with code `SUPABASE.JWT_INVALID` and a typed `meta.reason` — including a mismatch between the token's algorithm and the configured key source (an ES256 token against a `jwtSecret` client names the problem and tells you to switch to `jwksUrl`). The Postgres role is derived from the token's `role` claim (defaults to `authenticated`). Note: `supabase status` still prints a `JWT_SECRET` even on projects that sign ES256 — its presence does not mean your project uses it.
|
|
32
|
+
- **Admin access to `auth.*` / `storage.*` is a secondary root on `service_role` only — and needs a one-time grant.** `db.asServiceRole().supabase` exposes the pack's own contract (`.sql`, `.orm`, `.nativeEnums`, `.execute`). The root exists only on `service_role` by design, but a real Supabase project grants `service_role` **no table privileges** on `auth.*` / `storage.*` (only schema `USAGE`; only `postgres` holds table grants). Before the admin root can read a Supabase-internal table, run the narrow grant once (see *Workflow — Grants*). `asUser` / `asAnon` have no `.supabase`, and the primary `asServiceRole().sql` / `.orm` stay scoped to *your* contract.
|
|
33
|
+
|
|
34
|
+
## Workflow — Wire the pack into the config
|
|
35
|
+
|
|
36
|
+
The concept: the pack registers the Supabase contract space so your contract can reference it and the planner/verifier know what Supabase owns. The extension has no `/control` subpath yet, so it can't go through the target façade's `defineConfig({ extensions: [...] })` — it wires into the low-level config's `extensions` (see *What Prisma Next doesn't do yet*). The low-level imports below are a **deliberate exception** to the façade-only import rule, forced by that gap; the block mirrors `examples/supabase/prisma.config.ts` verbatim — copy it rather than composing your own:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// prisma.config.ts
|
|
40
|
+
import postgresAdapter from '@internal/adapter-postgres/control';
|
|
41
|
+
import { defineConfig } from '@internal/cli/config-types';
|
|
42
|
+
import postgresDriver from '@internal/driver-postgres/control';
|
|
43
|
+
import supabasePack from '@internal/extension-supabase/pack';
|
|
44
|
+
import sql from '@internal/family-sql/control';
|
|
45
|
+
import { prismaContract } from '@internal/sql-contract-psl/provider';
|
|
46
|
+
import postgres from '@internal/target-postgres/control';
|
|
47
|
+
import postgresPackRef from '@internal/target-postgres/pack';
|
|
48
|
+
import { postgresCreateNamespace } from '@internal/target-postgres/types';
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
family: sql,
|
|
52
|
+
target: postgres,
|
|
53
|
+
adapter: postgresAdapter,
|
|
54
|
+
driver: postgresDriver,
|
|
55
|
+
extensions: [supabasePack],
|
|
56
|
+
contract: prismaContract('./src/contract.prisma', {
|
|
57
|
+
output: 'src/contract.json',
|
|
58
|
+
target: postgresPackRef,
|
|
59
|
+
createNamespace: postgresCreateNamespace,
|
|
60
|
+
}),
|
|
61
|
+
migrations: { dir: 'migrations' },
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Workflow — Contract: FK into `auth.users` + RLS policies
|
|
66
|
+
|
|
67
|
+
The concept: your models live in your namespaces (`public`); Supabase's live in the pack's (`auth`, `storage`). A relation field typed `supabase:auth.AuthUser` is a **cross-space FK** — the planner emits `REFERENCES "auth"."users"("id")`, and the target table is verified, never migrated. RLS policies are top-level `policy_<operation>` blocks in the same namespace as their target model, and the target model must opt in with `@@rls`. Mirror `examples/supabase/src/contract.prisma`:
|
|
68
|
+
|
|
69
|
+
```prisma
|
|
70
|
+
namespace public {
|
|
71
|
+
model Profile {
|
|
72
|
+
id Uuid @id @default(uuid())
|
|
73
|
+
username String
|
|
74
|
+
userId Uuid @unique
|
|
75
|
+
user supabase:auth.AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
76
|
+
@@map("profile")
|
|
77
|
+
@@rls
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// authenticated may read only their own profile.
|
|
81
|
+
policy_select profile_owner_read {
|
|
82
|
+
target = Profile
|
|
83
|
+
roles = [authenticated]
|
|
84
|
+
using = "\"userId\"::uuid = auth.uid()"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// anon may read every profile (a public directory listing).
|
|
88
|
+
policy_select profile_public_read {
|
|
89
|
+
target = Profile
|
|
90
|
+
roles = [anon]
|
|
91
|
+
using = "true"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// authenticated may update only their own profile, and may not
|
|
95
|
+
// reassign it to another owner (WITH CHECK).
|
|
96
|
+
policy_update profile_owner_write {
|
|
97
|
+
target = Profile
|
|
98
|
+
roles = [authenticated]
|
|
99
|
+
using = "\"userId\"::uuid = auth.uid()"
|
|
100
|
+
withCheck = "\"userId\"::uuid = auth.uid()"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `Uuid` constructor selects native UUID storage in type position. The legacy `@db.Uuid` spelling is removed; rewrite any `String @db.Uuid` alias or field as `Uuid` before emitting.
|
|
106
|
+
|
|
107
|
+
The pieces:
|
|
108
|
+
|
|
109
|
+
- **Per-operation policy blocks**: `policy_select`, `policy_insert`, `policy_update`, `policy_delete`, `policy_all`. Body is `key = value`: `target` (a model in this namespace), `roles` (resolve against the composed contract — the pack supplies `anon` / `authenticated` / `service_role`), `using`, and (for write operations) `withCheck`. Multiple permissive policies per `(target, operation)` are valid — Postgres ORs them. A block may also carry `@@map("physical name")` to adopt an existing live policy under its exact name (no wire-name hash; drift detection then byte-compares the body against Postgres's reprint, so keep the text as captured — hand-authoring it warns).
|
|
110
|
+
- **`@@rls` is required on policy targets.** A `policy_*` block whose target model lacks `@@rls` fails emit with `PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE`. A model with `@@rls` and *no* policies is also meaningful: RLS enabled, deny-all.
|
|
111
|
+
- **Predicates are verbatim SQL strings.** Quote camelCase column names inside them (`\"userId\"`), and cast where needed — `auth.uid()` returns `uuid`. Renames in your contract do not rewrite predicate bodies.
|
|
112
|
+
- **TS-builder parity exists.** `@internal/postgres/contract-builder` exports `policySelect` / `policyInsert` / `policyUpdate` / `policyDelete` / `policyAll`, `rlsEnabled(Model)`, and `role('anon')` — mirroring the PSL lowering key-for-key (identical emitted wire names). PSL is the canonical path shown here.
|
|
113
|
+
|
|
114
|
+
Emit + migrate as usual (`prisma contract emit`, then `references/migrations.md`). The plan creates your table, its FK, `ENABLE ROW LEVEL SECURITY`, and the `CREATE POLICY` statements — and **no DDL for `auth.*`**.
|
|
115
|
+
|
|
116
|
+
## Workflow — `db.ts` with the `supabase()` factory
|
|
117
|
+
|
|
118
|
+
The concept: instead of the stock `postgres()` factory, a Supabase app builds its client with `supabase()` from the extension's `/runtime` subpath. The factory is **async** (it prepares JWT key material — including the one-time JWKS fetch when `jwksUrl` is set), and the result is role-first.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// src/prisma/db.ts
|
|
122
|
+
import { supabase } from '@internal/extension-supabase/runtime';
|
|
123
|
+
import type { Contract } from './contract.d';
|
|
124
|
+
import contractJson from './contract.json' with { type: 'json' };
|
|
125
|
+
|
|
126
|
+
export const db = await supabase<Contract>({
|
|
127
|
+
contractJson,
|
|
128
|
+
url: process.env['DATABASE_URL'], // direct Postgres connection — see pitfalls
|
|
129
|
+
jwksUrl: process.env['SUPABASE_JWKS_URL'], // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
|
|
130
|
+
// Legacy HS256 projects use jwtSecret: process.env['SUPABASE_JWT_SECRET'] instead — exactly one of the two.
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Options beyond the basics: `middleware` (same composition as `postgres()` — see `references/runtime.md`; middleware never sees the role-binding `set_config` calls), `poolOptions`, `pg` (BYO `pg.Pool` / `pg.Client` instead of `url`). Teardown is `await db.close()` / `await using` exactly as in `references/runtime.md` — the same script-hang rules apply.
|
|
135
|
+
|
|
136
|
+
## Workflow — Role-bound queries
|
|
137
|
+
|
|
138
|
+
The concept: bind the role that should execute the request, then query through the returned `RoleBoundDb` — every query surface from `references/queries.md` works, RLS-filtered by Postgres.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// A signed-in user: rows are RLS-scoped to the JWT's auth.uid().
|
|
142
|
+
const userDb = await db.asUser(jwt); // async — rejects with code SUPABASE.JWT_INVALID on a bad/expired token
|
|
143
|
+
const mine = await userDb.orm.public.Profile.select('id', 'username').all();
|
|
144
|
+
|
|
145
|
+
// The anon role: sees what anon policies permit.
|
|
146
|
+
const listing = await db.asAnon().orm.public.Profile.select('id', 'username').all();
|
|
147
|
+
|
|
148
|
+
// service_role: BYPASSRLS — sees everything in YOUR contract.
|
|
149
|
+
const all = await db.asServiceRole().orm.public.Profile.select('id', 'username').all();
|
|
150
|
+
|
|
151
|
+
// Writes ride the same surfaces; RLS filters them too. An UPDATE against
|
|
152
|
+
// another owner's row affects 0 rows; a withCheck violation raises an error.
|
|
153
|
+
const updated = await userDb.orm.public.Profile
|
|
154
|
+
.where({ userId: me })
|
|
155
|
+
.updateAndCount({ username: 'new-name' });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Notes: `asAnon()` / `asServiceRole()` are sync; only `asUser` is async. Multi-namespace contracts address models by coordinate (`orm.public.Profile`, `sql.public.profile`) — see `references/queries.md` § *Namespace-aware accessors*. `RoleBoundDb.transaction(fn)` wraps work in a transaction on the role-bound session.
|
|
159
|
+
|
|
160
|
+
## Workflow — Admin reads of `auth.*` / `storage.*`
|
|
161
|
+
|
|
162
|
+
The concept: Supabase-internal tables are not part of your contract, so they are not on your query surfaces. The `service_role` binding carries a **secondary root** — `db.asServiceRole().supabase` — which is the *pack's* contract surface:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const admin = db.asServiceRole();
|
|
166
|
+
|
|
167
|
+
// SQL builder over the pack contract:
|
|
168
|
+
const users = await admin.supabase
|
|
169
|
+
.execute(admin.supabase.sql.auth.users.select('id', 'email').build())
|
|
170
|
+
.toArray();
|
|
171
|
+
|
|
172
|
+
// ORM over the pack contract:
|
|
173
|
+
const sessions = await admin.supabase.orm.auth.AuthSession.select('id', 'aal').all();
|
|
174
|
+
|
|
175
|
+
// Native enum values (e.g. auth.aal_level) come typed:
|
|
176
|
+
type AalLevel = (typeof admin.supabase.nativeEnums.auth.AalLevel)['Value'];
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**The admin root needs a one-time grant.** A real Supabase project gives `service_role` no table privileges on `auth.*` / `storage.*` — out of the box, the reads above fail with `permission denied for table users` (sqlState `42501`). Grant exactly what you read, narrowly:
|
|
180
|
+
|
|
181
|
+
```sql
|
|
182
|
+
GRANT USAGE ON SCHEMA auth TO service_role;
|
|
183
|
+
GRANT SELECT ON TABLE auth.users TO service_role;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Other boundaries to respect: `asUser` / `asAnon` have **no** `.supabase`; the admin root has **no** `.transaction` (it is a separate contract-bound runtime sharing the pool — a transaction spanning both roots is out of scope); and for user *management* (creating users, password resets) prefer the GoTrue Admin API — Supabase-internal schemas can drift across platform upgrades; direct `service_role` SQL is for ad-hoc admin reads.
|
|
187
|
+
|
|
188
|
+
## Workflow — Grants
|
|
189
|
+
|
|
190
|
+
The concept: RLS policies are row filters on top of ordinary table privileges — a role with policies but no `GRANT` gets `permission denied`, not filtered rows. On Supabase the two directions are easy to get backwards:
|
|
191
|
+
|
|
192
|
+
- **Your own `public` tables need nothing.** Supabase ships `ALTER DEFAULT PRIVILEGES` on `public`, so tables created by `prisma db init` / `prisma db migrate` inherit full grants for `anon` / `authenticated` / `service_role` automatically — the same as dashboard-created tables. RLS policies are what actually protect the rows; do not add per-table grants, and do not narrow the defaults unless you have a reason.
|
|
193
|
+
- **The one grant you do need is for admin reads of Supabase-internal tables** — `service_role` has no table privileges on `auth.*` / `storage.*` (see *Admin reads* above for the narrow `GRANT USAGE` / `GRANT SELECT` pair).
|
|
194
|
+
|
|
195
|
+
Run grants via the Supabase SQL editor or `psql`. Symptom of a missing grant: `permission denied for table …` (sqlState `42501`) instead of an empty result.
|
|
196
|
+
|
|
197
|
+
## Workflow — Connecting to a real Supabase project
|
|
198
|
+
|
|
199
|
+
The concept: the runtime needs a **direct, session-capable** Postgres connection — it binds roles with session-scoped `set_config` + `RESET ALL`.
|
|
200
|
+
|
|
201
|
+
- **Session pooler** (`aws-0-<region>.pooler.supabase.com:5432`, username `postgres.<project-ref>`) — works everywhere, IPv4. The default choice.
|
|
202
|
+
- **Direct connection** (`db.<project-ref>.supabase.co:5432`) — works, but is **IPv6-only** on new projects; from IPv4-only environments it fails DNS/connect.
|
|
203
|
+
- **Transaction pooler (port 6543) — do not use.** Transaction pooling breaks session GUCs; role binding will misbehave.
|
|
204
|
+
|
|
205
|
+
`.env` carries `DATABASE_URL` and the JWT key source. For current projects that is `SUPABASE_JWKS_URL` — `https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json` (local stack: `http://127.0.0.1:54321/auth/v1/.well-known/jwks.json`). Only legacy HS256 projects use `SUPABASE_JWT_SECRET` (Project Settings → API → JWT Secret) — and note `supabase status` prints a `JWT_SECRET` even on ES256 projects, so don't infer the mode from its presence; check the JWKS endpoint or a token's header `alg`.
|
|
206
|
+
|
|
207
|
+
## Common Pitfalls
|
|
208
|
+
|
|
209
|
+
1. **Using the transaction pooler (port 6543).** Session GUC role binding requires a session-capable connection — use the session pooler (5432) or the direct connection.
|
|
210
|
+
2. **Wiring `jwtSecret` because `supabase status` prints a `JWT_SECRET`.** Current projects sign ES256; `asUser` then throws `SUPABASE.JWT_INVALID` explaining the token is ES256 and the client needs `jwksUrl`. Configure `SUPABASE_JWKS_URL`; reserve `jwtSecret` for legacy HS256 projects.
|
|
211
|
+
3. **Grants in the wrong direction.** Your `public` tables need no grants (Supabase's default privileges cover them; RLS protects the rows) — the grant you need is the narrow `auth.*` pair for `service_role` admin reads. `permission denied` (42501) means a missing grant, not a filtered result.
|
|
212
|
+
4. **Expecting `db.sql` / `db.orm` on the top-level `db`.** The Supabase db is role-first; bind a role, query the `RoleBoundDb`.
|
|
213
|
+
5. **Forgetting `await`** — on the `supabase()` factory and on `asUser(jwt)`. Both are async; `asAnon()` / `asServiceRole()` are not.
|
|
214
|
+
6. **Expecting `.supabase` on `asUser` / `asAnon`.** Admin access to `auth.*` is `service_role`-only by construction — and even `service_role` needs the one-time narrow grant first.
|
|
215
|
+
7. **A `policy_*` block whose target lacks `@@rls`.** Emit fails with `PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE` — add `@@rls` to the model.
|
|
216
|
+
8. **Unquoted camelCase columns or missing casts in predicates.** Predicates are verbatim SQL: `"userId"` needs quotes; compare uuid to `auth.uid()` with a `::uuid` cast where the column isn't already `uuid`.
|
|
217
|
+
9. **Passing both `jwksUrl` and `jwtSecret`** (or neither) — the `supabase()` promise rejects with `SUPABASE.CONFIG_INVALID`. It's an async factory, so the misconfiguration surfaces as a rejection (`await` / `.catch`), not a synchronous throw.
|
|
218
|
+
10. **Treating an RLS-filtered write as an error.** An `UPDATE` against a row the role can't see affects **0 rows** (no exception); only `withCheck` violations raise.
|
|
219
|
+
|
|
220
|
+
## What Prisma Next doesn't do yet
|
|
221
|
+
|
|
222
|
+
- **No `/control` subpath on the extension** — it can't register through the target façade's `defineConfig({ extensions: [...] })`; wiring goes through the low-level config's `extensions` as shown above. File interest via `references/feedback.md`.
|
|
223
|
+
- **`GRANT` authoring.** Table privileges are not contract elements; the one grant a Supabase app needs (the `service_role` `auth.*` pair for admin reads) is run once by hand (SQL editor / `psql`). If you want grants managed by the contract, file via `references/feedback.md`.
|
|
224
|
+
- **Transactions spanning the app root and the `.supabase` admin root.** The two roots are separate contract-bound runtimes sharing one pool; a cross-root transaction is not supported.
|
|
225
|
+
- **Triggers / functions as contract elements.** The classic "create a profile row on signup" `auth.users` trigger is authored as raw SQL against your database, not in the contract. `auth.uid()` etc. appear only inside opaque policy predicate strings.
|
|
226
|
+
- **Supabase Realtime, storage uploads, PostgREST / `@supabase/supabase-js` interop, edge runtimes.** Out of scope for the extension — it speaks Postgres directly (Node.js / Bun).
|
|
227
|
+
|
|
228
|
+
## Reference Files
|
|
229
|
+
|
|
230
|
+
- `examples/supabase` — the canonical runnable app: config, contract, `db.ts`, acceptance tests, README.
|
|
231
|
+
- `packages/3-extensions/supabase/README.md` — package-level reference (JWT modes, role-binding model, unsupported scope).
|
|
232
|
+
- `packages/3-extensions/supabase/src/runtime/supabase.ts` — the authoritative options/type surface (`SupabaseOptions`, `RoleBoundDb`, `ServiceRoleDb`).
|
|
233
|
+
|
|
234
|
+
## Checklist
|
|
235
|
+
|
|
236
|
+
- [ ] `extensions: [supabasePack]` in the low-level `defineConfig` (no `/control` subpath exists).
|
|
237
|
+
- [ ] Cross-space FK typed `supabase:auth.AuthUser` with explicit `fields` / `references` (+ `onDelete` if wanted).
|
|
238
|
+
- [ ] Every policy target model carries `@@rls`; predicates quote camelCase columns and cast for `auth.uid()`.
|
|
239
|
+
- [ ] `db.ts` uses `await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret })` — exactly one JWT key source; `jwksUrl` for current projects, `jwtSecret` only for legacy HS256.
|
|
240
|
+
- [ ] Queries go through a `RoleBoundDb` from `asUser` / `asAnon` / `asServiceRole`; `asUser` is awaited.
|
|
241
|
+
- [ ] `auth.*` / `storage.*` reads go through `asServiceRole().supabase` only, after the one-time narrow grant (`GRANT USAGE ON SCHEMA auth` + `GRANT SELECT` on the tables you read).
|
|
242
|
+
- [ ] No per-table grants added for your own `public` tables — Supabase default privileges cover them; RLS does the protecting.
|
|
243
|
+
- [ ] Connection is session-capable: session pooler or direct connection — never the 6543 transaction pooler.
|
|
244
|
+
- [ ] Did NOT confabulate a `/control` subpath, a top-level `db.sql`, `.supabase` on non-service roles, or grant authoring in the contract.
|