prisma-next-supabase

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Prisma 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
supabase()
runtime.
编辑你的数据契约,其余工作由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
    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.
  • 用户拥有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
    db.ts
    wiring, middleware, teardown →
    prisma-next-runtime
    .
  • General query shapes (filtering, includes, aggregates) →
    prisma-next-queries
    — everything there applies to a role-bound
    db
    too.
  • 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
    external
    contract space.
    @prisma-next/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.
  • 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.
  • 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.
  • 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.
  • 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 (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.
  • 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.
  • 扩展包是一个
    external
    契约空间
    @prisma-next/extension-supabase/pack
    包含一套完整的、基于 introspection 生成的Supabase托管资源契约——
    auth
    storage
    模式、原生枚举类型,以及平台角色(
    anon
    authenticated
    service_role
    )——所有资源均标记为
    external
    控制策略。通过
    extensions
    组合后,迁移规划器不会为这些对象生成DDL(由Supabase管理),且
    db verify
    验证它们在实时数据库中是否存在。你自己的表仍保持默认的
    managed
    状态。
  • 角色由扩展包提供,无需自行声明。RLS中的
    roles = [authenticated]
    标识符会解析为组合契约中的角色。若运行时指向非Supabase的Postgres数据库,
    verify
    会抛出
    not-found
    错误,提示缺失对应角色——这种常见的「数据库配置错误」会在查询执行前暴露。
  • 运行时以角色为核心
    supabase()
    返回的
    SupabaseDb
    没有顶层查询接口——绑定角色前,不存在
    db.sql
    /
    db.orm
    接口。
    await db.asUser(jwt)
    /
    db.asAnon()
    /
    db.asServiceRole()
    各自返回一个
    RoleBoundDb
    ,提供
    .sql
    .orm
    .raw
    .execute(plan)
    .transaction(fn)
    接口。这是刻意设计的:在Supabase应用中,不存在有意义的「无角色」执行上下文,默认使用连接的登录角色会导致RLS被静默绕过的风险。
  • 角色绑定位于中间件之下,不会泄露。每个角色绑定的查询都会在一个已执行
    set_config('role', …)
    set_config('request.jwt.claims', …)
    的连接上运行,且这些配置会在连接释放时执行
    RESET ALL
    。Postgres端的
    auth.uid()
    /
    auth.jwt()
    会读取这些会话变量——RLS的执行由Postgres负责;运行时的职责是绑定上下文。
  • RLS由策略和权限授予(GRANT)共同强制执行。策略用于过滤
    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
    ——包括令牌算法与配置的密钥源不匹配(例如ES256令牌对应
    jwtSecret
    客户端时,会指出问题并建议切换到
    jwksUrl
    )。Postgres角色由令牌的
    role
    声明派生(默认为
    authenticated
    )。注意:即使在使用ES256签名的项目中,
    supabase status
    仍会输出
    JWT_SECRET
    ,但这并不意味着项目使用该密钥。
  • service_role
    可通过二级根访问
    auth.*
    /
    storage.*
    ——且需要一次性权限授予
    db.asServiceRole().supabase
    提供扩展包自身的契约接口(
    .sql
    .orm
    .nativeEnums
    .execute
    )。该根接口仅在
    service_role
    下存在,但实际Supabase项目未授予
    service_role
    auth.*
    /
    storage.*
    表权限(仅授予模式
    USAGE
    权限;只有
    postgres
    角色拥有表权限)。在管理员根接口能够读取Supabase内部表之前,需先执行一次窄范围的权限授予(详见「工作流——权限授予」)。
    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
/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:
typescript
// 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托管的资源。该扩展目前没有
/control
子路径,因此无法通过目标外观的
defineConfig({ extensions: [...] })
注册——需接入底层配置的
extensions
(详见「Prisma Next暂不支持的功能」)。以下底层导入是刻意打破外观唯一导入规则的例外,由当前功能缺口导致;代码块完全镜像
examples/supabase/prisma-next.config.ts
——建议直接复制而非自行编写:
typescript
// 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
的外键 + RLS策略

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
:
prisma
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
    ,
    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.
  • @@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.
  • 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.
  • TS-builder parity exists.
    @prisma-next/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.
Emit + migrate as usual (
prisma-next contract emit
, then
prisma-next-migrations
). The plan creates your table, its FK,
ENABLE ROW LEVEL SECURITY
, and the
CREATE POLICY
statements — and no DDL for
auth.*
.
核心思路:你的模型位于自己的命名空间(
public
);Supabase的模型位于扩展包的命名空间(
auth
storage
)。类型为
supabase:auth.AuthUser
的关联字段是跨空间外键——规划器会生成
REFERENCES "auth"."users"("id")
,目标表会被验证但不会被迁移。RLS策略是与目标模型同命名空间的顶层
policy_<operation>
块,且目标模型必须通过
@@rls
启用RLS。镜像
examples/supabase/src/contract.prisma
的写法:
prisma
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
    。同一
    (target, operation)
    下可存在多个宽松策略——Postgres会将它们进行逻辑或运算。
  • 策略目标模型必须启用
    @@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)
    role('anon')
    ——与PSL语法完全对应(生成的键名一致)。本文以PSL为标准写法展示。
