关于 prisma-next-supabase
面向将 Prisma Next 接入 Supabase 的开发者,涵盖契约配置、RLS 策略和按角色执行查询。其重点是组合 Supabase 外部契约,并在查询前绑定并验证用户角色与 JWT。
主要功能
- 01组合 Supabase 外部契约
- 02定义 RLS 策略
- 03关联 auth.users 外键
- 04按角色绑定查询
- 05验证用户 JWT
- 06读取 Supabase 内部表
适用场景
- 01为自有表配置 RLS
- 02将模型关联到 auth.users
- 03以用户或匿名角色查询
- 04通过 service_role 管理读取 auth.*
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ prisma/ prisma-next --skill prisma-next-supabase安装后在 Claude Code 中输入/prisma-next-supabase确认已加载。
详细介绍
Prisma Next — Supabase
编辑你的数据契约,其余工作交给 Prisma。
本技能涵盖如何端到端地将 Prisma Next 用于 Supabase 项目:组合 Supabase 扩展包、在契约中引用 Supabase 所有的表、编写行级安全(RLS)策略,以及通过 supabase() 运行时执行绑定角色的查询。
适用场景
- 用户已有 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。
不适用场景
- 通用契约编辑(模型、字段、关系)→
prisma-next-contract。 - 非 Supabase 的
db.ts接线、中间件、拆卸 →prisma-next-runtime。 - 通用查询结构(筛选、包含、聚合)→
prisma-next-queries—— 其中的所有内容同样适用于绑定角色的db。 - 迁移规划或执行 →
prisma-next-migrations。
核心概念
- 该包是一个
external契约空间。@prisma-next/extension-supabase/pack提供一份由内省生成的完整契约,涵盖 Supabase 管理的所有内容——auth和storageschema、它们的原生枚举类型,以及平台角色(anon、authenticated、service_role)——控制策略均为external。通过extensions组合后,这意味着:迁移规划器不会为这些对象生成 DDL(它们由 Supabase 管理),而db verify会确认它们存在于实时数据库中。你自己的表仍按惯例保持为managed。 - 角色来自该包;你无需声明它们。 RLS 中的
roles = [authenticated]标识符会根据组合后的契约进行解析。若将运行时指向非 Supabase 的 Postgres,验证会失败,并报告一条命名缺失角色的not-found问题——常见的“数据库选错”配置错误会在运行查询前暴露。 - 运行时以角色为先。
supabase()返回一个没有顶层查询接口的SupabaseDb——绑定角色前,不存在db.sql/db.orm。await db.asUser(jwt)/db.asAnon()/db.asServiceRole()各自会返回一个提供.sql、.orm、.raw、.execute(plan)和.transaction(fn)的RoleBoundDb。这是有意设计的:在 Supabase 应用中,不存在有意义的“无角色”执行上下文;默认使用连接的登录角色,则会带来悄无声息绕过 RLS 的风险。 - 角色绑定发生在中间件下层,不会泄漏。 每次角色绑定查询都会使用一个已在用户中间件链下方应用
set_config('role', …)和set_config('request.jwt.claims', …)的连接,并在释放时执行RESET ALL。Postgres 端的auth.uid()/auth.jwt()会读取这些会话变量——RLS 强制执行由 Postgres 负责;运行时负责绑定上下文。 - RLS 由策略和授权共同强制执行。 策略筛选行;
GRANT控制表访问权限。Prisma Next 的作者会编写并迁移策略,但不会编写授权(参见Prisma Next 目前尚未实现的功能)。有策略但没有GRANT的角色会收到权限错误,而不是筛选后的行。在 Supabase 上,你的public表已通过默认权限获得平台角色授权——开箱即缺少的授权实际上是service_role对auth.*/storage.*的授权(参见工作流 — 授权)。 - JWT 验证会提前执行,且可配置——当前的 Supabase 项目需要
jwksUrl。asUser(jwt)会在获取任何连接之前验证令牌(通过jose):根据jwksUrl验证签名和过期时间(非对称签名密钥——当前 Supabase 项目的默认配置,使用 ES256 签名),或者使用jwtSecret(对称 HS256 密钥——仅限旧版项目)。两者同时配置或两者都未配置时,都会返回带有代码SUPABASE.CONFIG_INVALID的结构化错误。无效令牌会抛出带有代码SUPABASE.JWT_INVALID和类型化meta.reason的结构化错误——其中也包括令牌算法与已配置密钥来源不匹配的情况(使用jwtSecret的客户端遇到 ES256 令牌时,会指出问题并提示切换到jwksUrl)。Postgres 角色根据令牌的role声明确定(默认为authenticated)。注意:即使项目使用 ES256 签名,supabase status仍会打印JWT_SECRET——它存在并不意味着你的项目在使用它。 - 通过
auth.*/storage.*进行管理访问仅在service_role上提供次级根接口——并且需要一次性授权。db.asServiceRole().supabase会公开该包自己的契约(.sql、.orm、.nativeEnums、.execute)。按设计,该根接口仅存在于service_role上,但真实的 Supabase 项目不会向service_role授予auth.*/storage.*的表权限(仅授予 schema 的USAGE;只有postgres持有表授权)。在管理根接口能够读取 Supabase 内部表之前,请先运行一次范围有限的授权(参见工作流 — 授权)。asUser/asAnon没有.supabase,而主要的asServiceRole().sql/.orm仍仅限于你自己的契约。
工作流 — 将 pack 接入配置
概念:pack 会注册 Supabase 合约空间,以便你的合约引用它,同时让规划器/验证器知道 Supabase 管理哪些内容。该扩展目前还没有 /control 子路径,因此无法通过目标门面的 defineConfig({ extensions: [...] }) 接入——它会接入底层配置的 extensions(参见 Prisma Next 目前尚未实现的功能)。下面的底层导入是对仅使用门面导入规则的有意例外,这是由这一缺口导致的;此代码块逐字对应 examples/supabase/prisma-next.config.ts——请直接复制,不要自行组合:
// 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' },
});
工作流 — 合约:指向 auth.users 的外键 + RLS 策略
概念:你的模型位于自己的命名空间(public);Supabase 的模型位于 pack 的命名空间(auth、storage)。类型为 supabase:auth.AuthUser 的关系字段是一个跨空间外键——规划器会生成 REFERENCES "auth"."users"("id"),并验证目标表,但绝不会迁移该表。RLS 策略是与目标模型处于同一命名空间的顶层 policy_<operation> 块,且目标模型必须通过 @@rls 显式启用。参照 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).
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()"
}
}
Uuid 构造器在类型位置选择原生 UUID 存储。旧版 @db.Uuid 写法已移除;生成前,请将所有 String @db.Uuid 别名或字段改写为 Uuid。
各个组成部分:
- 按操作划分的策略块:
policy_select、policy_insert、policy_update、policy_delete、policy_all。内容采用key = value格式:target(此命名空间中的模型)、roles(根据组合后的合约解析——pack 提供anon/authenticated/service_role)、using,以及(对于写操作)withCheck。同一(target, operation)可以有多个宽松策略——Postgres 会对它们进行 OR 运算。 - 策略目标必须包含
@@rls。 如果policy_*块的目标模型缺少@@rls,生成时会报错PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE。包含@@rls且没有任何策略的模型也有意义:RLS 已启用,默认拒绝所有访问。 - 谓词是原样使用的 SQL 字符串。 在其中用引号括住 camelCase 列名(
\"userId\"),并在需要时进行类型转换——auth.uid()返回uuid。合约中的重命名不会改写谓词内容。 - TS 构建器也支持同等功能。
@prisma-next/postgres/contract-builder导出policySelect/policyInsert/policyUpdate/policyDelete/policyAll、rlsEnabled(Model)和role('anon')——与 PSL 的降级映射逐项对应(生成的线上名称完全相同)。此处展示的是 PSL 这一规范用法。
照常生成并迁移(先运行 prisma-next contract emit,然后运行 prisma-next-migrations)。执行计划会创建你的表、外键、ENABLE ROW LEVEL SECURITY 和 CREATE POLICY 语句——不会为 auth.* 生成任何 DDL。
工作流 — 使用 supabase() 工厂的 db.ts
概念:Supabase 应用不是使用默认的 postgres() 工厂,而是通过扩展的 /runtime 子路径中的 supabase() 构建客户端。该工厂是异步的(它会准备 JWT 密钥材料——包括在设置 jwksUrl 时进行的一次性 JWKS 获取),且结果以角色为先。
// 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.
});
除基本选项外,还可设置:middleware(组合方式与 postgres() 相同——参见 prisma-next-runtime;中间件不会看到角色绑定的 set_config 调用)、poolOptions、pg(自行提供 pg.Pool/pg.Client,而不是使用 url)。关闭资源时,可使用 await db.close()/await using,与 prisma-next-runtime 完全相同——适用相同的脚本挂起规则。
工作流 — 绑定角色的查询
概念:绑定应执行请求的角色,然后通过返回的 RoleBoundDb 进行查询——prisma-next-queries 中的所有查询接口都可使用,并由 Postgres 执行 RLS 过滤。
// 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' });
注意:asAnon() / asServiceRole() 是同步的;只有 asUser 是异步的。多命名空间契约通过坐标引用模型(orm.public.Profile、sql.public.profile)——参见 prisma-next-queries § 命名空间感知访问器。RoleBoundDb.transaction(fn) 会在绑定角色的会话上包装事务中的工作。
工作流 — 读取 auth.* / storage.* 的管理操作
概念:Supabase 内部表不属于你的契约,因此不会出现在你的查询接口中。service_role 绑定提供了一个辅助根节点——db.asServiceRole().supabase,这是该 包 的契约接口:
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'];
管理根节点需要一次性授权。 在真实的 Supabase 项目中,service_role 对 auth.* / storage.* 没有表权限——开箱即用时,上述读取会因 permission denied for table users(sqlState 42501)而失败。请仅对实际读取的内容进行精确授权:
GRANT USAGE ON SCHEMA auth TO service_role;
GRANT SELECT ON TABLE auth.users TO service_role;
还需注意其他边界:asUser / asAnon 没有 .supabase;管理根节点没有 .transaction(它是共享连接池的独立契约绑定运行时;跨两个根节点的事务不在范围内);对于用户管理(创建用户、重置密码),优先使用 GoTrue Admin API——Supabase 内部架构可能会随平台升级而变化;直接使用 service_role SQL 仅适用于临时管理读取。
工作流 — 授权
概念:RLS 策略是在常规表权限之上的行过滤器——只有策略而没有 GRANT 的角色会得到 permission denied,而不是经过过滤的行。在 Supabase 上,这两个方向很容易弄反:
- 自有的
public表无需额外设置。 Supabase 在public上配置了ALTER DEFAULT PRIVILEGES,因此由prisma-next db init/migrate创建的表会自动继承anon/authenticated/service_role的完整授权——与通过仪表板创建的表相同。真正保护行的是 RLS 策略;不要添加逐表授权,也不要缩小默认权限,除非确有需要。 - 你确实需要的一项授权,是读取 Supabase 内部表的管理权限——
service_role对auth.*/storage.*没有表权限(较上文的管理读取,了解精简的GRANT USAGE/GRANT SELECT配对设置)。
通过 Supabase SQL 编辑器或 psql 执行授权。缺少授权的表现:出现 permission denied for table …(sqlState 42501),而不是返回空结果。
工作流 — 连接到真实的 Supabase 项目
概念:运行时需要一个支持会话的直接 Postgres 连接——它通过会话范围的 set_config + RESET ALL 绑定角色。
- 会话池化器(
aws-0-<region>.pooler.supabase.com:5432,用户名为postgres.<project-ref>)——适用于所有环境,支持 IPv4。默认选择。 - 直接连接(
db.<project-ref>.supabase.co:5432)——可以使用,但新项目中仅支持 IPv6;在仅支持 IPv4 的环境中会出现 DNS / 连接失败。 - 事务池化器(端口 6543)——请勿使用。 事务池化会破坏会话 GUC;角色绑定将无法正常工作。
.env 包含 DATABASE_URL 和 JWT 密钥来源。对于当前项目,该来源为 SUPABASE_JWKS_URL——https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json(本地堆栈:http://127.0.0.1:54321/auth/v1/.well-known/jwks.json)。只有旧版 HS256 项目使用 SUPABASE_JWT_SECRET(Project Settings → API → JWT Secret)——请注意,即使是 ES256 项目,supabase status 也会打印 JWT_SECRET,因此不要根据它是否存在来推断模式;请检查 JWKS 端点或令牌标头中的 alg。
常见陷阱
- 使用事务池化器(端口 6543)。 会话 GUC 角色绑定需要支持会话的连接——请使用会话池化器(5432)或直接连接。
- 因为
supabase status会打印JWT_SECRET,就配置jwtSecret。 当前项目使用 ES256 签名;此时asUser会抛出SUPABASE.JWT_INVALID,说明令牌采用 ES256,客户端需要jwksUrl。请配置SUPABASE_JWKS_URL;jwtSecret仅用于旧版 HS256 项目。 - 授权方向错误。 你的
public表不需要授权(Supabase 的默认权限已涵盖这些表;RLS 会保护行)——你需要的是为service_role管理员读取配置一对范围狭窄的auth.*授权。permission denied(42501)表示缺少授权,而不是结果被筛选。 - 在顶层
db上寻找db.sql/db.orm。 Supabase 的 db 以角色为先;绑定角色后,再查询RoleBoundDb。 - 忘记使用
await——用于supabase()工厂和asUser(jwt)。两者都是异步的;asAnon()/asServiceRole()则不是。 - 期待
asUser/asAnon上有.supabase。 对auth.*的管理员访问按设计仅限service_role——而且即便是service_role,也必须先进行一次性范围狭窄的授权。 policy_*块的目标缺少@@rls。 Emit 会失败并报错PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE——请将@@rls添加到模型。- 谓词中的 camelCase 列未加引号,或缺少类型转换。 谓词是原样 SQL:
"userId"需要加引号;当列本身不是uuid类型时,用::uuid转换后再与auth.uid()比较。 - 同时传入
jwksUrl和jwtSecret(或两者都不传)——supabase()promise 会以SUPABASE.CONFIG_INVALID拒绝。它是异步工厂,因此配置错误会表现为拒绝(使用await/.catch处理),而不是同步抛出异常。 - 把被 RLS 筛选的写入视为错误。 对角色不可见的行执行
UPDATE会影响 0 行(不会抛出异常);只有违反withCheck才会报错。
Prisma Next 目前尚不支持的功能
- 扩展没有
/control子路径——无法通过目标 façade 的defineConfig({ extensions: [...] })注册;如上所示,需要通过底层配置的extensions进行接线。欢迎通过prisma-next-feedback提交相关需求。 - 编写
GRANT。 表权限不是契约元素;Supabase 应用所需的一项授权(用于管理员读取的service_roleauth.*授权对)需要手动运行一次(通过 SQL 编辑器 /psql)。如果你希望通过契约管理授权,请通过prisma-next-feedback提交相关需求。 - 跨越应用根和
.supabase管理根的事务。 这两个根是共享一个池的独立契约绑定运行时;不支持跨根事务。 - 将触发器 / 函数作为契约元素。 经典的“注册时创建 profile 行”
auth.users触发器需要以原始 SQL 的形式针对数据库编写,而不是写在契约中。auth.uid()等仅会出现在不透明的策略谓词字符串中。 - Supabase Realtime、存储上传、PostgREST /
@supabase/supabase-js互操作、边缘运行时。 这些不在扩展的范围内——扩展直接连接 Postgres(Node.js / Bun)。
参考文件
examples/supabase— 可运行应用的规范示例:配置、契约、db.ts、验收测试、README。packages/3-extensions/supabase/README.md— 包级参考(JWT 模式、角色绑定模型、不支持的范围)。packages/3-extensions/supabase/src/runtime/supabase.ts— 权威选项 / 类型定义(SupabaseOptions、RoleBoundDb、ServiceRoleDb)。
检查清单
- 在低级别的
defineConfig中使用extensions: [supabasePack](不存在/control子路径)。 - 跨空间 FK 类型为
supabase:auth.AuthUser,并显式指定fields/references(如有需要,也指定onDelete)。 - 每个策略目标模型都带有
@@rls;谓词中的 camelCase 列名需加引号,并为auth.uid()转换类型。 -
db.ts使用await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret })——JWT 密钥来源必须且只能有一个;当前项目使用jwksUrl,仅旧版 HS256 使用jwtSecret。 - 查询通过
asUser/asAnon/asServiceRole获取的RoleBoundDb执行;asUser需使用await。 -
auth.*/storage.*读取只能通过asServiceRole().supabase执行,且需先进行一次范围受限的授权(GRANT USAGE ON SCHEMA auth+ 对读取的表执行GRANT SELECT)。 - 不要为自己的
public表添加逐表授权——Supabase 默认权限已涵盖这些表;由 RLS 负责保护。 - 连接必须支持会话:使用会话池化器或直连——绝不能使用 6543 事务池化器。
- 不得臆造
/control子路径、顶层db.sql、非服务角色上的.supabase,或在契约中编写授权。