首页
/ 使用 dotenvx 加密变量管理 Supabase 多环境配置:Next.js Slack Clone 实战

使用 dotenvx 加密变量管理 Supabase 多环境配置:Next.js Slack Clone 实战

2026-09-06 18:55:30作者:翟萌耘Ralph

Supabase 的 config.toml 原生支持 env() 语法引用环境变量,而 dotenvx 提供了密钥加密能力,让团队可以在提交到 Git 的配置文件中安全地存放 GitHub OAuth 等第三方凭证。本文以仓库中的 nextjs-slack-clone-dotenvx 全栈示例为蓝本,完整演示本地开发、远程部署与 Preview 分支三种场景下的环境变量组织、加密与下发方式。读完本文你将掌握一套"明文留本地、加密进仓库、密钥进 Secrets"的多环境密钥管理方案,并可将其直接迁移到自己的 Supabase 项目。


1. 示例项目概览

本示例是一个使用 Next.js + Supabase 构建的全栈 Slack 克隆,前端通过 Supabase.js 完成用户管理(GitHub 登录)与实时数据同步,后端直接使用 Supabase 托管的 Postgres 数据库及其 RESTful API。你可以对照 完整 README 查看该项目相对 nextjs-slack-clone 变体的差异——后者没有引入 dotenvx,二者的对比恰好体现了 dotenvx 带来的安全收益。

项目的依赖声明在 package.json 中:运行时依赖 @supabase/supabase-js@^2nextreact,开发依赖中则同时引入了 @dotenvx/dotenvx@^1.28.0supabase@^2.7.2(Supabase CLI)与 Tailwind 相关工具链。可见 dotenvx 被定位为开发/部署工具链的一部分,而非应用运行时依赖。

前端与实时数据层的代码结构值得留意,它们是理解"为什么需要不同环境配置"的背景:

  • lib/Store.js:以 process.env.NEXT_PUBLIC_SUPABASE_URLprocess.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY 创建 Supabase 客户端,并通过 postgres_changes 订阅 messagesuserschannels 三张表的 INSERT/DELETE/UPDATE 事件实现实时聊天。
  • pages/_app.js:通过 jwtDecode(session.access_token) 解码 JWT 并取出由 Auth Hook 注入的 user_role claim,实现前端侧的权限判断。
  • supabase/migrations/20240214102356_init.sql:建表与 RLS 策略,定义了 userschannelsmessagesuser_roles 等核心表。
  • supabase/migrations/20240214114147_auth-hook.sql:Custom Access Token Hook,把用户角色写入访问令牌。

应用要真正跑起来,就必须让"前端连哪个 Supabase 项目、Auth 的 GitHub OAuth 用哪对凭证"在不同环境下可切换——这正是本示例引入 dotenvx 与 env() 语法的直接动机。


2. 核心概念:env() 语法与 dotenvx 加密

Supabase 项目级配置集中在 supabase/config.toml。该文件支持两类变量注入:

  • env(VAR_NAME):运行时把环境变量值写入配置项,适用于 site_urlclient_idsecret 等几乎所有字段;
  • encrypted:<encrypted-value>:把 dotenvx 加密后的密文直接内联到配置值中。

dotenvx 的核心价值在于密钥分离管理:

  • 敏感值以密文形式存储,私钥(解密密钥)单独保存在 .env.keys 中,且该文件应被 Git 排除;
  • 团队之间可以通过共享公钥安全地交换并加密环境值;
  • 只要掌握解密密钥,就可以在任意环境安全地读取密文。

示例的 config.toml 中可以直观看到 env() 的实际用法——它把环境相关的取值全部抽离成了变量:

site_url = "env(SUPABASE_AUTH_SITE_URL)"
additional_redirect_urls = [
    # Will be localhost:3000 in development or the URL of your deployed app in production.
    "env(SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS)",
]

[auth.external.github]
enabled = true
client_id = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"

重要限制:encrypted: 语法仅对配置中"设计为 secret 的字段"(例如 Auth Provider 的 secret)生效。 把它用在非 secret 字段(如 client_id)上不会被自动解密,反而可能引发问题。若需要在非 secret 字段中保护敏感信息,请改用 env() 语法引用环境变量:

[auth.external.github]
enabled = true
client_id = "encrypted:<value>"   # 不生效:client_id 不是 secret 字段,不会解密
secret = "encrypted:<encrypted-value>"  # 生效:secret 是指定的 secret 字段