按常规流程生成并迁移(
prisma-next contract emit
,然后执行
prisma-next-migrations
)。迁移计划会创建你的表、外键、
ENABLE ROW LEVEL SECURITY
CREATE POLICY
语句——不会为
auth.*
生成任何DDL

Workflow —
db.ts
with the
supabase()
factory

工作流——使用
supabase()
工厂构建
db.ts

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.
typescript
// 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:
middleware
(same composition as
postgres()
— see
prisma-next-runtime
; 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
prisma-next-runtime
— the same script-hang rules apply.
核心思路:替代默认的
postgres()
工厂,Supabase应用需使用扩展包
/runtime
子路径中的
supabase()
构建客户端。该工厂是异步的(会准备JWT密钥材料——包括设置
jwksUrl
时的一次性JWKS获取),且返回结果以角色为核心。
typescript
// 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']替代——二选一,不可同时配置
});
基础配置之外的选项:
middleware
(与
postgres()
的组合方式相同——详见
prisma-next-runtime
;中间件不会看到角色绑定的
set_config
调用)、
poolOptions
pg
(自定义
pg.Pool
/
pg.Client
替代
url
)。销毁操作与
prisma-next-runtime
一致:
await db.close()
/
await using
——同样遵循脚本挂起规则。

Workflow — Role-bound queries

工作流——角色绑定查询

The concept: bind the role that should execute the request, then query through the returned
RoleBoundDb
— every query surface from
prisma-next-queries
works, RLS-filtered by Postgres.
typescript
// 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:
asAnon()
/
asServiceRole()
are sync; only
asUser
is async. Multi-namespace contracts address models by coordinate (
orm.public.Profile
,
sql.public.profile
) — see
prisma-next-queries
§ Namespace-aware accessors.
RoleBoundDb.transaction(fn)
wraps work in a transaction on the role-bound session.
核心思路:绑定执行请求的角色,然后通过返回的
RoleBoundDb
执行查询——
prisma-next-queries
中的所有查询接口均适用,结果由Postgres进行RLS过滤。
typescript
// 已登录用户:行数据受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()
是同步操作;仅
asUser
是异步操作。多命名空间契约通过坐标访问模型(
orm.public.Profile
sql.public.profile
)——详见
prisma-next-queries
中的「命名空间感知访问器」章节。
RoleBoundDb.transaction(fn)
会将操作包裹在角色绑定会话的事务中。

Workflow — Admin reads of
auth.*
/
storage.*

