prisma-next-supabase
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrisma Next — Supabase
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 runtime.
supabase()编辑你的数据契约,其余工作由Prisma处理。
本技能涵盖如何端到端地在Supabase项目中使用Prisma Next:组合Supabase扩展包、在契约中引用Supabase托管的表、编写行级安全(RLS)策略,以及通过运行时执行角色绑定查询。
supabase()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 (cross-space FK).
auth.users - User wants to read Supabase-internal tables (,
auth.*) as an admin.storage.* - 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.
- 用户拥有Supabase项目(或计划创建),并希望将Prisma Next接入其中。
- 用户希望为表配置RLS策略(、
policy_select、@@rls)。auth.uid() - 用户需要按请求绑定角色(、
asUser(jwt)、asAnon())。asServiceRole() - 用户需要创建指向的跨空间外键。
auth.users - 用户希望以管理员身份读取Supabase内部表(、
auth.*)。storage.* - 用户提及:supabase、RLS、行级安全、策略、匿名用户(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 wiring, middleware, teardown →
db.ts.prisma-next-runtime - General query shapes (filtering, includes, aggregates) → — everything there applies to a role-bound
prisma-next-queriestoo.db - Migration planning / applying → .
prisma-next-migrations
- 通用契约编辑(模型、字段、关系)→ 使用。
prisma-next-contract - 非Supabase环境下的配置、中间件、销毁操作→ 使用
db.ts。prisma-next-runtime - 通用查询结构(过滤、关联查询、聚合)→ 使用——该技能中的所有内容同样适用于角色绑定的
prisma-next-queries。db - 迁移规划/执行→ 使用。
prisma-next-migrations
Key Concepts
核心概念
- The pack is an contract space.
externalships a complete, introspection-generated contract of everything Supabase owns — the@prisma-next/extension-supabase/packandauthschemas, their native enum types, and the platform roles (storage,anon,authenticated) — all with control policyservice_role. Composed viaexternal, it means: the migration planner emits no DDL for those objects (Supabase manages them), andextensionsconfirms they exist in the live database. Your own tables staydb verifyas usual.managed - Roles come from the pack; you never declare them. RLS identifiers resolve against the composed contract. Pointing the runtime at a non-Supabase Postgres fails verify with a
roles = [authenticated]issue naming the missing role — the common "wrong database" misconfiguration surfaces before queries run.not-found - The runtime is role-first. returns a
supabase()with no top-level query surface — there is noSupabaseDb/db.sqluntil you bind a role.db.orm/await db.asUser(jwt)/db.asAnon()each return adb.asServiceRole()exposingRoleBoundDb,.sql,.orm,.raw, and.execute(plan). 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..transaction(fn) - Role binding is below middleware and cannot leak. Each role-bound query runs on a connection that had and
set_config('role', …)applied beneath the user-middleware chain, withset_config('request.jwt.claims', …)on release. Postgres-sideRESET ALL/auth.uid()read those session vars — RLS enforcement is Postgres's job; the runtime's job is binding the context.auth.jwt() - RLS is enforced by policies and grants. Policies filter rows; 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
GRANTgets a permission error, not filtered rows. On Supabase yourGRANTtables already carry the platform-role grants via default privileges — the grant that is actually missing out of the box ispublic's onservice_role/auth.*(see Workflow — Grants).storage.* - JWT validation is eager and configurable — current Supabase projects need .
jwksUrlverifies the token (viaasUser(jwt)) before any connection is acquired: signature + expiry againstjose(asymmetric signing keys — the default on current Supabase projects, which sign ES256) xorjwksUrl(the symmetric HS256 secret — legacy projects only). Both or neither → a structured error with codejwtSecret. Bad tokens throw a structured error with codeSUPABASE.CONFIG_INVALIDand a typedSUPABASE.JWT_INVALID— including a mismatch between the token's algorithm and the configured key source (an ES256 token against ameta.reasonclient names the problem and tells you to switch tojwtSecret). The Postgres role is derived from the token'sjwksUrlclaim (defaults torole). Note:authenticatedstill prints asupabase statuseven on projects that sign ES256 — its presence does not mean your project uses it.JWT_SECRET - Admin access to /
auth.*is a secondary root onstorage.*only — and needs a one-time grant.service_roleexposes the pack's own contract (db.asServiceRole().supabase,.sql,.orm,.nativeEnums). The root exists only on.executeby design, but a real Supabase project grantsservice_roleno table privileges onservice_role/auth.*(only schemastorage.*; onlyUSAGEholds table grants). Before the admin root can read a Supabase-internal table, run the narrow grant once (see Workflow — Grants).postgres/asUserhave noasAnon, and the primary.supabase/asServiceRole().sqlstay scoped to your contract..orm
- 扩展包是一个契约空间。
external包含一套完整的、基于 introspection 生成的Supabase托管资源契约——@prisma-next/extension-supabase/pack和auth模式、原生枚举类型,以及平台角色(storage、anon、authenticated)——所有资源均标记为service_role控制策略。通过external组合后,迁移规划器不会为这些对象生成DDL(由Supabase管理),且extensions会验证它们在实时数据库中是否存在。你自己的表仍保持默认的db verify状态。managed - 角色由扩展包提供,无需自行声明。RLS中的标识符会解析为组合契约中的角色。若运行时指向非Supabase的Postgres数据库,
roles = [authenticated]会抛出verify错误,提示缺失对应角色——这种常见的「数据库配置错误」会在查询执行前暴露。not-found - 运行时以角色为核心。返回的
supabase()没有顶层查询接口——绑定角色前,不存在SupabaseDb/db.sql接口。db.orm/await db.asUser(jwt)/db.asAnon()各自返回一个db.asServiceRole(),提供RoleBoundDb、.sql、.orm、.raw和.execute(plan)接口。这是刻意设计的:在Supabase应用中,不存在有意义的「无角色」执行上下文,默认使用连接的登录角色会导致RLS被静默绕过的风险。.transaction(fn) - 角色绑定位于中间件之下,不会泄露。每个角色绑定的查询都会在一个已执行和
set_config('role', …)的连接上运行,且这些配置会在连接释放时执行set_config('request.jwt.claims', …)。Postgres端的RESET ALL/auth.uid()会读取这些会话变量——RLS的执行由Postgres负责;运行时的职责是绑定上下文。auth.jwt() - RLS由策略和权限授予(GRANT)共同强制执行。策略用于过滤行;用于控制表访问权限。Prisma Next负责编写和迁移策略,但不处理权限授予(详见「Prisma Next暂不支持的功能」)。拥有策略但无
GRANT权限的角色会收到权限错误,而非过滤后的行数据。在Supabase中,你的GRANT表已通过默认权限配置了平台角色的授予——真正缺失的默认权限是public对service_role/auth.*的访问权限(详见「工作流——权限授予」)。storage.* - JWT验证是即时且可配置的——当前Supabase项目需要。
jwksUrl会在获取任何连接之前验证令牌(通过asUser(jwt)库):基于jose(非对称签名密钥——当前Supabase项目的默认方式,采用ES256签名)或jwksUrl(对称HS256密钥——仅适用于旧版项目)验证签名和过期时间。若同时配置两者或均未配置,会抛出带有jwtSecret代码的结构化错误。无效令牌会抛出带有SUPABASE.CONFIG_INVALID代码的结构化错误,以及类型化的SUPABASE.JWT_INVALID——包括令牌算法与配置的密钥源不匹配(例如ES256令牌对应meta.reason客户端时,会指出问题并建议切换到jwtSecret)。Postgres角色由令牌的jwksUrl声明派生(默认为role)。注意:即使在使用ES256签名的项目中,authenticated仍会输出supabase status,但这并不意味着项目使用该密钥。JWT_SECRET - 仅可通过二级根访问
service_role/auth.*——且需要一次性权限授予。storage.*提供扩展包自身的契约接口(db.asServiceRole().supabase、.sql、.orm、.nativeEnums)。该根接口仅在.execute下存在,但实际Supabase项目未授予service_role对service_role/auth.*的表权限(仅授予模式storage.*权限;只有USAGE角色拥有表权限)。在管理员根接口能够读取Supabase内部表之前,需先执行一次窄范围的权限授予(详见「工作流——权限授予」)。postgres/asUser没有asAnon接口,且.supabase/asServiceRole().sql仍限定于你的契约范围。.orm
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 subpath yet, so it can't go through the target façade's — it wires into the low-level config's (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 verbatim — copy it rather than composing your own:
/controldefineConfig({ extensions: [...] })extensionsexamples/supabase/prisma-next.config.tstypescript
// 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' },
});核心思路:扩展包注册Supabase契约空间,以便你的契约可以引用它,同时规划器/验证器能够识别Supabase托管的资源。该扩展目前没有子路径,因此无法通过目标外观的注册——需接入底层配置的(详见「Prisma Next暂不支持的功能」)。以下底层导入是刻意打破外观唯一导入规则的例外,由当前功能缺口导致;代码块完全镜像——建议直接复制而非自行编写:
/controldefineConfig({ extensions: [...] })extensionsexamples/supabase/prisma-next.config.tstypescript
// 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
auth.users工作流——契约:指向auth.users
的外键 + RLS策略
auth.usersThe concept: your models live in your namespaces (); Supabase's live in the pack's (, ). A relation field typed is a cross-space FK — the planner emits , and the target table is verified, never migrated. RLS policies are top-level blocks in the same namespace as their target model, and the target model must opt in with . Mirror :
publicauthstoragesupabase:auth.AuthUserREFERENCES "auth"."users"("id")policy_<operation>@@rlsexamples/supabase/src/contract.prismaprisma
types {
Uuid = String @db.Uuid
}
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).
policy_select profile_public_read {
target = Profile
roles = [anon]
using = "true"
}
// authenticated may update only their own profile, and may not
// reassign it to another owner (WITH CHECK).
policy_update profile_owner_write {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
withCheck = "\"userId\"::uuid = auth.uid()"
}
}The pieces:
- Per-operation policy blocks: ,
policy_select,policy_insert,policy_update,policy_delete. Body ispolicy_all:key = value(a model in this namespace),target(resolve against the composed contract — the pack suppliesroles/anon/authenticated),service_role, and (for write operations)using. Multiple permissive policies perwithCheckare valid — Postgres ORs them.(target, operation) - is required on policy targets. A
@@rlsblock whose target model lackspolicy_*fails emit with@@rls. A model withPSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTEand no policies is also meaningful: RLS enabled, deny-all.@@rls - Predicates are verbatim SQL strings. Quote camelCase column names inside them (), and cast where needed —
\"userId\"returnsauth.uid(). Renames in your contract do not rewrite predicate bodies.uuid - TS-builder parity exists. exports
@prisma-next/postgres/contract-builder/policySelect/policyInsert/policyUpdate/policyDelete,policyAll, andrlsEnabled(Model)— mirroring the PSL lowering key-for-key (identical emitted wire names). PSL is the canonical path shown here.role('anon')
Emit + migrate as usual (, then ). The plan creates your table, its FK, , and the statements — and no DDL for .
prisma-next contract emitprisma-next-migrationsENABLE ROW LEVEL SECURITYCREATE POLICYauth.*核心思路:你的模型位于自己的命名空间();Supabase的模型位于扩展包的命名空间(、)。类型为的关联字段是跨空间外键——规划器会生成,目标表会被验证但不会被迁移。RLS策略是与目标模型同命名空间的顶层块,且目标模型必须通过启用RLS。镜像的写法:
publicauthstoragesupabase:auth.AuthUserREFERENCES "auth"."users"("id")policy_<operation>@@rlsexamples/supabase/src/contract.prismaprisma
types {
Uuid = String @db.Uuid
}
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
}
// 已认证用户仅可读取自己的个人资料
policy_select profile_owner_read {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
}
// 匿名用户可读取所有个人资料(公开目录列表)
policy_select profile_public_read {
target = Profile
roles = [anon]
using = "true"
}
// 已认证用户仅可更新自己的个人资料,且不可将其重新分配给其他所有者(WITH CHECK)
policy_update profile_owner_write {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
withCheck = "\"userId\"::uuid = auth.uid()"
}
}关键要点:
- 按操作划分的策略块:、
policy_select、policy_insert、policy_update、policy_delete。内容为policy_all格式:key = value(当前命名空间中的模型)、target(解析为组合契约中的角色——扩展包提供roles/anon/authenticated)、service_role,以及(针对写入操作)using。同一withCheck下可存在多个宽松策略——Postgres会将它们进行逻辑或运算。(target, operation) - 策略目标模型必须启用。若
@@rls块的目标模型未配置policy_*,生成过程会抛出@@rls错误。启用PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE但未配置任何策略的模型也是有意义的:RLS已启用,默认拒绝所有访问。@@rls - 谓词是原始SQL字符串。需对驼峰式列名添加引号(),必要时进行类型转换——
\"userId\"返回auth.uid()类型。契约中的重命名不会改写谓词内容。uuid - 支持TS构建器语法。导出
@prisma-next/postgres/contract-builder/policySelect/policyInsert/policyUpdate/policyDelete、policyAll和rlsEnabled(Model)——与PSL语法完全对应(生成的键名一致)。本文以PSL为标准写法展示。role('anon')
按常规流程生成并迁移(,然后执行)。迁移计划会创建你的表、外键、和语句——不会为生成任何DDL。
prisma-next contract emitprisma-next-migrationsENABLE ROW LEVEL SECURITYCREATE POLICYauth.*Workflow — db.ts
with the supabase()
factory
db.tssupabase()工作流——使用supabase()
工厂构建db.ts
supabase()db.tsThe concept: instead of the stock factory, a Supabase app builds its client with from the extension's subpath. The factory is async (it prepares JWT key material — including the one-time JWKS fetch when is set), and the result is role-first.
postgres()supabase()/runtimejwksUrltypescript
// src/prisma/db.ts
import { supabase } from '@prisma-next/extension-supabase/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = await supabase<Contract>({
contractJson,
url: process.env['DATABASE_URL'], // direct Postgres connection — see pitfalls
jwksUrl: process.env['SUPABASE_JWKS_URL'], // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
// Legacy HS256 projects use jwtSecret: process.env['SUPABASE_JWT_SECRET'] instead — exactly one of the two.
});Options beyond the basics: (same composition as — see ; middleware never sees the role-binding calls), , (BYO / instead of ). Teardown is / exactly as in — the same script-hang rules apply.
middlewarepostgres()prisma-next-runtimeset_configpoolOptionspgpg.Poolpg.Clienturlawait db.close()await usingprisma-next-runtime核心思路:替代默认的工厂,Supabase应用需使用扩展包子路径中的构建客户端。该工厂是异步的(会准备JWT密钥材料——包括设置时的一次性JWKS获取),且返回结果以角色为核心。
postgres()/runtimesupabase()jwksUrltypescript
// src/prisma/db.ts
import { supabase } from '@prisma-next/extension-supabase/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = await supabase<Contract>({
contractJson,
url: process.env['DATABASE_URL'], // 直接Postgres连接——注意陷阱
jwksUrl: process.env['SUPABASE_JWKS_URL'], // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
// 旧版HS256项目使用jwtSecret: process.env['SUPABASE_JWT_SECRET']替代——二选一,不可同时配置
});基础配置之外的选项:(与的组合方式相同——详见;中间件不会看到角色绑定的调用)、、(自定义/替代)。销毁操作与一致:/——同样遵循脚本挂起规则。
middlewarepostgres()prisma-next-runtimeset_configpoolOptionspgpg.Poolpg.Clienturlprisma-next-runtimeawait db.close()await usingWorkflow — Role-bound queries
工作流——角色绑定查询
The concept: bind the role that should execute the request, then query through the returned — every query surface from works, RLS-filtered by Postgres.
RoleBoundDbprisma-next-queriestypescript
// A signed-in user: rows are RLS-scoped to the JWT's auth.uid().
const userDb = await db.asUser(jwt); // async — rejects with code SUPABASE.JWT_INVALID on a bad/expired token
const mine = await userDb.orm.public.Profile.select('id', 'username').all();
// The anon role: sees what anon policies permit.
const listing = await db.asAnon().orm.public.Profile.select('id', 'username').all();
// service_role: BYPASSRLS — sees everything in YOUR contract.
const all = await db.asServiceRole().orm.public.Profile.select('id', 'username').all();
// Writes ride the same surfaces; RLS filters them too. An UPDATE against
// another owner's row affects 0 rows; a withCheck violation raises an error.
const updated = await userDb.orm.public.Profile
.where({ userId: me })
.updateAndCount({ username: 'new-name' });Notes: / are sync; only is async. Multi-namespace contracts address models by coordinate (, ) — see § Namespace-aware accessors. wraps work in a transaction on the role-bound session.
asAnon()asServiceRole()asUserorm.public.Profilesql.public.profileprisma-next-queriesRoleBoundDb.transaction(fn)核心思路:绑定执行请求的角色,然后通过返回的执行查询——中的所有查询接口均适用,结果由Postgres进行RLS过滤。
RoleBoundDbprisma-next-queriestypescript
// 已登录用户:行数据受JWT的auth.uid()范围限制
const userDb = await db.asUser(jwt); // 异步操作——令牌无效/过期时会抛出SUPABASE.JWT_INVALID错误
const mine = await userDb.orm.public.Profile.select('id', 'username').all();
// 匿名角色:仅能看到匿名策略允许的数据
const listing = await db.asAnon().orm.public.Profile.select('id', 'username').all();
// service_role:绕过RLS——可查看你的契约中的所有数据
const all = await db.asServiceRole().orm.public.Profile.select('id', 'username').all();
// 写入操作使用相同接口;同样受RLS过滤。更新其他所有者的行时,影响行数为0;违反withCheck规则会抛出错误
const updated = await userDb.orm.public.Profile
.where({ userId: me })
.updateAndCount({ username: 'new-name' });注意事项:/是同步操作;仅是异步操作。多命名空间契约通过坐标访问模型(、)——详见中的「命名空间感知访问器」章节。会将操作包裹在角色绑定会话的事务中。
asAnon()asServiceRole()asUserorm.public.Profilesql.public.profileprisma-next-queriesRoleBoundDb.transaction(fn)Workflow — Admin reads of auth.*
/ storage.*
auth.*storage.*工作流——管理员读取auth.*
/storage.*
auth.*storage.*The concept: Supabase-internal tables are not part of your contract, so they are not on your query surfaces. The binding carries a secondary root — — which is the pack's contract surface:
service_roledb.asServiceRole().supabasetypescript
const admin = db.asServiceRole();
// SQL builder over the pack contract:
const users = await admin.supabase
.execute(admin.supabase.sql.auth.users.select('id', 'email').build())
.toArray();
// ORM over the pack contract:
const sessions = await admin.supabase.orm.auth.AuthSession.select('id', 'aal').all();
// Native enum values (e.g. auth.aal_level) come typed:
type AalLevel = (typeof admin.supabase.nativeEnums.auth.AalLevel)['Value'];The admin root needs a one-time grant. A real Supabase project gives no table privileges on / — out of the box, the reads above fail with (sqlState ). Grant exactly what you read, narrowly:
service_roleauth.*storage.*permission denied for table users42501sql
GRANT USAGE ON SCHEMA auth TO service_role;
GRANT SELECT ON TABLE auth.users TO service_role;Other boundaries to respect: / have no ; the admin root has no (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 SQL is for ad-hoc admin reads.
asUserasAnon.supabase.transactionservice_role核心思路:Supabase内部表不属于你的契约,因此不会出现在你的查询接口中。绑定提供二级根接口——,即扩展包的契约接口:
service_roledb.asServiceRole().supabasetypescript
const admin = db.asServiceRole();
// 基于扩展包契约的SQL构建器:
const users = await admin.supabase
.execute(admin.supabase.sql.auth.users.select('id', 'email').build())
.toArray();
// 基于扩展包契约的ORM:
const sessions = await admin.supabase.orm.auth.AuthSession.select('id', 'aal').all();
// 原生枚举值(如auth.aal_level)带有类型:
type AalLevel = (typeof admin.supabase.nativeEnums.auth.AalLevel)['Value'];管理员根接口需要一次性权限授予。实际Supabase项目未授予对/的表权限——默认情况下,上述读取操作会抛出错误(sqlState )。请仅授予实际需要读取的权限,保持范围尽可能窄:
service_roleauth.*storage.*permission denied for table users42501sql
GRANT USAGE ON SCHEMA auth TO service_role;
GRANT SELECT ON TABLE auth.users TO service_role;需遵守的其他限制:/没有接口;管理员根接口没有(它是共享连接池的独立契约绑定运行时——跨根接口的事务不受支持);对于用户管理(创建用户、重置密码),建议使用GoTrue Admin API——Supabase内部模式可能随平台升级而变化;直接使用执行SQL仅适用于临时管理员读取操作。
asUserasAnon.supabase.transactionservice_roleWorkflow — Grants
工作流——权限授予
The concept: RLS policies are row filters on top of ordinary table privileges — a role with policies but no gets , not filtered rows. On Supabase the two directions are easy to get backwards:
GRANTpermission denied- Your own tables need nothing. Supabase ships
publiconALTER DEFAULT PRIVILEGES, so tables created bypublic/prisma-next db initinherit full grants formigrate/anon/authenticatedautomatically — 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.service_role - The one grant you do need is for admin reads of Supabase-internal tables — has no table privileges on
service_role/auth.*(see Admin reads above for the narrowstorage.*/GRANT USAGEpair).GRANT SELECT
Run grants via the Supabase SQL editor or . Symptom of a missing grant: (sqlState ) instead of an empty result.
psqlpermission denied for table …42501核心思路:RLS策略是在普通表权限之上的行过滤器——拥有策略但无权限的角色会收到错误,而非过滤后的行数据。在Supabase中,容易混淆以下两种权限配置方向:
GRANTpermission denied- 你的表无需额外配置。Supabase在
public模式上配置了public,因此通过ALTER DEFAULT PRIVILEGES/prisma-next db init创建的表会自动继承migrate/anon/authenticated的完整权限——与通过仪表盘创建的表一致。RLS策略才是真正保护行数据的机制;无需添加表级权限,除非有特殊理由,否则不要修改默认权限。service_role - 唯一需要配置的权限是管理员读取Supabase内部表的权限——对
service_role/auth.*没有表权限(详见「管理员读取」章节中的窄范围storage.*/GRANT USAGE组合)。GRANT SELECT
可通过Supabase SQL编辑器或执行权限授予操作。权限缺失的症状:抛出错误(sqlState ),而非返回空结果。
psqlpermission denied for table …42501Workflow — Connecting to a real Supabase project
工作流——连接到真实的Supabase项目
The concept: the runtime needs a direct, session-capable Postgres connection — it binds roles with session-scoped + .
set_configRESET ALL- Session pooler (, username
aws-0-<region>.pooler.supabase.com:5432) — works everywhere, IPv4. The default choice.postgres.<project-ref> - Direct connection () — works, but is IPv6-only on new projects; from IPv4-only environments it fails DNS/connect.
db.<project-ref>.supabase.co:5432 - Transaction pooler (port 6543) — do not use. Transaction pooling breaks session GUCs; role binding will misbehave.
.envDATABASE_URLSUPABASE_JWKS_URLhttps://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonhttp://127.0.0.1:54321/auth/v1/.well-known/jwks.jsonSUPABASE_JWT_SECRETsupabase statusJWT_SECRETalg核心思路:运行时需要支持会话的直接Postgres连接——它通过会话级别的+绑定角色。
set_configRESET ALL- 会话池(,用户名
aws-0-<region>.pooler.supabase.com:5432)——适用于所有环境,支持IPv4。推荐使用。postgres.<project-ref> - 直接连接()——可用,但新项目仅支持IPv6;在仅支持IPv4的环境中会出现DNS/连接失败。
db.<project-ref>.supabase.co:5432 - 事务池(端口6543)——请勿使用。事务池会破坏会话GUC;角色绑定将无法正常工作。
.envDATABASE_URLSUPABASE_JWKS_URLhttps://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonhttp://127.0.0.1:54321/auth/v1/.well-known/jwks.jsonSUPABASE_JWT_SECRETsupabase statusJWT_SECRETalgCommon Pitfalls
常见陷阱
- Using the transaction pooler (port 6543). Session GUC role binding requires a session-capable connection — use the session pooler (5432) or the direct connection.
- Wiring because
jwtSecretprints asupabase status. Current projects sign ES256;JWT_SECRETthen throwsasUserexplaining the token is ES256 and the client needsSUPABASE.JWT_INVALID. ConfigurejwksUrl; reserveSUPABASE_JWKS_URLfor legacy HS256 projects.jwtSecret - Grants in the wrong direction. Your tables need no grants (Supabase's default privileges cover them; RLS protects the rows) — the grant you need is the narrow
publicpair forauth.*admin reads.service_role(42501) means a missing grant, not a filtered result.permission denied - Expecting /
db.sqlon the top-leveldb.orm. The Supabase db is role-first; bind a role, query thedb.RoleBoundDb - Forgetting — on the
awaitfactory and onsupabase(). Both are async;asUser(jwt)/asAnon()are not.asServiceRole() - Expecting on
.supabase/asUser. Admin access toasAnonisauth.*-only by construction — and evenservice_roleneeds the one-time narrow grant first.service_role - A block whose target lacks
policy_*. Emit fails with@@rls— addPSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTEto the model.@@rls - Unquoted camelCase columns or missing casts in predicates. Predicates are verbatim SQL: needs quotes; compare uuid to
"userId"with aauth.uid()cast where the column isn't already::uuid.uuid - Passing both and
jwksUrl(or neither) — thejwtSecretpromise rejects withsupabase(). It's an async factory, so the misconfiguration surfaces as a rejection (SUPABASE.CONFIG_INVALID/await), not a synchronous throw..catch - Treating an RLS-filtered write as an error. An against a row the role can't see affects 0 rows (no exception); only
UPDATEviolations raise.withCheck
- 使用事务池(端口6543)。会话GUC角色绑定需要支持会话的连接——请使用会话池(5432)或直接连接。
- 因输出
supabase status而配置JWT_SECRET。当前项目使用ES256签名;此时jwtSecret会抛出asUser错误,提示令牌为ES256类型,客户端需使用SUPABASE.JWT_INVALID。请配置jwksUrl;仅旧版HS256项目使用SUPABASE_JWKS_URL。jwtSecret - 权限授予方向错误。你的表无需额外权限(Supabase的默认权限已覆盖;RLS负责保护行数据)——唯一需要配置的是
public读取service_role的窄范围权限。auth.*(42501)表示权限缺失,而非结果被过滤。permission denied - 期望顶层拥有
db/db.sql接口。Supabase的db以角色为核心;需先绑定角色,再通过db.orm执行查询。RoleBoundDb - 忘记添加——
await工厂和supabase()均为异步操作。asUser(jwt)/asAnon()是同步操作,无需asServiceRole()。await - 期望/
asUser拥有asAnon接口。按设计,仅.supabase可访问service_role的管理员接口——且auth.*需先完成一次性窄范围权限授予。service_role - 块的目标模型未配置
policy_*。生成过程会抛出@@rls错误——需为模型添加PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE。@@rls - 谓词中未对驼峰式列名添加引号或缺失类型转换。谓词是原始SQL:需要引号;若列不是
"userId"类型,需添加uuid转换以与::uuid比较。auth.uid() - 同时配置和
jwksUrl(或均未配置)——jwtSecret承诺会抛出supabase()错误。它是异步工厂,因此配置错误会以拒绝(SUPABASE.CONFIG_INVALID/await)的形式暴露,而非同步抛出。.catch - 将RLS过滤的写入操作视为错误。更新角色无权访问的行时,影响行数为0(无异常);仅违反规则时才会抛出错误。
withCheck
What Prisma Next doesn't do yet
Prisma Next暂不支持的功能
- No subpath on the extension — it can't register through the target façade's
/control; wiring goes through the low-level config'sdefineConfig({ extensions: [...] })as shown above. File interest viaextensions.prisma-next-feedback - authoring. Table privileges are not contract elements; the one grant a Supabase app needs (the
GRANTservice_rolepair for admin reads) is run once by hand (SQL editor /auth.*). If you want grants managed by the contract, file viapsql.prisma-next-feedback - Transactions spanning the app root and the admin root. The two roots are separate contract-bound runtimes sharing one pool; a cross-root transaction is not supported.
.supabase - Triggers / functions as contract elements. The classic "create a profile row on signup" trigger is authored as raw SQL against your database, not in the contract.
auth.usersetc. appear only inside opaque policy predicate strings.auth.uid() - Supabase Realtime, storage uploads, PostgREST / interop, edge runtimes. Out of scope for the extension — it speaks Postgres directly (Node.js / Bun).
@supabase/supabase-js
- 扩展包无子路径——无法通过目标外观的
/control注册;需按上述方式接入底层配置的defineConfig({ extensions: [...] })。可通过extensions提交需求。prisma-next-feedback - 权限授予(GRANT)管理。表权限不属于契约元素;Supabase应用唯一需要的权限授予(读取
service_role的组合)需手动执行一次(SQL编辑器/auth.*)。若希望通过契约管理权限授予,可通过psql提交需求。prisma-next-feedback - 跨应用根和管理员根的事务。两个根接口是共享连接池的独立契约绑定运行时;跨根接口的事务不受支持。
.supabase - 触发器/函数作为契约元素。经典的「注册时创建个人资料行」触发器需通过原始SQL编写,而非在契约中定义。
auth.users等仅出现在策略谓词的原始字符串中。auth.uid() - Supabase Realtime、存储上传、PostgREST/互操作、边缘运行时。这些不在本扩展的范围内——它仅直接与Postgres通信(Node.js/Bun环境)。
@supabase/supabase-js
Reference Files
参考文件
- — the canonical runnable app: config, contract,
examples/supabase, acceptance tests, README.db.ts - — package-level reference (JWT modes, role-binding model, unsupported scope).
packages/3-extensions/supabase/README.md - — the authoritative options/type surface (
packages/3-extensions/supabase/src/runtime/supabase.ts,SupabaseOptions,RoleBoundDb).ServiceRoleDb
- ——标准可运行应用:配置、契约、
examples/supabase、验收测试、README。db.ts - ——包级参考文档(JWT模式、角色绑定模型、不支持的范围)。
packages/3-extensions/supabase/README.md - ——权威选项/类型接口(
packages/3-extensions/supabase/src/runtime/supabase.ts、SupabaseOptions、RoleBoundDb)。ServiceRoleDb
Checklist
检查清单
- in the low-level
extensions: [supabasePack](nodefineConfigsubpath exists)./control - Cross-space FK typed with explicit
supabase:auth.AuthUser/fields(+referencesif wanted).onDelete - Every policy target model carries ; predicates quote camelCase columns and cast for
@@rls.auth.uid() - uses
db.ts— exactly one JWT key source;await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret })for current projects,jwksUrlonly for legacy HS256.jwtSecret - Queries go through a from
RoleBoundDb/asUser/asAnon;asServiceRoleis awaited.asUser - /
auth.*reads go throughstorage.*only, after the one-time narrow grant (asServiceRole().supabase+GRANT USAGE ON SCHEMA authon the tables you read).GRANT SELECT - No per-table grants added for your own tables — Supabase default privileges cover them; RLS does the protecting.
public - Connection is session-capable: session pooler or direct connection — never the 6543 transaction pooler.
- Did NOT confabulate a subpath, a top-level
/control,db.sqlon non-service roles, or grant authoring in the contract..supabase
- 在底层中配置
defineConfig(目前无extensions: [supabasePack]子路径)。/control - 配置类型为的跨空间外键,指定
supabase:auth.AuthUser/fields(可选配置references)。onDelete - 所有策略目标模型均配置;谓词对驼峰式列名添加引号,并针对
@@rls进行类型转换。auth.uid() - 使用
db.ts——JWT密钥源二选一;当前项目使用await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret }),仅旧版HS256项目使用jwksUrl。jwtSecret - 查询通过/
asUser/asAnon返回的asServiceRole执行;RoleBoundDb已添加asUser。await - /
auth.*读取仅通过storage.*执行,且已完成一次性窄范围权限授予(asServiceRole().supabase+GRANT USAGE ON SCHEMA auth目标表)。GRANT SELECT - 未为自己的表添加表级权限——Supabase默认权限已覆盖;RLS负责保护数据。
public - 使用支持会话的连接:会话池或直接连接——绝不使用6543端口的事务池。
- 未虚构子路径、顶层
/control、非服务角色的db.sql接口,或在契约中管理权限授予。.supabase