此外,也可以直接把加密值写进 config.toml 的 secret 字段,从而完全绕过环境变量

[auth.external.github]
enabled = true
secret = "encrypted:<encrypted-value>"

两种方案都能保证安全性——前者通过 env() 把密文交给 dotenvx 在运行时解密后注入,后者则让 config.toml 本身成为加密配置的载体。

除 GitHub OAuth 外,该 config.toml 还启用了 Custom Access Token Hook(supabase/config.toml[auth.hook.custom_access_token] 指向 pg-functions://postgres/public/custom_access_token_hook),它配合 auth-hook 迁移 中的 PL/pgSQL 函数把 public.user_roles 的角色写入 JWT claim,为前端 RBAC 提供依据。


3. 环境文件的组织规范

本示例遵循如下 dotenv 文件约定,所有文件均位于 supabase/ 目录下:

文件 适用环境 加入 .gitignore 是否加密
.env.keys 所有环境
.env.local 本地开发
.env.production 生产环境
.env.preview Preview 分支
.env 任意环境 视情况

几点设计意图需要展开说明:

  • .env 永远会被默认加载,因此可把它作为跨环境兜底,包括 Preview 分支场景;
  • 但如果你决定把 .env 提交进 Git,就必须按照下文"Preview 分支"一节的方法对 secret 值做加密,防止明文泄露;
  • .env.local 存放本地开发所需的明文(本地机器可信,无需加密);
  • .env.keys 存放解密密钥,是整条安全链路的命门,必须加入 .gitignore,并通过项目 Secrets(而非 Git)分发给 CI/CD 执行器。

4. 本地开发:明文 + 本地栈

本地开发时无需加密——机器上的 .env.local 可以直接保存明文凭证。先到 GitHub 创建 OAuth App,取得 <client-id><client-secret>,然后写入 supabase/.env.local

SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID=<client-id>
SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET=<client-secret>

由于 config.toml 中已经用 env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID) 等语法引用了这些变量,Supabase CLI 在本地读取配置时会把它们注入对应字段,无需改动任何配置文件即可完成本地 Auth 接线。

接着启动本地栈:

npx supabase start
npm run dev

第一条命令拉起本地 Postgres、GoTrue、Realtime 等 Supabase 服务(镜像与端口定义见 supabase/config.toml 中的 [db][api][realtime][auth] 段);第二条命令启动 Next.js 开发服务器。打开 localhost:3000 即可使用 GitHub OAuth 登录并体验实时聊天。登录成功后,Auth Hook 会在签发 JWT 时注入 user_role,前端则用 jwt-decode 解析它来做权限展示——这两步在本地与生产环境行为一致。

本地初始化数据库依赖两份迁移文件:20240214102356_init.sql(建表、RLS、Realtime publication、handle_new_user 触发器)与 20240214114147_auth-hook.sql(自定义 Access Token Hook),它们会在 supabase start 或后续 db push 时自动执行。


5. 远程部署到生产环境

5.1 前置条件

  • 一个 Vercel 账号(托管 Next.js 前端)
  • 一个 Supabase 账号与项目

5.2 创建远程项目并写入生产变量

在 Supabase Dashboard 新建项目,待数据库初始化完成后,创建 supabase/.env.production 并写入项目专属的连接信息(这部分属于"公开可读"的 URL/Key,可以明文):

NEXT_PUBLIC_SUPABASE_URL=https://<your-project>.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<your-project-apikey>

前端 lib/Store.js 正是通过这两个 NEXT_PUBLIC_ 前缀变量构造 Supabase 客户端的。Next.js 会在构建期把 NEXT_PUBLIC_ 变量内联进浏览器 bundle,因此它们必须对每个部署目标显式提供。

5.3 配置 Auth 回调地址

把认证服务使用的站点 URL 指向 Vercel 部署域名:

SUPABASE_AUTH_SITE_URL=https://<your-app-url>.vercel.app/
SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS=https://<your-app-url>.vercel.app/**

这两项分别对应 config.toml 中的 site_urladditional_redirect_urls,GitHub OAuth 完成回调后必须命中这里的 allow-list 才能正确跳转。

5.4 加密 GitHub 凭证

用 dotenvx 的 set 子命令加密生产环境的 GitHub Secret:

npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "<your-secret>" -f supabase/.env.production

该命令会做三件事:

  1. supabase/.env.production 中生成新的**加密密钥(公钥)**与密文;
  2. supabase/.env.keys 中写入对应的解密密钥(私钥)
  3. 自动把加密后的值替换进该 dotenv 文件对应的变量位。

