Supabase 环境变量生产环境 undefined 的修复

部署后 Supabase URL 或 anon / publishable key 变成 undefined——修宿主 env 配置、框架前缀、旧 build,以及 2026 新 key 迁移。

本地好好的,一上线就崩。你的 .env 里有 NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYcreateClient(url, key) 一切正常,部署到 Vercel / Netlify / Cloudflare Pages 后,浏览器 console 报:

Error: supabaseUrl is required.
TypeError: Cannot read properties of undefined (reading 'auth')

或者更安静——所有 Supabase 调用静默失败,页面能渲染但没数据。

最快的修复(90% 的情况): 部署平台压根没拿到这两个 env。到宿主后台加上 NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY,把所有环境(Production + Preview + Development)都勾上,然后重新部署——因为 NEXT_PUBLIC_* 的值是在 build 时被写死进 JS bundle 的,光在后台改不触发新 build 就毫无作用。

理解关键:env vars 不会「自动从 .env 同步到云」。每个平台都要单独配置;任何需要让浏览器读到的值都得带框架前缀(NEXT_PUBLIC_ / VITE_ / PUBLIC_),少了前缀就会被从 client bundle 里剥掉,读出来是 undefined

先定位你是哪种情况

动手前先把你看到的具体现象对到原因上。

现象 / 你看到的最可能的原因跳到
后台 env 列表是空的平台压根没拿到这两个 env原因 1
后台有这个 env,浏览器里仍是 undefined前缀不对 / 缺失,或者没重新部署原因 2、3
preview / 分支 URL 正常,生产域名崩env 只勾了 Preview原因 4
组件里 process.env.SUPABASE_SERVICE_ROLE_KEYundefined在 client 端读了 server-only key原因 5
明明设了值还报 Invalid URL / supabaseUrl is required值末尾带了空格 / 换行原因 6
跑了几个月,这周突然 401迁移途中旧 anon key 被停用原因 7

常见原因

按命中率从高到低排。

1. 部署平台压根没拿到这两个 env

最高频。你本地有 .env,但 Vercel / Netlify / Cloudflare 后台里什么都没加。.env 不进 git(也不该进),所以部署后 process.env.X === undefined

如何判断: 打开 Vercel / Netlify / Cloudflare Pages → Environment Variables → 这两个名字到底列没列出来?

2. 前缀和框架不匹配

只有带前缀的名字才会被写进 client bundle。前缀不对,值就从浏览器里消失了。

Next.js    → NEXT_PUBLIC_X    (process.env.NEXT_PUBLIC_X)
Vite       → VITE_X           (import.meta.env.VITE_X)
Astro      → PUBLIC_X         (import.meta.env.PUBLIC_X)
SvelteKit  → PUBLIC_X         (import { PUBLIC_X } from '$env/static/public')
Remix      → 不强制前缀;要在 loader / window.ENV 里显式暴露

如何判断: 每一处 client 端引用都用了带前缀的名字吗?在 'use client' 组件里裸写 process.env.SUPABASE_URL 永远是 undefined

3. 这次部署是在加 env 之前就 build 好的

最浪费时间的一种。NEXT_PUBLIC_* / VITE_* / PUBLIC_* 的值是在 build 时 写死进去的,不是 runtime 读取的。如果你加了 env 但只更新了后台,那个早就 build 好的 bundle 里写死的还是 undefined。必须触发一次全新的 build。

注意:Vercel 的 build cache 不会 挡住 env 更新——cache 缓存的是 node_modules 和框架产物,每次部署都会拿到最新的 env 值。坑在于:你部署的那个 commit 是在加 env 之前 就 build 完的。一次干净的 redeploy 两个问题一起解决。

如何判断: 你刚加完 env,后台也显示了,但线上 bundle 没变,因为根本没跑新 build。

4. 只设了 Preview,没设 Production

Vercel 的 env scope 分 Development / Preview / Production。只勾了 Preview,生产域名照样 undefined。这些值绝不会在 scope 之间自动同步。

如何判断: 分支 / preview URL 正常,生产域名不行。

5. 在 client 端读了 server-only env

