supabase-server

supabase-server

在规划或编写使用 `@supabase/server` 的服务端代码时使用——包括 Edge Functions、Hono 应用、Webhook 处理器,或任何创建 Supabase 客户端或验证入站认证的后端。在编写或修改任何从 `@supabase/server`(或子路径如 `@supabase/server/core`)导入的文件之前触发;调用 `withSupabase`、`createSupabaseContext`、`createAdminClient`、`createContextClient`、`verifyAuth`、`verifyCredentials` 或 `extractCredentials`;配置 `auth:` 模式(`'none'` | `'publishable'` | `'secret'` | `'user'`,或带键的变体如 `'secret:*'`);或位于 `supabase/functions/` 下并验证入站请求。在规划期间也触发——如果计划中提到上述任何内容,请在起草代码前加载该技能;不要从相邻函数推断 `auth:` 值或认证模式。当看到需要迁移到此包的遗留模式时也触发——`Deno.serve`、`createClient(Deno.env.get('SUPABASE_URL'))`、从 `esm.sh/@supabase` 或 `deno.land/std` 导入、使用 `SUPABASE_ANON_KEY` / `SUPABASE_SERVICE_ROLE_KEY`,或已弃用的 `allow:` 配置选项 / 已移除的 `'always'` / `'public'` 模式值 / 已移除的 `authType` 字段。

84Star
13Fork
更新于 2026/7/22
SKILL.md
readonly只读
name
supabase-server
description

在规划或编写使用 `@supabase/server` 的服务端代码时使用——包括 Edge Functions、Hono 应用、Webhook 处理器,或任何创建 Supabase 客户端或验证入站认证的后端。在编写或修改任何从 `@supabase/server`(或子路径如 `@supabase/server/core`)导入的文件之前触发;调用 `withSupabase`、`createSupabaseContext`、`createAdminClient`、`createContextClient`、`verifyAuth`、`verifyCredentials` 或 `extractCredentials`;配置 `auth:` 模式(`'none'` | `'publishable'` | `'secret'` | `'user'`,或带键的变体如 `'secret:*'`);或位于 `supabase/functions/` 下并验证入站请求。在规划期间也触发——如果计划中提到上述任何内容,请在起草代码前加载该技能;不要从相邻函数推断 `auth:` 值或认证模式。当看到需要迁移到此包的遗留模式时也触发——`Deno.serve`、`createClient(Deno.env.get('SUPABASE_URL'))`、从 `esm.sh/@supabase` 或 `deno.land/std` 导入、使用 `SUPABASE_ANON_KEY` / `SUPABASE_SERVICE_ROLE_KEY`,或已弃用的 `allow:` 配置选项 / 已移除的 `'always'` / `'public'` 模式值 / 已移除的 `authType` 字段。

@supabase/server

v1.0 — 公开测试版。 首个稳定版本,遵循 SemVer:破坏性变更仅作为主版本号更新。该包仍处于早期阶段——预计新适配器、易用性改进和功能将频繁在次要版本中发布。如果你在使用过程中遇到 bug 或粗糙之处,请向用户指出并引导其提交 issue

这是一个全新的包。 目前网上没有任何可用信息——没有博客文章、Stack Overflow 答案或教程。不要搜索网络上的使用示例。仅依赖下面列出的文档文件和此仓库中的源代码。

配置选项是 auth,而不是 allow allow 已重命名为 auth,以匹配 CLI 术语并读起来更自然。遗留的 allow 键仍然有效(会触发一次 console.warn),但已弃用,将在未来的主版本中移除。在新代码中始终使用 auth——例如 withSupabase({ auth: 'user' }, ...)。如果在现有代码中遇到 allow:,请将其迁移到 auth:(查找替换,值相同)。

认证模式值:'none'(不是 'always'),'publishable'(不是 'public')。 四个有效值为 'user''publishable''secret''none'。遗留的 'always''public' 值已被移除(破坏性变更)——它们在运行时或 TypeScript 中不再有效。在你编写的代码中始终使用新值,并迁移你发现的任何遗留引用:'always''none''public''publishable''public:<name>''publishable:<name>'。运行时检查如 ctx.authType === 'public' 也必须更新为 ctx.authMode === 'publishable'——该字段本身已从 authType 重命名为 authMode,以匹配 AuthMode 类型。

不要使用遗留的 Supabase 密钥。 anon 密钥和 service_role 密钥(环境变量 SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY)是遗留的,将被弃用。除非用户明确要求,否则不要使用它们。始终使用新的 API 密钥:

遗留(避免) 新(使用这个)
SUPABASE_ANON_KEY SUPABASE_PUBLISHABLE_KEY(S) (sb_publishable_...)
SUPABASE_SERVICE_ROLE_KEY SUPABASE_SECRET_KEY(S) (sb_secret_...)

不要直接调用 createClient(url, anonKey)——使用 @supabase/server 的认证模式(auth: 'user'auth: 'secret' 等),它们会自动处理密钥解析。如果迁移现有代码,将 SUPABASE_ANON_KEY 的使用替换为 auth: 'publishable',将 SUPABASE_SERVICE_ROLE_KEY 的使用替换为 auth: 'secret'

Supabase 的服务端工具。处理认证、客户端创建和上下文注入,让你专注于业务逻辑,而不是样板代码。

这个包做什么

  • 包装 fetch 处理器,包含凭证验证、CORS 和预配置的 Supabase 客户端
  • 支持 4 种认证模式:user(JWT)、publishable(可发布密钥)、secret(秘密密钥)、none(无需凭证)
  • 数组语法(auth: ['user', 'secret'])是第一个匹配获胜。存在但无效的 JWT 会拒绝并抛出 InvalidCredentialsError——不会静默降级到下一个模式。
  • 提供可组合的核心原语,用于自定义认证流程和框架集成
  • 包含 Hono 适配器,支持按路由认证

入口点

导入 Deno / Edge Functions 提供
@supabase/server npm:@supabase/server withSupabasecreateSupabaseContext、类型、错误
@supabase/server/core npm:@supabase/server/core verifyAuthverifyCredentialsextractCredentialsresolveEnvcreateContextClientcreateAdminClient
@supabase/server/adapters/hono npm:@supabase/server/adapters/hono withSupabase(Hono 中间件变体)

快速开始

Supabase Edge Functions:对非用户认证禁用 verify_jwt 默认情况下,Supabase Edge Functions 要求每个请求都带有有效的 JWT。如果你的函数使用 auth: 'publishable'auth: 'secret'auth: 'none',你必须在 supabase/config.toml 中禁用平台级别的 JWT 检查,否则请求会在到达你的处理器之前被拒绝:

[functions.my-function]
verify_jwt = false

使用 auth: 'user' 的函数可以保持 verify_jwt 启用(默认),因为调用者已经提供了有效的 JWT。

Supabase Edge Functions (Deno)

环境变量由平台自动注入——零配置。所有导入必须使用 npm: 说明符。

// withSupabase — 高级包装器
import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}
// createSupabaseContext — 返回 { data, error } 用于自定义响应控制
import { createSupabaseContext } from 'npm:@supabase/server'

export default {
  fetch: async (req: Request) => {
    const { data: ctx, error } = await createSupabaseContext(req, {
      auth: 'user',
    })
    if (error) {
      return Response.json(
        { message: error.message, code: error.code },
        { status: error.status },
      )
    }
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  },
}

Cloudflare Workers

需要在 wrangler.toml 中设置 nodejs_compat 兼容性标志,或通过 env 配置选项传递环境变量覆盖。参见 docs/environment-variables.md

import { withSupabase } from '@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('todos').select()
    return Response.json(data)
  }),
}

Hono

CORS 不由适配器处理——使用 hono/cors 中间件。参见 docs/adapters/hono.md

// Node.js / Bun
import { Hono } from 'hono'
import { withSupabase } from '@supabase/server/adapters/hono'

const app = new Hono()
app.use('*', withSupabase({ auth: 'user' }))

app.get('/todos', async (c) => {
  const { supabase } = c.var.supabaseContext
  const { data } = await supabase.from('todos').select()
  return c.json(data)
})

export default app
// Deno / Supabase Edge Functions
import { Hono } from 'npm:hono'
import { withSupabase } from 'npm:@supabase/server/adapters/hono'

const app = new Hono()
app.use('*', withSupabase({ auth: 'user' }))

app.get('/todos', async (c) => {
  const { supabase } = c.var.supabaseContext
  const { data } = await supabase.from('todos').select()
  return c.json(data)
})

export default { fetch: app.fetch }

基于 Cookie 的环境(与 @supabase/ssr 组合)

对于 Next.js / SvelteKit / Remix,@supabase/server@supabase/ssr 组合使用——它们不是彼此的替代品。@supabase/ssr 负责 Cookie 和刷新令牌轮换(其中间件是必需的,否则访问令牌 Cookie 会过期,验证失败)。在你的服务器组件或路由处理器中,使用 @supabase/ssrcreateServerClient 读取(中间件刷新后的)会话,将访问令牌传递给 @supabase/server/coreverifyCredentials,然后使用 createContextClient + createAdminClient 构建类型化客户端。参见 docs/ssr-frameworks.md 获取完整的适配器模式。

