Three ways to manage Supabase credentials across environments. Two require custom CI code you maintain forever. The third puts encrypted secrets in git — and it’s the one that works.
dotenvx with Supabase’s native integration cuts that to zero. Encrypted secrets live in git, decryption keys live in Supabase, and preview branches automatically get the right credentials. No custom scripts, no API calls to AWS Secrets Manager, no debugging why CI failed at 2am.
Here’s why I use it, and when you shouldn’t.
The Manual Credentials Problem
Four environments times five Supabase secrets equals 20 credentials to track. New developer joins? Slack them a .env file and hope it’s current. Preview deployment for a PR? Manually create a Supabase project, copy credentials, paste into Vercel dashboard. Wrong .env file loaded? Staging code hits production database. Monday morning panic.
I spent two hours onboarding an engineer once because credentials were scattered across Slack, 1Password, and a Notion doc that was outdated. That’s when I went looking for a better way.
The Secrets Manager Tax
The “correct” solution is AWS Secrets Manager or 1Password. Store credentials there, fetch them in CI, inject into the build. Sounds clean until you write the code:
# .github/workflows/deploy.yml
- name: Fetch secrets
run: |
aws secretsmanager get-secret-value \
--secret-id prod/supabase \
--query SecretString \
--output text > .env.production
# + IAM setup, error handling, retries, rate limits...
Now you maintain this. For every project. When it breaks (and it will — API rate limits, expired tokens, network timeouts), you debug CI logs at midnight. When a new environment spins up, you write more scripts.
The cost isn’t the secrets manager itself. It’s the CI plumbing around it.
The dotenvx Workflow
Here’s the entire setup:
npx @dotenvx/dotenvx set SECRET_NAME "value" -f supabase/.env.production
npx supabase secrets set --env-file supabase/.env.keys
First command encrypts secrets and creates .env.production (commit this) and .env.keys (gitignored). Second uploads decryption keys to Supabase. Deploy.
Supabase sees the encrypted .env.production file in your repo, fetches the decryption key from its own secrets store, decrypts at deploy time. Zero CI code. No custom scripts. It just works.
| Approach | CI Code | Key Storage | Preview Branches | Maintenance |
|---|---|---|---|---|
| Manual .env files | None | Gitignored | Manual setup per PR | High (drift, sharing) |
| Secrets Manager | Hundreds of lines | AWS/1Password | Custom automation | High (CI debugging) |
| dotenvx | Zero | Supabase secrets | Automatic | Low (rotate keys only) |
The Preview Branch Magic
Encrypt preview secrets once:
npx @dotenvx/dotenvx set \
SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "preview-secret" \
-f supabase/.env.preview
Commit .env.preview to git. Every preview branch automatically:
- Sees
.env.previewin the repo - Fetches decryption key from Supabase secrets
- Decrypts at deploy time
No per-PR setup. No manual credential copying. No Vercel dashboard env vars for 50 preview deploys.
I used to spend 10 minutes per PR configuring preview environments. Now I push code and the preview works. That 10 minutes compounds fast when you’re reviewing 20 PRs a week.
What I Learned the Hard Way
The .env.keys file is critical.
It lives in .gitignore — never commit this. It contains decryption keys for all environments. Upload it once with npx supabase secrets set. If someone leaves the team, rotate: regenerate all encrypted files with a new key and re-upload. I didn’t document this in our offboarding checklist initially. Cost us an afternoon when we realized a former contractor still had the old keys.
Two ways to use encrypted values.
Reference environment variables in config.toml:
[auth.external.github]
secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"
Or embed encrypted values directly:
[auth.external.github]
secret = "encrypted:BDE...base64..."
I prefer the first. Keeps config readable and env files as the single source of truth.
Local development still needs keys.
Developers need .env.keys to decrypt locally. Store it in 1Password, share on onboarding. Or use .env.local (gitignored, not encrypted) for local dev only and keep .env.production for deployments. I do the latter — local dev uses test credentials, production uses real ones.
When it breaks, it’s confusing.
Missing .env.keys upload to Supabase? Runtime errors, not deploy errors. You won’t know until the app tries to use GitHub auth and gets undefined. Check with npx supabase secrets list to verify keys are uploaded. I debugged this for 30 minutes once because I forgot to run secrets set after rotating keys.
The Security Trade-off
“We’re putting secrets in git” sounds bad. Even encrypted, some security teams veto this on principle.
Why it’s acceptable for most teams:
- Industry-standard AES-256 encryption — same algorithm class as AWS Secrets Manager
- Decryption keys stored separately — Supabase secrets, never in the repo
- Compromise requires multiple accesses — repo AND Supabase AND key extraction
- Audit trail — git history shows when credentials changed
- Rotation is simple — regenerate encrypted files with a new key
Why it might not be acceptable:
- Compliance requirements — SOC2/HIPAA auditors get nervous about secrets near git
- Organizational policy — some orgs have hard rules against this
- Already invested — if you have Vault/AWS Secrets Manager working, the CI code is already written
The real question: does your threat model include an attacker with both repo access and Supabase admin access? That level of compromise means they can already access production directly.
When to Use This
| You Should Use dotenvx If | You Should Not Use It If |
|---|---|
| Small-to-mid team (<50 people) | Enterprise compliance vetos encrypted-in-git |
| Preview deploys are frequent | Multi-cloud secrets (not just Supabase) |
| Zero CI maintenance is a priority | Already using Vault with automation in place |
| Using Supabase for hosting | Security team has hard policy against this |
The right amount of complexity is the minimum needed. For most Supabase projects, dotenvx is that minimum. The CI code you don’t write is code you don’t debug.
Example implementation: Supabase Slack Clone with dotenvx