SUPABASE_SERVICE_ROLE_KEY / sb_secret_... 没有公开前缀,所以在浏览器里就是 undefined——而且这是对的。secret / service-role key 拥有完整数据库权限、能绕过 Row Level Security,绝不能流到 client。

如何判断: 你在 client component 里读了一个不带前缀的 key。把这段代码挪到 server route、Server Action 或 Edge Function。

6. 值末尾带了空格或换行

复制 key 时多带了空格或 \n,初始化时 URL parse 失败、报 Invalid URL,尽管「值确实设了」。

如何判断: 在后台把值 trim 一下就好了。重新从 Supabase 仔细复制一遍。

7. 迁移途中旧 anon key 被停用(2026 新增)

截至 2026 年 6 月,Supabase 正在推行新的 key 体系,旧的 anon / service_role JWT key 计划在 2026 年底前停用。新项目下发的是 sb_publishable_...(client 安全,替代 anon)和 sb_secret_...(仅 server,替代 service_role)。如果队友在后台把 legacy key 停掉了,所有还在用旧 anon key 的请求就会开始返回 401 / Invalid API key,哪怕那个 env 还「在」。

如何判断: 跑了好几个月,某次 key 轮换后才崩,而且报的是 auth 错误而不是 undefined。见 Step 8。

最短修复路径

Step 1:到 Supabase 后台复制正确值

2026 年 Supabase 把 keys 从旧的 API settings 里拆了出来,所以菜单路径变成:

Supabase Dashboard → Project → Settings → API
  → Project URL  (https://<ref>.supabase.co)

Supabase Dashboard → Project → Settings → API Keys
  → Publishable key  (sb_publishable_...)   ← 浏览器端用这个
  → Legacy API Keys 标签页 → anon public(eyJ...)   ← 还没迁移就用这个

两种 key 可以同时生效,所以你能一个 client 一个 client 地迁。复制时别带前后空格。

Step 2:到部署平台添加 env

Vercel:
  Project → Settings → Environment Variables
  → Key: NEXT_PUBLIC_SUPABASE_URL    Value: https://<ref>.supabase.co
  → Key: NEXT_PUBLIC_SUPABASE_ANON_KEY  Value: sb_publishable_...(或 eyJ... 旧 key)
  → Environments: 勾上 Production + Preview + Development

Netlify:
  Site configuration → Environment variables → Add a variable
  → "Deploy contexts" 选 All

Cloudflare Pages:
  Settings → Variables and Secrets → Production(以及 Preview)
  → build 时要读的变量必须设在这里,VITE_ / NEXT_PUBLIC_ 才会被 inline 进去

确认所有环境都勾上。在 Cloudflare 上,带 client 前缀的变量必须在 build 时就存在,光设 runtime 不会被 inline。

Step 3:grep 代码确认前缀

# Next.js——每处 client 引用都必须是 NEXT_PUBLIC_ 名字
grep -rn "process.env.*SUPABASE" src/

# Vite
grep -rn "import.meta.env.*SUPABASE" src/   # 应是 VITE_SUPABASE_*

# Astro
grep -rn "import.meta.env.*SUPABASE" src/   # 应是 PUBLIC_SUPABASE_*

不匹配的改对:

// Next.js
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

Step 4:强制重新部署

光改后台没用——必须重新 build,新值才会被 inline 进去。

Vercel:
  Deployments → 最新 → "..." 菜单 → Redeploy
  → 取消勾选 "Use existing Build Cache"(强制干净重 build)

Netlify:
  Deploys → Trigger deploy → "Clear cache and deploy site"

Cloudflare Pages:
  Deployments → 最新 → "Retry deployment"(用当前变量重 build)

Step 5:线上验证

打开生产 URL,开 DevTools → Console。因为值是 inline 进去的,这个变量会实打实地出现在已发布的 bundle 里:

// Next.js
console.log(process.env.NEXT_PUBLIC_SUPABASE_URL);
// → "https://<ref>.supabase.co",不是 undefined

// Vite
console.log(import.meta.env.VITE_SUPABASE_URL);

也可以确认一次真实请求能打到 Supabase:

const { error } = await supabase.from('any_table').select('*').limit(1);
console.log(error ?? 'reached Supabase');

还是 undefined → 前缀不对(Step 3)或 build cache 没清(Step 4)。报 401 / Invalid API key → 是 key 本身的问题,不是 env 接线(Step 8)。

Step 6:加 boot 时校验

让下次缺 env 直接 build 失败,而不是让用户在 runtime 撞上:

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(20),
});