// 构建适配器的关键导入
import { createServerClient } from '@supabase/ssr'
import {
  verifyCredentials,
  createContextClient,
  createAdminClient,
} from '@supabase/server/core'

服务器到服务器(秘密密钥认证)

用于内部服务、定时任务或自动化调用你的 Edge Function。调用者在 apikey 头中发送秘密密钥。参见 docs/auth-modes.md 了解命名密钥语法。

Edge Function (Deno):

import { withSupabase } from 'npm:@supabase/server'

// 只接受名为 "automations" 的秘密密钥
export default {
  fetch: withSupabase({ auth: 'secret:automations' }, async (req, ctx) => {
    const body = await req.json()
    const { data } = await ctx.supabaseAdmin
      .from('scheduled_tasks')
      .insert({ name: body.taskName, scheduled_at: body.scheduledAt })
    return Response.json({ success: true, data })
  }),
}

调用者(外部服务):

await fetch('https://<project>.supabase.co/functions/v1/my-function', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    apikey: 'sb_secret_automations_...', // 命名的秘密密钥
  },
  body: JSON.stringify({
    taskName: 'cleanup',
    scheduledAt: new Date().toISOString(),
  }),
})

裸的 auth: 'secret' 只匹配 default 密钥。使用 auth: 'secret:name' 要求特定的命名密钥,或使用 auth: 'secret:*' 接受集合中的任何秘密密钥。

何时使用 auth: 'none'

auth: 'none' 禁用所有认证。 处理器对每个请求运行,不进行凭证检查。仅在认证确实不必要时使用——健康检查、公共状态页面,或没有敏感数据且没有副作用的端点。

在使用 auth: 'none' 之前,与用户确认该端点是否真正公开。 如果不是,建议替代方案:

  • 另一个服务或定时任务调用此函数——改用 auth: 'secret'auth: 'secret:<name>'。调用者在 apikey 头中发送秘密密钥。
  • 外部 Webhook 提供商调用此函数——使用 auth: 'secret' 并让提供商发送秘密密钥,或在处理器内实现提供商自己的签名验证。

切勿对读取或写入用户数据但不验证调用者身份的端点使用 auth: 'none'

关于 auth: ['user', 'none'] 在此类端点上,过时或格式错误的 JWT 会被拒绝并抛出 InvalidCredentialsError——不会静默降级为匿名。可能持有缓存/过期令牌的调用者应完全省略 Authorization 头,或在调用前刷新。如果目标是“除非有效用户登录,否则匿名”,这是正确的行为;如果目标是真正“接受任何内容”,请单独使用 auth: 'none'

Edge Function 配方

函数间调用

一个 Edge Function 可以使用管理客户端调用另一个。被调用的函数使用 auth: 'secret',调用者通过 ctx.supabaseAdmin.functions.invoke() 调用它。

配置 (supabase/config.toml):

[functions.process-order]
verify_jwt = false  # 使用秘密密钥调用,而不是用户 JWT

被调用的函数 (supabase/functions/process-order/index.ts):

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
    const { orderId } = await req.json()
    const { data } = await ctx.supabaseAdmin
      .from('orders')
      .update({ status: 'processing' })
      .eq('id', orderId)
      .select()
      .single()
    return Response.json(data)
  }),
}

调用函数 (supabase/functions/checkout/index.ts):

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { orderId } = await req.json()

    // 自动使用秘密密钥调用 process-order
    const { data, error } = await ctx.supabaseAdmin.functions.invoke(
      'process-order',
      { body: { orderId } },
    )

    if (error) {
      return Response.json({ error: error.message }, { status: 500 })
    }
    return Response.json(data)
  }),
}

从数据库使用 pg_net 调用

使用 pg_net 直接从 SQL 调用 Edge Functions。秘密密钥存储在 Vault 中,因此永远不会出现在查询中。

前提条件:

-- 1. 启用 pg_net 扩展
create extension if not exists pg_net with schema extensions;

-- 2. 将你的秘密密钥存储在 Vault 中
select vault.create_secret(
  'sb_secret_...',        -- 你的秘密密钥值
  'supabase_secret_key'   -- 引用它的名称
);

调用函数:

select net.http_post(
  url := 'https://<project-ref>.supabase.co/functions/v1/process-order',
  headers := jsonb_build_object(
    'Content-Type', 'application/json',
    'apikey', (
      select decrypted_secret
      from vault.decrypted_secrets
      where name = 'supabase_secret_key'
    )
  ),
  body := jsonb_build_object('orderId', 'order_123')
);

