OpenNext Cloudflare 本地开发中的环境变量边界

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处
OpenNextCloudflareNext.jsAuth.js

问题现象

在本地运行 Next.js 开发服务器时,控制台同时出现了两个看起来互相矛盾的现象:

Using secrets defined in .dev.vars

但访问 /api/auth/session 时,Auth.js 仍然报错:

[auth][error] MissingSecret: Please define a `secret`.
GET /api/auth/session 500

.dev.vars 中已经写了 AUTH_SECRET,wrangler.toml 中也有同名配置,为什么 Auth.js 还会认为没有 secret?

关键边界

这里的问题不在于 .dev.vars 没有被读取,而在于“谁读取、读到哪里”。

OpenNext Cloudflare 的本地开发与部署文档说明,next dev 会通过 initOpenNextCloudflareForDev() 初始化 Cloudflare 本地上下文。这个过程让 Wrangler/Miniflare 读取 wrangler.toml 和 .dev.vars,所以日志会打印:

Using secrets defined in .dev.vars

OpenNext Cloudflare Bindings 文档给出的典型访问方式是通过本地运行时上下文读取:

import { getCloudflareContext } from "@opennextjs/cloudflare";

const { env } = getCloudflareContext();
const secret = env.AUTH_SECRET;

但是 Auth.js 的 MissingSecret 检查默认读取的是 Node 进程环境变量:

process.env.AUTH_SECRET

在 next dev 这条链路里,.dev.vars 被读进了 Cloudflare context,不等于被同步进了 process.env。所以会出现:

  • Wrangler/OpenNext:我读到了 .dev.vars。
  • Auth.js:我在 process.env 里没有看到 AUTH_SECRET。

这两句话可以同时为真。

为什么容易误判

Using secrets defined in .dev.vars 这句日志很容易让人以为所有库都能读到 .dev.vars 中的变量。

实际不是。它只说明 Wrangler 侧加载了本地 secrets。对于使用 getCloudflareContext().env 的代码,例如 D1、KV、R2 绑定,这通常没有问题。

但很多 Next.js / Node.js 生态库不认识 Cloudflare context,只读取 process.env。Auth.js 环境变量文档中的 AUTH_SECRET / NEXTAUTH_SECRET 自动推断也来自 process.env。

本项目里的实际链路

本地开发命令:

npm run dev

最终进入:

npm run -w apps/web dev
next dev --webpack

apps/web/next.config.ts 中调用了:

import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";

initOpenNextCloudflareForDev();

这能让应用代码通过 getCloudflareContext() 拿到 Cloudflare env 和 bindings,但不会自动把 .dev.vars 全量写入 process.env。

可以用 Wrangler API 验证 .dev.vars 是否被 Cloudflare context 读到:

Push-Location apps/web
node -e "import('wrangler').then(async ({ getPlatformProxy }) => { const { env } = await getPlatformProxy({ envFiles: [] }); console.log({ hasAuthSecret: Boolean(env.AUTH_SECRET), nextjsEnv: env.NEXTJS_ENV, trustHost: env.AUTH_TRUST_HOST }); })"
Pop-Location

如果输出类似:

Using secrets defined in .dev.vars
{ hasAuthSecret: true, nextjsEnv: 'development', trustHost: 'true' }

只能证明 Cloudflare context 有值,不能证明 process.env.AUTH_SECRET 有值。

正确处理方式

如果某个库读取的是 process.env,本地开发时就应该把变量放进 Next.js 会加载的 .env 文件,例如:

apps/web/.env.local

内容示例:

AUTH_SECRET=...
AUTH_TRUST_HOST=true

.dev.vars 仍然可以保留给 Cloudflare 本地 context 使用:

NEXTJS_ENV=development
AUTH_SECRET=...
AUTH_TRUST_HOST=true

但要记住:

  • .env.local 面向 next dev 和 process.env。
  • .dev.vars 面向 Wrangler/OpenNext/Cloudflare context。
  • wrangler.toml [vars] 面向 Wrangler 配置和部署运行时。

如果不想手动维护两份本地文件,可以在项目里写一个同步脚本,把 wrangler.toml [vars] 或 .dev.vars 同步到 .env.local。这属于项目约定,不是 OpenNext 或 Wrangler 自动完成的行为。

这个判断来自 OpenNext Cloudflare 的环境变量文档:本地开发时,如果要让变量进入 process.env,应使用 Next.js 的 .env 文件,而不是依赖 wrangler 配置或 .dev.vars。Cloudflare Workers 环境变量文档则把 .dev.vars 描述为 Wrangler 本地开发 secrets 文件。两者服务的入口不同。

不建议的处理

不建议为了 Auth.js 直接在通用配置里混入 Cloudflare context:

secret: getCloudflareContext().env.AUTH_SECRET

这样会把一个本应由 process.env 解决的通用库配置,绑定到 Cloudflare 运行时上下文。除非明确决定 Auth 配置只运行在 Cloudflare 环境中,否则这会让本地、构建、测试和其它运行方式的边界更难理解。

也不建议把开发命令写成很绕的 Node 入口,只为了强行加载 .dev.vars:

node --env-file=.dev.vars ... next dev

这种写法虽然能工作,但会让普通的 Next.js 开发命令变得不直观,也容易和团队习惯脱节。

排查清单

遇到 MissingSecret 时,可以按下面顺序判断:

  1. 确认 Auth.js 实际读取的是 AUTH_SECRET 还是 NEXTAUTH_SECRET。
  2. 确认当前命令是 next dev、OpenNext preview,还是 Cloudflare Worker 运行时。
  3. 如果是 next dev,检查 .env.local 是否有 AUTH_SECRET。
  4. 如果日志显示 Using secrets defined in .dev.vars,只说明 Cloudflare context 读到了,不代表 process.env 有值。
  5. 修改 env 文件后必须重启 dev server,旧 Node 进程不会自动获得新的环境变量。

结论

这次坑的核心不是 secret 没配置,而是配置进入了不同的环境容器。

Using secrets defined in .dev.vars 说明 Wrangler/OpenNext 读到了本地 secrets;Auth.js MissingSecret 说明 process.env.AUTH_SECRET 没有值。两者不冲突。

Auth.js 部署文档也要求按运行环境提供所需配置。本地开发时,如果依赖的是 Node/Next 生态库的 process.env,就使用 .env.local;如果依赖的是 Cloudflare bindings 或 getCloudflareContext().env,才使用 .dev.vars / wrangler.toml 这条链路。