export const env = envSchema.parse({
  NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
  NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
});

build 阶段因为缺 env 直接挂,比生产页面默默渲染一片空白要省事得多。

Step 7:把 server-only key 分清

// ✅ client 可见(低权限,可以安全下发)
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY        // 或 sb_publishable_...

// ❌ 只 server 用——绝不能加 NEXT_PUBLIC_ / VITE_ / PUBLIC_ 前缀
SUPABASE_SERVICE_ROLE_KEY            // 或 sb_secret_...  完整 DB 权限,绕过 RLS

一旦 secret / service-role key 进了 client bundle,就当它已经泄露:立刻到 Settings → API Keys 轮换。

Step 8:如果你正处在 key 迁移途中

如果请求报的是 401 / Invalid API key 而不是 undefined,那 env 接线没问题,问题在 key。切到 publishable key,并同步更新 env 和任何写死的兜底值:

NEXT_PUBLIC_SUPABASE_ANON_KEY = sb_publishable_...   # 替代 eyJ... anon
SUPABASE_SECRET_KEY           = sb_secret_...        # 替代 service_role

一次迁一个 client——两种 key 在你停用 legacy key 之前都一直有效,所以不存在「一刀切」的切换日。等到确认没有任何东西依赖 legacy key 了,再把它停掉。

预防建议

  • 在 README 或 CLAUDE.md 里列出所有必须的 env(连前缀一起)。
  • 项目里放一个进了 git 的 .env.example(不含真值)跟 .env 同步,新人照着配。
  • 用 zod 在 boot 时校验 env,缺什么立刻 fail-fast。
  • 任何含 _KEY_SECRET_TOKENsb_secret_ 的名字,永远不加 NEXT_PUBLIC_ / VITE_ / PUBLIC_ 前缀。
  • 加新 env 时,一次性把 Local + Preview + Production 三处都设好。
  • 改完 env 习惯性「Clear cache and redeploy」,别假设增量 build 会带上新值。
  • 把 env 改动写进 PR template 或 deploy checklist。
  • 跟进 Supabase 的 key 迁移:计划在 2026 年底停用期之前从 legacy anon / service_role 切走。

常见问题

为什么本地行、生产不行? 本地进程直接读 .env;云端 build 不读。每个平台都要在它自己的后台里配好这些值,然后值才会在 build 时被 inline 进 bundle。后台没这一条,就没这个值。

我在 Vercel 上加了,怎么还是 undefined 因为 NEXT_PUBLIC_* 是在 build 时编进 JS 的,你加 env 之前就 build 好的那个部署里写死的仍是 undefined。重新部署(Step 4),让全新的 build 把新值 inline 进去。

该用 anon key 还是新的 publishable key? 今天两种都能用。截至 2026 年 6 月,旧的 anon / service_role JWT key 计划在 2026 年底前停用,所以新项目优先用 sb_publishable_...(client)和 sb_secret_...(server)。两种 key 同时有效,可以平滑迁移。

把 anon / publishable key 暴露在浏览器里安全吗? 安全。它是低权限、本就设计来下发到 client 代码里的;真正保护数据的是你的 Row Level Security 策略。危险的是 service-role / sb_secret_... key,那个必须留在 server 端。

key 现在在哪找?旧的 API 页面变了。 Project URL 仍在 Settings → API 下。keys 在 2026 年挪到了独立的 Settings → API Keys 页,旧的 anon / service_roleLegacy API Keys 标签页里。

跑了好几个月,突然 401。 这不是 env 接线问题——是 key 的问题。多半是迁移期间某个 legacy key 被轮换或停用了。把 env 切到 publishable / secret key(Step 8)。

相关阅读

标签: #后端 #排查 #排查 #Supabase