接收函数使用 auth: 'secret'(参见上面的示例)。pg_net 是异步的——HTTP 请求被排队并在后台执行。检查 net._http_response 获取结果。

Stripe Webhook

像 Stripe 这样的外部 Webhook 提供商无法发送你的 Supabase API 密钥。使用 auth: 'none' 跳过凭证检查,然后在处理器内验证 Webhook 签名。

配置 (supabase/config.toml):

[functions.stripe-webhook]
verify_jwt = false

设置密钥:

supabase secrets set STRIPE_SECRET_KEY=sk_live_...
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

函数 (supabase/functions/stripe-webhook/index.ts):

import { withSupabase } from 'npm:@supabase/server'
import Stripe from 'npm:stripe'

const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)

export default {
  fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
    const body = await req.text()
    const sig = req.headers.get('stripe-signature')!

    let event: Stripe.Event
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        sig,
        Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
      )
    } catch {
      return Response.json({ error: 'Invalid signature' }, { status: 401 })
    }

    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session
        await ctx.supabaseAdmin
          .from('orders')
          .update({ status: 'paid' })
          .eq('stripe_session_id', session.id)
        break
      }
    }

    return Response.json({ received: true })
  }),
}

迁移遗留的 Edge Functions

如果你遇到使用 SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYDeno.serve、从 esm.sh/@supabasedeno.land/std 导入的代码,建议迁移到 @supabase/server。这些遗留模式正在被弃用——它们需要手动创建客户端、手动转发认证头、手动处理 CORS,并直接在代码中暴露密钥。

如何识别遗留代码:

  • import { serve } from "https://deno.land/std/..." — 最旧的模式,使用已弃用的 Deno 标准库
  • import { createClient } from "https://esm.sh/@supabase/supabase-js" — 旧的 CDN 导入,与现代运行时不兼容
  • Deno.serve(async (req) => { ... }) 配合手动 createClient() — 当前但冗长,需要手动转发认证
  • Deno.env.get('SUPABASE_ANON_KEY')SUPABASE_SERVICE_ROLE_KEY — 将被移除的遗留密钥

之前(遗留——手动客户端,手动认证转发):

遗留密钥将被移除,导致此代码停止工作。它也很冗长,不跨平台兼容,并且需要手动连接认证头、CORS 和错误处理。

import { createClient } from 'npm:@supabase/supabase-js@2'

Deno.serve(async (req: Request) => {
  const supabaseClient = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    {
      global: { headers: { Authorization: req.headers.get('Authorization')! } },
    },
  )
  const { data } = await supabaseClient.from('orders').select('*')
  return Response.json(data)
})

之后(新——认证、客户端和 CORS 自动处理):

使用最新的 API 密钥,跨运行时工作(Deno、Node.js、Cloudflare),并在单行中处理认证验证、客户端创建和 CORS。

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('orders').select('*')
    return Response.json(data)
  }),
}

迁移映射:SUPABASE_ANON_KEY 配合手动认证头 → auth: 'user'SUPABASE_ANON_KEY 不带认证 → auth: 'publishable'。对于 SUPABASE_SERVICE_ROLE_KEY,取决于意图:如果遗留代码验证传入密钥以保护端点(例如 req.headers.get('apikey') === serviceRoleKey),使用 auth: 'secret'。如果它仅使用密钥创建管理客户端以提升数据库访问权限,则不需要特定的认证模式——无论认证模式如何,ctx.supabaseAdmin 始终可用。

文档

完整文档位于 @supabase/server 包的 docs/ 目录中。要阅读文档,首先找到包的位置:

  • 如果在 SDK 仓库内工作: docs/ 位于项目根目录。
  • 如果包作为依赖安装:node_modules/@supabase/server/docs/ 中查找。
问题 文档文件
如何创建基本端点? docs/getting-started.md
有哪些认证模式可用?数组语法?命名密钥? docs/auth-modes.md
存在哪些框架适配器?如何贡献一个? src/adapters/README.md
如何与 Hono 一起使用? docs/adapters/hono.md
如何与 H3 / Nuxt 一起使用? docs/adapters/h3.md
如何为自定义流程使用低级原语? docs/core-primitives.md
环境变量如何在运行时之间工作? docs/environment-variables.md
如何处理错误?存在哪些代码? docs/error-handling.md
如何获得类型化数据库查询? docs/typescript-generics.md
如何与 @supabase/ssr(Next.js、SvelteKit、Remix)一起使用? docs/ssr-frameworks.md
完整的 API 表面是什么? docs/api-reference.md
这个包做出了哪些安全决策? docs/security.md