随后 supabase/.env.production 就可以安全地提交进 Git 仓库,因为其中不含任何明文 secret。

5.5 部署到 Supabase 远程项目

linkdb pushconfig push 三条命令都需要在 dotenvx 解密的环境中执行,因此统一用 run 包装:

npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase link
npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase db push
npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase config push
  • supabase link:把本地目录与远程项目关联(期间会校验访问令牌与项目 ref);
  • supabase db push:把 supabase/migrations/ 下的两份迁移推送到远程数据库,建立表结构、RLS 策略与 Auth Hook 函数;
  • supabase config push:把 config.toml 中的 Auth、Hook 等配置下发到远程项目,其中 env(...) 引用的变量会在 dotenvx 解密后被正确解析。

6. 结合 Preview 分支安全管理加密 Secret

Supabase 的分支(Branching)机制会为每个 Preview 分支创建一套独立的数据库与配置,dotenvx 已支持与之对接,使每个分支都可以持有专属的加密 secret 而不互相泄露。

6.1 为 Preview 生成独立密钥并加密

为 Preview 环境单独执行一次加密,得到独立的密钥对:

npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "<your-secret>" -f supabase/.env.preview

这会在 supabase/.env.preview 中写入一个新的加密公钥与密文,并在 supabase/.env.keys 中追加对应的解密私钥。生产与 Preview 各自拥有独立密钥对,互不干扰。

6.2 把解密密钥注入项目 Secrets

分支执行器(branching executor)在配置各分支服务时,需要读取解密密钥才能还原 secret。因此要把 .env.keys 中产/Preview 两套解密密钥一并登记到项目的 Secret 处理器:

npx supabase secrets set --env-file supabase/.env.keys

这一步执行后,分支执行器即可在部署流程中访问到解密所需的私钥。

6.3 在配置中引用加密值(二选一)

方案 A:直接把密文写进 config.toml

secret_value = "encrypted:<encrypted-value>"

方案 B:在 config.toml 中引用承载密文的环境变量

secret_value = "env(SOME_KEY)"

并提交携带密文的 supabase/.env.preview。分支执行器部署分支时会自动从 .env.preview 读取并解密这些值。

两种方案结合本仓库 config.toml 的惯例,可以推广为:敏感字段优先 env() + 加密 dotenv 文件,特殊字段可直接内联 encrypted: 密文

完成上述步骤后,Preview 分支即拥有各自独立的加密 secret。分支执行器会同时自动处理数据库迁移(migrations)与配置更新(config),无需为每个分支单独执行 db push / config push


7. 安全性与最佳实践小结

将整套流程落地的过程中,有几个关键点值得沉淀为团队规范:

  1. 密钥文件绝不入库.env.keys 是唯一能解开所有密文的私钥集合,必须始终处于 .gitignore 保护之下,并通过 supabase secrets set --env-file supabase/.env.keys 注入项目 Secrets,而不是通过 Git 分发。
  2. 明文只允许出现在本地.env.local 不进版本库;凡是进入 Git 的 dotenv 文件(.env.production.env.preview、以及要提交的 .env),其中的 secret 一律先经 dotenvx set 加密。
  3. encrypted: 只在 secret 字段使用:非 secret 字段的敏感信息请改用 env(...),否则不会自动解密,可能造成配置解析失败。
  4. 分支级密钥隔离:生产与 Preview 使用各自的密钥对,一个环境的泄露不会波及其他环境;每次 dotenvx set 都会把新增解密密钥追加进 .env.keys,记得随后重新同步一次项目 Secrets。
  5. 环境相关的取值全部变量化:仿照本示例把 site_url、回调地址、GitHub OAuth 凭证等写入 config.tomlenv() 引用,让同一份配置文件在不同环境只依赖外部变量差异,配置文件本身保持稳定。

dotenvx 与 Supabase env()/encrypted: 机制的组合,把"环境差异化"与"密钥安全"两个问题一并解决:本地开发体验不变,生产与 Preview 分支则获得可审计、可轮换的加密配置管理能力。若需查看完整可运行代码,可对照仓库中的 nextjs-slack-clone-dotenvx 目录(含前端组件、迁移文件、supabase/config.toml 与全量 SQL full-schema.sql)按步骤自行复现。

登录后查看全文
热门项目推荐
相关项目推荐