name: dotenvx-secrets description: "How this repo manages API secrets and the three local-dev environments (local/dev/prod). They are dotenvx-ENCRYPTED in git and the keys live in Dotenv Armor. Load this WHENEVER you touch a secret, API key, token, credential, or any apps/api/.env* file; whenever the user pastes a key/token/secret to store or use; whenever choosing/switching which environment to run (local vs dev vs prod); and whenever adding, reading, rotating, or sharing a secret."
Secrets & environments (this repo)
API secrets are encrypted in git with dotenvx; the decryption keys live off-device in Dotenv Armor. This is mandatory — a plaintext secret never belongs in a tracked file.
The three environments (local-dev secrets)
There are three environments, each a separate encrypted file with its own keypair. They differ only in which backend the locally-running API talks to — same code, different DB / Stripe / keys:
pnpm command |
Env | File | API talks to | private key in .env.keys |
|---|---|---|---|---|
pnpm dev |
local | apps/api/.env |
100% local stack (local Supabase in Docker, test Stripe) + runs web + tunnel | DOTENV_PRIVATE_KEY |
pnpm dev:dev-env |
dev | apps/api/.env.dev |
the dev stack — dev Supabase DB, test Stripe, dev keys (dev-api.kortix.com) |
DOTENV_PRIVATE_KEY_DEV |
pnpm dev:prod-env |
prod | apps/api/.env.prod |
the prod stack — prod Supabase DB, LIVE Stripe, prod keys (api.kortix.com) |
DOTENV_PRIVATE_KEY_PROD |
pnpm devruns the full local stack (web + API + local Supabase + tunnel) viascripts/dev-local.sh.pnpm dev:dev-env/pnpm dev:prod-envrun the API only, locally, against the remote dev/prod backend (dotenvx run -f apps/api/.env.<dev|prod> -- bun run --hot src/index.ts). They do not start local Supabase.- ⚠️
pnpm dev:prod-envpoints your local API at production — DB writes and Stripe calls are real. Use deliberately.
CRITICAL — .env.prod is NOT what production runs
The deployed production infra loads its env from AWS Secrets Manager at runtime. apps/api/.env.prod is only for running locally against the prod backend. Editing apps/api/.env.prod does not change what production runs — to change real prod secrets, update AWS Secrets Manager.
The one rule (non-negotiable)
Never write a plaintext secret into a tracked file, a commit, or a code/PR artifact. Every secret goes in through dotenvx, which encrypts it in place. The only plaintext that ever exists is in process memory at runtime and in the gitignored apps/api/.env.keys / apps/api/.env.local.
When the user pastes a key/token/secret
Do not paste it into a file, echo it back, or commit it. Store it encrypted in the right env file:
dotenvx set THE_KEY_NAME 'pasted-value' -f apps/api/.env # local
dotenvx set THE_KEY_NAME 'pasted-value' -f apps/api/.env.dev # dev
dotenvx set THE_KEY_NAME 'pasted-value' -f apps/api/.env.prod # prod
This re-encrypts the file in place (value becomes KEY=encrypted:…). Then commit. No Armor push is needed for a new/changed secret — the keypair is unchanged, so teammates can already decrypt it; the new ciphertext just rides in git.
How it works
- Every value is AES-encrypted. The public key (encrypts) sits at the top of each file and is safe to commit; the private key (decrypts) never touches git.
- Private keys live in Dotenv Armor (cloud) and/or the gitignored
apps/api/.env.keys. dotenvx run -f <file> -- <cmd>decrypts in memory and injects real env vars — nothing plaintext hits disk.
Commands
| Task | Command |
|---|---|
| Run local / dev / prod | pnpm dev · pnpm dev:dev-env · pnpm dev:prod-env |
| Verify all 3 envs decrypt + are separated | pnpm test:envs |
| Read a secret | `dotenvx get KEY -f apps/api/.env[.dev |
| Add / change a secret | `dotenvx set KEY value -f apps/api/.env[.dev |
| First time / new machine | dotenvx-armor login then cd apps/api && for f in .env .env.dev .env.prod; do dotenvx-armor pull -f "$f"; done |
| Share a NEW profile / rotated key | dotenvx-armor push -f <file> |
| Remove a key from the cloud | dotenvx-armor down -f <file> |
Machine-local overrides
Need a different value just on your machine? Put it in the gitignored apps/api/.env.local (plaintext is fine — never committed). Bun loads it at higher precedence than apps/api/.env. Never edit a committed profile file to a machine-local value.
Guardrails (don't bypass)
apps/api/.env.keys,apps/api/.env.local,apps/web/.env,supabase/.envare gitignored.- Version-controlled git hooks in
.githooks/(enable per clone:git config core.hooksPath .githooks). Every committable.envis dotenvx-managed, no exceptions: the pre-commit hook discovers any staged.env/.env.<env>(new services included) and auto-encrypts it (--no-armor, mints a keypair into the adjacent.env.keysfor new files), then blocks the commit if any unencrypted, non-gitignored.envremains; pre-push re-checks. Excluded:.env.keys(private keys) and.env.example(templates); gitignored files like.env.local/supabase/.envare never staged so they're untouched. .gitleaks.tomlallowlists the encryptedapps/api/.env*sosecret-scanpasses while still catching real plaintext anywhere else.- GitHub secret-scanning push protection is enabled on the repo.
If a guard fires, the fix is to encrypt the value, never to bypass it.
The web app (apps/web) — same setup
apps/web has the same three encrypted profiles (apps/web/.env / .env.dev / .env.prod), own keypairs in apps/web/.env.keys, armed in Armor. All values are now public (NEXT_PUBLIC_*) — they're encrypted purely for a standardized flow. Decrypted the same way: pnpm dev (via load_local_env) and pnpm dev:web (wrapped in dotenvx run -f .env). Pull on a new machine: cd apps/web && for f in .env .env.dev .env.prod; do dotenvx-armor pull -f "$f"; done.
Maintenance flags are DB-backed now (was Vercel Edge Config): stored in kortix.platform_settings['maintenance_config'], read via public GET /v1/system/maintenance, written via admin-only PUT /v1/system/maintenance, set from /admin/utils. The EDGE_CONFIG/EDGE_CONFIG_ID/VERCEL_API_TOKEN secrets + the @vercel/edge-config dep are gone.
Out of scope (not dotenvx-managed)
supabase/.env (local Supabase CLI / GitHub OAuth) is intentionally plaintext + gitignored — it's auto-loaded by the Supabase CLI, which can't read dotenvx encryption. Don't try to dotenvx-manage it.