工作流——管理员读取
auth.*
/
storage.*

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:
typescript
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
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:
sql
GRANT USAGE ON SCHEMA auth TO service_role;
GRANT SELECT ON TABLE auth.users TO service_role;
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.
核心思路:Supabase内部表不属于你的契约,因此不会出现在你的查询接口中。
service_role
绑定提供二级根接口——
db.asServiceRole().supabase
,即扩展包的契约接口:
typescript
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项目未授予
service_role
auth.*
/
storage.*
的表权限——默认情况下,上述读取操作会抛出
permission denied for table users
错误(sqlState
42501
)。请仅授予实际需要读取的权限,保持范围尽可能窄:
sql
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仅适用于临时管理员读取操作。

Workflow — Grants

工作流——权限授予

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:
  • Your own
    public
    tables need nothing.
    Supabase ships
    ALTER DEFAULT PRIVILEGES
    on
    public
    , so tables created by
    prisma-next db init
    /
    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.
  • 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).
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.
核心思路: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
),而非返回空结果。

Workflow — 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_config
+
RESET ALL
.
  • Session pooler (
    aws-0-<region>.pooler.supabase.com:5432
    , username
    postgres.<project-ref>
    ) — works everywhere, IPv4. The default choice.
  • Direct connection (
    db.<project-ref>.supabase.co:5432
    ) — works, but is IPv6-only on new projects; from IPv4-only environments it fails DNS/connect.
  • Transaction pooler (port 6543) — do not use. Transaction pooling breaks session GUCs; role binding will misbehave.
.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
.
核心思路:运行时需要支持会话的直接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
(项目设置→API→JWT密钥)——注意即使在ES256项目中,
supabase status
仍会输出
JWT_SECRET
,因此不要根据其存在推断签名模式;请检查JWKS端点或令牌头部的
alg
字段。

Common Pitfalls

常见陷阱

  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.
  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.
  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.
  4. Expecting
    db.sql
    /
    db.orm
    on the top-level
    db
    .
    The Supabase db is role-first; bind a role, query the
    RoleBoundDb
    .
  5. Forgetting
    await
    — on the
    supabase()
    factory and on
    asUser(jwt)
    . Both are async;
    asAnon()
    /
    asServiceRole()
    are not.
  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.
  7. A
    policy_*
    block whose target lacks
    @@rls
    .
    Emit fails with
    PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE
    — add
    @@rls
    to the model.
  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
    .
  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.
  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.
  1. 使用事务池(端口6543)。会话GUC角色绑定需要支持会话的连接——请使用会话池(5432)或直接连接。
  2. supabase status
    输出
    JWT_SECRET
    而配置
    jwtSecret
    。当前项目使用ES256签名;此时
    asUser
    会抛出
    SUPABASE.JWT_INVALID
    错误,提示令牌为ES256类型,客户端需使用
    jwksUrl
    。请配置
    SUPABASE_JWKS_URL
    ;仅旧版HS256项目使用
    jwtSecret
  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()
    是同步操作,无需
    await
  6. 期望
    asUser
    /
    asAnon
    拥有
    .supabase
    接口
    。按设计,仅
    service_role
    可访问
    auth.*
    的管理员接口——且
    service_role
    需先完成一次性窄范围权限授予。
  7. policy_*
    块的目标模型未配置
    @@rls
    。生成过程会抛出
    PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE
    错误——需为模型添加
    @@rls
  8. 谓词中未对驼峰式列名添加引号或缺失类型转换。谓词是原始SQL:
    "userId"
    需要引号;若列不是
    uuid
    类型,需添加
    ::uuid
    转换以与
    auth.uid()
    比较。
  9. 同时配置
    jwksUrl
    jwtSecret
    (或均未配置)
    ——
    supabase()
    承诺会抛出
    SUPABASE.CONFIG_INVALID
    错误。它是异步工厂,因此配置错误会以拒绝(
    await
    /
    .catch
    )的形式暴露,而非同步抛出。
  10. 将RLS过滤的写入操作视为错误。更新角色无权访问的行时,影响行数为0(无异常);仅违反
    withCheck
    规则时才会抛出错误。

What Prisma Next doesn't do yet

