使用 dotenvx 加密变量管理 Supabase 多环境配置:Next.js Slack Clone 实战
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@^2、next、react,开发依赖中则同时引入了 @dotenvx/dotenvx@^1.28.0、supabase@^2.7.2(Supabase CLI)与 Tailwind 相关工具链。可见 dotenvx 被定位为开发/部署工具链的一部分,而非应用运行时依赖。
前端与实时数据层的代码结构值得留意,它们是理解"为什么需要不同环境配置"的背景:
- lib/Store.js:以
process.env.NEXT_PUBLIC_SUPABASE_URL与process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY创建 Supabase 客户端,并通过postgres_changes订阅messages、users、channels三张表的 INSERT/DELETE/UPDATE 事件实现实时聊天。 - pages/_app.js:通过
jwtDecode(session.access_token)解码 JWT 并取出由 Auth Hook 注入的user_roleclaim,实现前端侧的权限判断。 - supabase/migrations/20240214102356_init.sql:建表与 RLS 策略,定义了
users、channels、messages、user_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_url、client_id、secret等几乎所有字段;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_url 与 additional_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
该命令会做三件事:
- 在
supabase/.env.production中生成新的**加密密钥(公钥)**与密文; - 在
supabase/.env.keys中写入对应的解密密钥(私钥); - 自动把加密后的值替换进该 dotenv 文件对应的变量位。
随后 supabase/.env.production 就可以安全地提交进 Git 仓库,因为其中不含任何明文 secret。
5.5 部署到 Supabase 远程项目
link、db push、config 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. 安全性与最佳实践小结
将整套流程落地的过程中,有几个关键点值得沉淀为团队规范:
- 密钥文件绝不入库:
.env.keys是唯一能解开所有密文的私钥集合,必须始终处于.gitignore保护之下,并通过supabase secrets set --env-file supabase/.env.keys注入项目 Secrets,而不是通过 Git 分发。 - 明文只允许出现在本地:
.env.local不进版本库;凡是进入 Git 的 dotenv 文件(.env.production、.env.preview、以及要提交的.env),其中的 secret 一律先经dotenvx set加密。 encrypted:只在 secret 字段使用:非 secret 字段的敏感信息请改用env(...),否则不会自动解密,可能造成配置解析失败。- 分支级密钥隔离:生产与 Preview 使用各自的密钥对,一个环境的泄露不会波及其他环境;每次
dotenvx set都会把新增解密密钥追加进.env.keys,记得随后重新同步一次项目 Secrets。 - 环境相关的取值全部变量化:仿照本示例把
site_url、回调地址、GitHub OAuth 凭证等写入config.toml的env()引用,让同一份配置文件在不同环境只依赖外部变量差异,配置文件本身保持稳定。
dotenvx 与 Supabase env()/encrypted: 机制的组合,把"环境差异化"与"密钥安全"两个问题一并解决:本地开发体验不变,生产与 Preview 分支则获得可审计、可轮换的加密配置管理能力。若需查看完整可运行代码,可对照仓库中的 nextjs-slack-clone-dotenvx 目录(含前端组件、迁移文件、supabase/config.toml 与全量 SQL full-schema.sql)按步骤自行复现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00