迁移到可发布和私密的 API 密钥
API 密钥的更改
Supabase 改变了密钥的工作方式,以提升项目安全性和开发者体验。你可以在 GitHub 上阅读完整公告。
🌐 Supabase has changed the way keys work to improve project security and developer experience. You can read the full announcement on GitHub.
它们将在2026年底被弃用,你现在应该改用可发布的(sb_publishable_xxx)和秘密的(sb_secret_xxx)密钥。
在大多数情况下,你可以从项目的 连接 对话框获得密钥,但如果你想要特定的密钥,可以在仪表板的设置 > API 密钥部分找到它们。
🌐 In most cases, you can get keys from the Project's Connect dialog, but if you want a specific key, you can find them in the Settings > API Keys section of the Dashboard.
- 对于新密钥,打开 API 密钥 标签,如果你还没有可发布的密钥,点击 创建新 API 密钥,然后从 可发布密钥 部分复制数值用于客户端操作。对于服务器端操作,则从 密钥 部分复制数值。
- 对于旧版密钥,从 Legacy API Keys 标签中复制
anon密钥用于客户端操作,service_role密钥用于服务器端操作。
本指南涵盖迁移已有项目。两种密钥类型可以同时使用,所以你可以一次更换一个客户端,只有在没有任何依赖旧密钥时才停用它们。
🌐 This guide covers migrating an existing project. Both key types work simultaneously, so you can swap clients one at a time and deactivate the legacy keys only after nothing depends on them.
在你开始之前 #
🌐 Before you start
这个迁移对应你现有的按键:
🌐 The migration maps onto your existing keys:
| 旧密钥 | 替换为 | 使用者 |
|---|---|---|
anon | 可发布密钥 | 浏览器、移动和桌面应用、命令行工具、公开源代码 |
service_role | 秘密密钥 | 服务器、Edge 函数、工作器、其他后端代码 |
想要了解每种密钥类型的详细说明,请阅读理解 API 密钥指南。
🌐 For a full explanation of each key type, read the Understanding API keys guide.
步骤1:创建新的API密钥 #
🌐 Step 1: Create the new API keys
打开仪表板的 设置 > API 密钥 部分,然后选择 可发布和秘密 API 密钥 标签。
🌐 Open the Settings > API Keys section of the Dashboard and select the Publishable and secret API keys tab.
旧项目还没有这些密钥。如果你看到一个 创建新 API 密钥 按钮,说明你的项目仍然只使用旧密钥。创建新密钥是安全的。它会在你现有的 anon 和 service_role 密钥旁边增加一个可发布密钥和一个秘密密钥。你的旧密钥仍然可以继续使用。
🌐 Older projects don't have these keys yet. If you see a Create new API keys button, your project is still on legacy keys only. Creating the new keys is safe. It adds a publishable key and a secret key alongside your existing anon and service_role keys. Your legacy keys keep working.
新密钥以 default 名义创建。你以后可以添加更多不同名称的密钥,例如,每个后端组件一个秘密密钥,这样你可以独立轮换它们。对于初始迁移来说,default 的可发布和秘密密钥就足够了。
🌐 The new keys are created under the name default. You can add more keys with different names later, for example, one secret key per backend component, so you can rotate them independently. For an initial migration, the default publishable and secret keys are all you need.
步骤 2:在客户端代码中替换可发布密钥 #
🌐 Step 2: Swap the publishable key in client code
在公共代码中使用 anon 密钥的任何地方,都要换成可发布的密钥。这包括网页、移动和桌面应用,以及任何向用户发布的命令行工具或脚本。
🌐 Anywhere you use the anon key in public code, switch to the publishable key. This includes web pages, mobile and desktop apps, and any CLI or script that ships to users.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 'https://your-project.supabase.co',5 'sb_publishable_...' // was the anon key6)可发布密钥的权限和 anon 密钥一样低,所以你的 行级安全 策略行为不变。通过 Supabase Auth 进行的用户认证也没有变化。用户仍然可以登录并获得自己的 JWT。
🌐 The publishable key carries the same low privileges as the anon key, so your Row Level Security policies behave the same. User authentication through Supabase Auth is unchanged. The user still signs in and gets their own JWT.
步骤3:在后端代码中替换密钥 #
🌐 Step 3: Swap the secret key in backend code
无论你在自己控制的服务器上哪里使用 service_role 密钥,都换成一个秘密密钥。
🌐 Anywhere you use the service_role key on a server you control, switch to a secret key.
1import { createClient } from '@supabase/supabase-js'23const supabaseAdmin = createClient(4 'https://your-project.supabase.co',5 'sb_secret_...' // was the service_role key6)秘密密钥提供了 service_role 密钥没有的保护。如果在浏览器中使用(根据 User-Agent 头匹配),它们会返回 HTTP 401,而且你可以为每个服务运行单独的密钥,这样一次泄露只需要轮换一次。
🌐 Secret keys add protections the service_role key doesn't have. They return HTTP 401 if used in a browser (matched on the User-Agent header), and you can run a separate key per service so a single leak only forces one rotation.
秘密密钥可以绕过行级安全性,完全访问你的数据。把它们放在你能控制的后台,不要放在源码管理里,也不要放在客户端代码里。
🌐 Secret keys bypass Row Level Security and have full access to your data. Keep them on backends you control, out of source control, and out of client code.
数据库 Webhooks 和 pg_net#
🌐 Database Webhooks and pg_net
从 Postgres 使用 pg_net 发起的调用,包括数据库 Webhooks,通常会在 Authorization: Bearer 头上发送 service_role 密钥。新的密钥不是 JWT,因此会被拒绝。请改为在 apikey 头上发送密钥。
🌐 Calls made from Postgres with pg_net, including Database Webhooks, usually send the service_role key on the Authorization: Bearer header. The new secret keys aren't JWTs, so they're rejected there. Send the secret key on the apikey header instead.
1-- before2select net.http_post(3 url := 'https://your-project.supabase.co/functions/v1/your-function',4 headers := jsonb_build_object(5 'Content-Type', 'application/json',6 'Authorization', 'Bearer <service_role key>'7 ),8 body := jsonb_build_object('event', 'ping')9);1011-- after12select net.http_post(13 url := 'https://your-project.supabase.co/functions/v1/your-function',14 headers := jsonb_build_object(15 'Content-Type', 'application/json',16 'apikey', 'sb_secret_...'17 ),18 body := jsonb_build_object('event', 'ping')19);对于在仪表板中创建的数据库 Webhooks,编辑每个 Webhook 的 HTTP 头:移除存放密钥的 Authorization 头,并改为添加带有密钥的 apikey 头。
🌐 For Database Webhooks created in the Dashboard, edit each webhook's HTTP headers: remove the Authorization header that holds the key and add an apikey header with a secret key instead.
不要在 SQL 或 webhook 配置中硬编码密钥,这样会以明文存储。把它存储在 Vault 中,并在调用时读取:
🌐 Don't hardcode a secret key in SQL or a webhook configuration, where it's stored in plain text. Store it in Vault and read it at call time:
1headers := jsonb_build_object(2 'Content-Type', 'application/json',3 'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'secret_key')4)步骤 4:更新 Edge 函数 #
🌐 Step 4: Update Edge Functions
Edge Functions 从环境变量中读取它们的密钥。Supabase 会在你的函数环境中添加两个新的变量 SUPABASE_PUBLISHABLE_KEYS 和 SUPABASE_SECRET_KEYS,同时保留旧的 SUPABASE_ANON_KEY 和 SUPABASE_SERVICE_ROLE_KEY。在开始之前,请先在仪表板的 Edge Functions > Secrets 部分确认它们是否存在。
🌐 Edge Functions read their keys from environment variables. Supabase adds two new ones to your functions' environment, SUPABASE_PUBLISHABLE_KEYS and SUPABASE_SECRET_KEYS, alongside the legacy SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY. Confirm they exist in the Edge Functions > Secrets section of the Dashboard before you start.
你有两个选择:一个是只做最小的修改,换一下读取的变量;另一个是完整升级到 @supabase/server SDK。
🌐 You have two options: a minimal change that swaps which variable you read, or a fuller upgrade to the @supabase/server SDK.
选项 1:从环境中读取新密钥 #
🌐 Option 1: Read the new keys from the environment
对于大多数函数,唯一的变化就是你读取键的方式。旧的变量保存的是普通字符串。新的变量保存的是按名称键控的 JSON 对象,所以你需要解析它们,然后按名称读取键。你在第 1 步创建的键名为 default。
🌐 For most functions, the only change is how you read the key. The legacy variables held a plain string. The new ones hold a JSON object keyed by name, so you parse them and read the key by name. The key you created in step 1 is named default.
1// before2const secretKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!34// after5const secretKey = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default']可发布的密钥通过 SUPABASE_PUBLISHABLE_KEYS 的方式工作。想了解更多 Edge Functions 中环境变量的内容,请阅读 管理机密指南。
🌐 The publishable keys work the same way through SUPABASE_PUBLISHABLE_KEYS. Read the Managing Secrets guide for more on environment variables in Edge Functions.
如果你在步骤 1中创建了多个密钥,每个密钥都存放在同一个 SUPABASE_SECRET_KEYS 对象中,并且都有自己的名字。读取非默认密钥的方式和读取默认密钥一样:
🌐 If you created more than one secret key in step 1, every key lives in the same SUPABASE_SECRET_KEYS object, each under its own name. Read a non-default key the same way:
1const secretKeys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)23const defaultKey = secretKeys['default']4const billingKey = secretKeys['billing'] // the secret key you named "billing"只在 apikey 头上发送可发布和秘密密钥。如果你也在 Authorization: Bearer 头上传递密钥(很多 Supabase 客户端默认会这么做),平台会尝试将其解析为 JWT 并用 Invalid JWT 拒绝请求。平台内置的 verify_jwt 检查只理解基于旧版 JWT 的密钥,所以为这些功能设置 verify_jwt = false 并在自己的代码中授权请求,或者让 @supabase/server SDK 为你处理 (选项 2)。
🌐 Send publishable and secret keys on the apikey header only. If you also pass the key on the Authorization: Bearer header, which many Supabase clients do by default, the platform tries to parse it as a JWT and rejects the request with Invalid JWT. The platform's built-in verify_jwt check only understands the legacy JWT-based keys, so set verify_jwt = false for these functions and authorize the request in your own code, or let the @supabase/server SDK do it for you (Option 2).
1[functions.my-function]2verify_jwt = false选项 2:采用 @supabase/server SDK #
🌐 Option 2: Adopt the @supabase/server SDK
要充分利用新的密钥模型,请迁移到 @supabase/server SDK。它消除了每个函数都会重复的客户端设置样板代码:从环境中读取密钥、解析 Authorization 头信息,以及初始化用户作用域客户端和独立的管理员客户端。你只需声明谁可以调用该函数,并在 ctx 上准备好使用两个客户端(ctx.supabase 支持行级安全,ctx.supabaseAdmin 使用密钥)。这是新函数的推荐做法。
🌐 To get the most out of the new key model, migrate to the @supabase/server SDK. It removes the client-setup boilerplate every function repeats: reading keys from the environment, parsing the Authorization header, and initializing a user-scoped client and a separate admin client. You declare who can call the function, and get both clients ready to use on ctx (ctx.supabase respects Row Level Security, ctx.supabaseAdmin uses the secret key). This is the recommended approach for new functions.
用 withSupabase 封装你现有的 Deno.serve 处理器,并声明一个 auth 模式来指定谁可以调用它。保留 verify_jwt = false,这样 SDK 就会处理授权。
🌐 Wrap your existing Deno.serve handler with withSupabase and declare an auth mode for who can call it. Keep verify_jwt = false so the SDK does the authorization.
对于用户从客户端调用的函数,使用 auth: 'user'。SDK 会验证用户的会话 JWT,并为你提供一个遵循他们行级安全策略的客户端。
🌐 For a function your users call from the client, use auth: 'user'. The SDK validates the user's session JWT and gives you a client scoped to their Row Level Security policies.
1import { withSupabase } from 'npm:@supabase/server'23Deno.serve(4 withSupabase({ auth: 'user' }, async (_req, ctx) => {5 // ctx.supabase is scoped to the authenticated user6 return Response.json({ email: ctx.userClaims?.email })7 })8)对于由你自己的后端、一个 worker 或 pg_net 调用的函数,使用 auth: 'secret'。SDK 会验证密钥,并给你一个可以绕过行级安全的客户端。
🌐 For a function called by your own backend, a worker, or pg_net, use auth: 'secret'. The SDK validates the secret key and gives you a client that bypasses Row Level Security.
1import { withSupabase } from 'npm:@supabase/server'23Deno.serve(4 withSupabase({ auth: 'secret' }, async (_req, ctx) => {5 // ctx.supabaseAdmin is authenticated with a valid secret key6 return Response.json({ ok: true })7 })8)要接受一个特定命名的密钥而不是 default,在模式后加上冒号再写上它的名称。例如,auth: 'secret:billing' 会根据你命名为 billing 的密钥来验证请求,而 auth: 'publishable:web' 则会根据命名为 web 的可发布密钥来验证。
🌐 To accept a specific named key instead of default, add its name after the mode with a colon. For example, auth: 'secret:billing' validates the request against the secret key you named billing, and auth: 'publishable:web' against a publishable key named web.
withSupabase 返回一个标准请求处理器,所以你也可以将它作为 fetch 处理器导出,而不是传递给 Deno.serve:
1import { withSupabase } from 'npm:@supabase/server'23export default {4 fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {5 // ctx.supabase is scoped to the authenticated user6 return Response.json({ email: ctx.userClaims?.email })7 }),8}export default { fetch } 等同于 Deno.serve(...):两者都定义了一个请求处理器。fetch 风格可以在 Edge Functions、Cloudflare Workers 和 Bun 上通用,所以如果你希望同一个函数在多个环境中运行,最好使用它。Deno.serve 在 Edge Functions 上依然有效,所以在迁移期间可以先保留,之后再切换也没问题。
一个尝试这个的好方法是先复制你的其中一个函数,然后先迁移这个副本。有关每种认证模式和使用场景,请参见 保护 Edge Functions,有关头信息的工作原理,请参见 授权头。
🌐 A good way to try this is to duplicate one of your functions and migrate the copy first. See Securing Edge Functions for every auth mode and use case, and Authorization headers for how the headers work.
步骤5:确认没有东西使用旧密钥 #
🌐 Step 5: Verify nothing uses the legacy keys
在关闭旧的密钥之前,确认没有任何东西还依赖它们。没有自动使用指示,所以这是手动检查。检查每个存放 Supabase 密钥的地方,确保现在使用的是可发布密钥或秘密密钥。
🌐 Before turning the legacy keys off, confirm nothing still depends on them. There's no automatic usage indicator, so this is a manual check. Go through every place that holds a Supabase key and make sure it now uses a publishable or secret key.
别忘了那些容易被忽略的来电:
🌐 Don't forget callers that are easy to miss:
- 用户手里已经有了手机版或桌面版应用。
- CI/CD 流水线和部署脚本。
- 第三方集成和网页钩子。
- Cron 作业、工作进程,以及
pg_net调用或数据库 Webhooks(参见 数据库 Webhooks 和pg_net)。
第6步:停用旧密钥 #
🌐 Step 6: Deactivate the legacy keys
一旦你确认没有什么在使用旧版密钥,就在仪表板的 设置 > API 密钥 部分将它们停用。如果你发现还有遗漏的客户端,你可以重新激活它们,所以这一步是可以撤销的。
🌐 Once you've confirmed nothing uses the legacy keys, deactivate them in the Settings > API Keys section of the Dashboard. You can re-activate them if you find a client you missed, so this step is reversible.
已知限制 #
🌐 Known limitations
有一些行为与传统基于 JWT 的密钥不同。在迁移过程中需要考虑它们:
🌐 A few behaviors differ from the legacy JWT-based keys. Plan for them during the migration:
- 你不能在
Authorization: Bearer ...头里发送可发布的或秘密的密钥。改在apikey头里发送吧。 - Edge 函数不会验证新密钥的
apikey头。使用verify_jwt = false并在代码中授权,如 第 4 步 所示。 - 公共实时连接限制为 24 小时,除非通过 Supabase Auth 或支持的第三方身份验证提供商进行用户级认证来升级连接。
下一步 #
🌐 Next steps
在迁移完你的密钥后,可以考虑也迁移到 JWT 签名密钥 系统。这是一次独立的迁移。新的可发布和私密密钥不是 JWT,所以它们不再涉及你项目的 JWT 密钥。但 Supabase Auth 发给你的用户的访问令牌仍然由那个共享的密钥签名。签名密钥用可轮换的密钥替代它,你可以在不中断服务的情况下更换。两个迁移结合起来,就可以让你的整个项目脱离共享的 JWT 密钥。
🌐 After migrating your keys, consider moving to the JWT signing keys system as well. This is a separate, independent migration. The new publishable and secret keys aren't JWTs, so they no longer touch your project's JWT secret. But the access tokens Supabase Auth issues to your users are still signed by that shared secret. Signing keys replace it with rotatable keys you can change without downtime. Together, the two migrations get your whole project off the shared JWT secret.