mcpField
P
prisma-next-supabase

prisma-next-supabase

github.com/prisma数据与数据库411678 次安装9 周前更新
代码仓库

关于 prisma-next-supabase

面向将 Prisma Next 接入 Supabase 的开发者,涵盖契约配置、RLS 策略和按角色执行查询。其重点是组合 Supabase 外部契约,并在查询前绑定并验证用户角色与 JWT。

主要功能

  1. 01组合 Supabase 外部契约
  2. 02定义 RLS 策略
  3. 03关联 auth.users 外键
  4. 04按角色绑定查询
  5. 05验证用户 JWT
  6. 06读取 Supabase 内部表

适用场景

  1. 01为自有表配置 RLS
  2. 02将模型关联到 auth.users
  3. 03以用户或匿名角色查询
  4. 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 和 storage schema、它们的原生枚举类型,以及平台角色(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。

常见陷阱

  1. 使用事务池化器(端口 6543)。 会话 GUC 角色绑定需要支持会话的连接——请使用会话池化器(5432)或直接连接。
  2. 因为 supabase status 会打印 JWT_SECRET,就配置 jwtSecret。 当前项目使用 ES256 签名;此时 asUser 会抛出 SUPABASE.JWT_INVALID,说明令牌采用 ES256,客户端需要 jwksUrl。请配置 SUPABASE_JWKS_URL;jwtSecret 仅用于旧版 HS256 项目。
  3. 授权方向错误。 你的 public 表不需要授权(Supabase 的默认权限已涵盖这些表;RLS 会保护行)——你需要的是为 service_role 管理员读取配置一对范围狭窄的 auth.* 授权。permission denied(42501)表示缺少授权,而不是结果被筛选。
  4. 在顶层 db 上寻找 db.sql / db.orm。 Supabase 的 db 以角色为先;绑定角色后,再查询 RoleBoundDb。
  5. 忘记使用 await——用于 supabase() 工厂和 asUser(jwt)。两者都是异步的;asAnon() / asServiceRole() 则不是。
  6. 期待 asUser / asAnon 上有 .supabase。 对 auth.* 的管理员访问按设计仅限 service_role——而且即便是 service_role,也必须先进行一次性范围狭窄的授权。
  7. policy_* 块的目标缺少 @@rls。 Emit 会失败并报错 PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE——请将 @@rls 添加到模型。
  8. 谓词中的 camelCase 列未加引号,或缺少类型转换。 谓词是原样 SQL:"userId" 需要加引号;当列本身不是 uuid 类型时,用 ::uuid 转换后再与 auth.uid() 比较。
  9. 同时传入 jwksUrl 和 jwtSecret(或两者都不传)——supabase() promise 会以 SUPABASE.CONFIG_INVALID 拒绝。它是异步工厂,因此配置错误会表现为拒绝(使用 await / .catch 处理),而不是同步抛出异常。
  10. 把被 RLS 筛选的写入视为错误。 对角色不可见的行执行 UPDATE 会影响 0 行(不会抛出异常);只有违反 withCheck 才会报错。

Prisma Next 目前尚不支持的功能

  • 扩展没有 /control 子路径——无法通过目标 façade 的 defineConfig({ extensions: [...] }) 注册;如上所示,需要通过底层配置的 extensions 进行接线。欢迎通过 prisma-next-feedback 提交相关需求。
  • 编写 GRANT。 表权限不是契约元素;Supabase 应用所需的一项授权(用于管理员读取的 service_role auth.* 授权对)需要手动运行一次(通过 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,或在契约中编写授权。

评论

支持 Markdown