Prisma Next Supabase
Use Prisma Next with a Supabase project via `@prisma-next/extension-supabase` — wire `extensions: [supabasePack]`, declare cross-space FKs to `supabase:auth.AuthUser`, author RLS policies (`policy_select` / `policy_update` / `@@rls`, `auth.uid()` predicates), build `db.ts` with the `supabase()` factory, bind roles per request (`asUser(jwt)` / `asAnon()` / `asServiceRole()`), query `auth.*` / `storage.*` via the `db.asServiceRole().supabase` admin root, and validate JWTs (`jwksUrl` for current projects / `jwtSecret` for legacy HS256). Use for supabase, RLS, row level security, policy, role binding, anon, authenticated, service_role, auth.users, auth.uid(), JWT, JWKS, SUPABASE_JWKS_URL, SUPABASE_JWT_SECRET, SUPABASE.JWT_INVALID, SUPABASE.CONFIG_INVALID, RoleBoundDb, session pooler, supabase:auth.AuthUser, @prisma-next/extension-supabase.
- Skill ID
- prisma/prisma-next/prisma-next-supabase
- Publisher
- prisma
- Repository
- prisma-next
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
prisma/prisma-next/prisma-next-supabaseInstalls these files- SKILL.md
What this skill tells the agent
Prisma Next — Supabase
Edit your data contract. Prisma handles the rest.
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.
When to Use
- User has a Supabase project (or wants one) and is wiring Prisma Next into it.
- User wants RLS policies on their tables (
policy_select,@@rls,auth.uid()). - User wants per-request role binding (
asUser(jwt),asAnon(),asServiceRole()). - User wants a foreign key into
auth.users(cross-space FK). - User wants to read Supabase-internal tables (
auth.*,storage.*) as an admin. - 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.
When Not to Use
- General contract editing (models, fields, relations) →
prisma-next-contract. - Non-Supabase
db.tswiring, middleware, teardown →prisma-next-runtime. - General query shapes (filtering, includes, aggregates) →
prisma-next-queries— everything there applies to a role-bounddbtoo. - Migration planning / applying →
prisma-next-migrations.
Key Concepts
- The pack is an `external` contract space.
@prisma-next/extension-supabase/packships a complete, introspection-generated contract of everything Supabase owns — theauthandstorageschemas, their native enum types, and the platform roles (anon,authenticated,service_role) — all with control policyexternal. Composed viaextensions, it means: the migration planner emits no DDL for those objects (Supabase manages them), anddb verifyconfirms they exist in the live database. Your own tables staymanagedas usual. - 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 anot-foundissue naming the missing role — the common "wrong database" misconfiguration surfaces before queries run. - The runtime is role-first.
supabase()returns aSupabaseDbwith no top-level query surface — there is nodb.sql/db.ormuntil you bind a role.await db.asUser(jwt)/db.asAnon()/db.asServiceRole()each return aRoleBoundDbexposing.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. - Role binding is below middleware and cannot leak. Each role-bound query runs on a connection that had
set_config('role', …)andset_config('request.jwt.claims', …)applied beneath the user-middleware chain, withRESET ALLon release. Postgres-sideauth.uid()/auth.jwt()read those session vars — RLS enforcement is Postgres's job; the runtime's job is binding the context. - *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*). - JWT validation is eager and configurable — current Supabase projects need `jwksUrl`.
asUser(jwt)verifies the token (viajose) before any connection is acquired: signature + expiry againstjwksUrl(asymmetric signing keys — the default on current Supabase projects, which sign ES256) xorjwtSecret(the symmetric HS256 secret — legacy projects only). Both or neither → a structured error with codeSUPABASE.CONFIG_INVALID. Bad tokens throw a structured error with codeSUPABASE.JWT_INVALIDand a typedmeta.reason— including a mismatch between the token's algorithm and the configured key source (an ES256 token against ajwtSecretclient names the problem and tells you to switch tojwksUrl). The Postgres role is derived from the token'sroleclaim (defaults toauthenticated). Note:supabase statusstill prints aJWT_SECRETeven on projects that sign ES256 — its presence does not mean your project uses it. - *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.
Workflow — Wire the pack into the config
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-next.config.ts verbatim — copy it rather than composing your own:
// prisma-next.config.ts
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import { defineConfig } from '@prisma-next/cli/config-types';
import postgresDriver from '@prisma-next/driver-postgres/control';
import supabasePack from '@prisma-next/extension-supabase/pack';
import sql from '@prisma-next/family-sql/control';
import { prismaContract } from '@prisma-next/sql-contract-psl/provider';
import postgres from '@prisma-next/target-postgres/control';
import postgresPackRef from '@prisma-next/target-postgres/pack';
import { postgresCreateNamespace } from '@prisma-next/target-postgres/types';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensions: [supabasePack],
contract: prismaContract('./src/contract.prisma', {
output: 'src/contract.json',
target: postgresPackRef,
createNamespace: postgresCreateNamespace,
}),
migrations: { dir: 'migrations' },
});Workflow — Contract: FK into auth.users + RLS policies
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:
namespace public {
model Profile {
id Uuid @id @default(uuid())
username String
userId Uuid @unique
user supabase:auth.AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profile")
@@rls
}
// authenticated may read only their own profile.
policy_select profile_owner_read {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
}
// anon may read every profile (a public directory listing).More from prisma
Use this skill in a project
Open any RiverX project, search the Skills panel for prisma/prisma-next/prisma-next-supabase, and install it in one click.