Prisma Next暂不支持的功能

  • 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
    prisma-next-feedback
    .
  • 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
    prisma-next-feedback
    .
  • 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.
  • 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.
  • Supabase Realtime, storage uploads, PostgREST /
    @supabase/supabase-js
    interop, edge runtimes.
    Out of scope for the extension — it speaks Postgres directly (Node.js / Bun).
  • 扩展包无
    /control
    子路径
    ——无法通过目标外观的
    defineConfig({ extensions: [...] })
    注册;需按上述方式接入底层配置的
    extensions
    。可通过
    prisma-next-feedback
    提交需求。
  • 权限授予(GRANT)管理。表权限不属于契约元素;Supabase应用唯一需要的权限授予(
    service_role
    读取
    auth.*
    的组合)需手动执行一次(SQL编辑器/
    psql
    )。若希望通过契约管理权限授予,可通过
    prisma-next-feedback
    提交需求。
  • 跨应用根和
    .supabase
    管理员根的事务
    。两个根接口是共享连接池的独立契约绑定运行时;跨根接口的事务不受支持。
  • 触发器/函数作为契约元素。经典的「注册时创建个人资料行」
    auth.users
    触发器需通过原始SQL编写,而非在契约中定义。
    auth.uid()
    等仅出现在策略谓词的原始字符串中。
  • Supabase Realtime、存储上传、PostgREST/
    @supabase/supabase-js
    互操作、边缘运行时
    。这些不在本扩展的范围内——它仅直接与Postgres通信(Node.js/Bun环境)。

Reference Files

参考文件

  • examples/supabase
    — the canonical runnable app: config, contract,
    db.ts
    , acceptance tests, README.
  • packages/3-extensions/supabase/README.md
    — package-level reference (JWT modes, role-binding model, unsupported scope).
  • packages/3-extensions/supabase/src/runtime/supabase.ts
    — the authoritative options/type surface (
    SupabaseOptions
    ,
    RoleBoundDb
    ,
    ServiceRoleDb
    ).
  • examples/supabase
    ——标准可运行应用:配置、契约、
    db.ts
    、验收测试、README。
  • packages/3-extensions/supabase/README.md
    ——包级参考文档(JWT模式、角色绑定模型、不支持的范围)。
  • packages/3-extensions/supabase/src/runtime/supabase.ts
    ——权威选项/类型接口(
    SupabaseOptions
    RoleBoundDb
    ServiceRoleDb
    )。

Checklist

检查清单

  • extensions: [supabasePack]
    in the low-level
    defineConfig
    (no
    /control
    subpath exists).
  • Cross-space FK typed
    supabase:auth.AuthUser
    with explicit
    fields
    /
    references
    (+
    onDelete
    if wanted).
  • Every policy target model carries
    @@rls
    ; predicates quote camelCase columns and cast for
    auth.uid()
    .
  • db.ts
    uses
    await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret })
    — exactly one JWT key source;
    jwksUrl
    for current projects,
    jwtSecret
    only for legacy HS256.
  • Queries go through a
    RoleBoundDb
    from
    asUser
    /
    asAnon
    /
    asServiceRole
    ;
    asUser
    is awaited.
  • 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).
  • No per-table grants added for your own
    public
    tables — Supabase default privileges cover them; RLS does the protecting.
  • Connection is session-capable: session pooler or direct connection — never the 6543 transaction pooler.
  • Did NOT confabulate a
    /control
    subpath, a top-level
    db.sql
    ,
    .supabase
    on non-service roles, or grant authoring in the contract.
  • 在底层
    defineConfig
    中配置
    extensions: [supabasePack]
    (目前无
    /control
    子路径)。
  • 配置类型为
    supabase:auth.AuthUser
    的跨空间外键,指定
    fields
    /
    references
    (可选配置
    onDelete
    )。
  • 所有策略目标模型均配置
    @@rls
    ;谓词对驼峰式列名添加引号,并针对
    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
    接口,或在契约中管理权限授予。