This project uses dotconfig to
manage environment configuration. dotconfig assembles a single .env file
from layered source files — public config, SOPS-encrypted secrets, and
per-developer local overrides. Secrets are encrypted at rest with
SOPS + age.
config/
sops.yaml ← SOPS encryption rules
dev/
public.env ← non-secret dev vars (committed)
secrets.env ← SOPS-encrypted dev secrets (committed)
secrets.env.example ← plaintext template showing required vars
prod/
public.env ← non-secret prod vars (committed)
secrets.env ← SOPS-encrypted prod secrets (committed)
secrets.env.example ← plaintext template showing required vars
local/
<username>/
public.env ← developer-specific overrides (gitignored)
secrets.env ← optional encrypted local secrets (gitignored)
dotconfig load decrypts and merges these into a single .env file.
dotconfig save round-trips edits from .env back to the source files.
In production, secrets also flow through Docker Swarm:
SOPS + age (at rest, in repo)
→ dotconfig load / sops decrypt
→ Docker Swarm secrets (runtime, file-mounted)
→ entrypoint.sh
→ environment variables (application reads process.env)
Application code never reads files from /run/secrets/ directly. The
docker/entrypoint.sh script handles that.
| File | Committed | Purpose |
|---|---|---|
config/sops.yaml |
Yes | Lists authorized age public keys |
config/dev/public.env |
Yes | Non-secret dev env vars (plaintext) |
config/dev/secrets.env |
Yes | Encrypted development secrets |
config/dev/secrets.env.example |
Yes | Plaintext template (shows required vars) |
config/prod/public.env |
Yes | Non-secret prod env vars (plaintext) |
config/prod/secrets.env |
Yes | Encrypted production secrets |
config/prod/secrets.env.example |
Yes | Plaintext template (shows required vars) |
config/local/<username>/ |
No (gitignored) | Developer-specific overrides |
.env |
No (gitignored) | Generated by dotconfig load — do not edit directly |
*.agekey |
No (gitignored) | Private keys |
Legacy: The
secrets/directory and.env.oldstill exist for reference but are superseded byconfig/. They will be removed in a future sprint.
| Secret | Used By | Description |
|---|---|---|
session_secret |
server | Express session signing key |
github_client_id |
server | GitHub OAuth app client ID |
github_client_secret |
server | GitHub OAuth app client secret |
google_client_id |
server | Google OAuth client ID |
google_client_secret |
server | Google OAuth client secret |
pike13_access_token |
server | Pike 13 API access token (pre-obtained) |
mcp_default_token |
server | Bearer token for MCP server authentication |
anthropic_api_key |
server | Claude API key for AI features |
openai_api_key |
server | OpenAI API key (optional, for future use) |
admin_password |
server | Admin dashboard login password |
All OAuth secrets are optional. The app starts cleanly without them —
unconfigured integrations return 501 with setup instructions. See
docs/api-integrations.md for credential setup.
# Load dev environment with your local overrides
dotconfig load dev <username>
# Load prod environment (no local overrides)
dotconfig load prodEdit .env directly, then save changes back to the source files:
$EDITOR .env
dotconfig savedotconfig save re-encrypts secrets sections with SOPS automatically.
SOPS_CONFIG=config/sops.yaml sops config/dev/secrets.env
SOPS_CONFIG=config/sops.yaml sops config/prod/secrets.envCodespaces environments are ephemeral — your age private key is not present by default. Store it as a GitHub Codespaces secret and the devcontainer will install it automatically on every new Codespace.
If you don't have a keypair yet, generate one first (see
Onboarding a New Developer), then have a
teammate add your public key to config/sops.yaml and re-encrypt the secrets.
On your local machine:
cat ~/.config/sops/age/keys.txtCopy the full output (the comment lines and the AGE-SECRET-KEY-1... line).
- Go to GitHub → Settings → Codespaces → New secret
- Name:
AGE_PRIVATE_KEY - Value: paste the full key contents
- Under Repository access, authorize this repository
When you next create (or rebuild) a Codespace, post-create.sh reads
$AGE_PRIVATE_KEY and writes it to ~/.config/sops/age/keys.txt
automatically. dotconfig load dev <username> will work immediately.
Note: The secret is user-scoped. Each developer must add their own
AGE_PRIVATE_KEYand have their public key onboarded toconfig/sops.yaml.
Prerequisites: sops, age, and dotconfig must be installed.
- Codespaces:
sopsandageare installed automatically bypost-create.sh. Install dotconfig:pipx install dotconfig - macOS (local):
brew install sops age && pipx install dotconfig - Linux (local): see SOPS releases
and age releases, then
pipx install dotconfig
Run this on your own machine:
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
cat ~/.config/sops/age/keys.txtThis prints your public key (starts with age1...). Share it with the team.
Run the interactive script — it handles both steps:
npm run secrets:add-keyIt will prompt for the new age public key, append it to config/sops.yaml,
and run sops updatekeys on every encrypted file in config/.
Commit and push the updated config/sops.yaml and re-encrypted files.
mkdir -p config/local/<yourname>
cp config/local/eric/public.env config/local/<yourname>/public.env
# Edit with your values (Docker context, key path, etc.)
$EDITOR config/local/<yourname>/public.env
# Load dev config
dotconfig load dev <yourname>- Add the key to
config/dev/secrets.env.exampleandconfig/prod/secrets.env.example - Edit the encrypted files:
SOPS_CONFIG=config/sops.yaml sops config/dev/secrets.env SOPS_CONFIG=config/sops.yaml sops config/prod/secrets.env
- Reload locally:
dotconfig load dev <username> - If the secret is used in production, add it to the
secrets:block indocker-compose.prod.yml:secrets: db_password: external: true new_secret_name: external: true
- Reference it in the server's
secrets:list in the same file - Load it to the swarm:
npm run secrets:prod:rm && npm run secrets:prod - Re-deploy:
npm run deploy:prod
The docker/entrypoint.sh script automatically converts any file under
/run/secrets/ to an uppercase environment variable. No code changes
needed for the entrypoint.
# Create secrets (first time)
npm run secrets:prod
# Update secrets (remove old, create new)
npm run secrets:prod:rm
npm run secrets:prodThese scripts use scripts/load-secrets.sh which:
- Decrypts
config/prod/secrets.envvia SOPS - Creates each
KEY=valueas a lowercase Docker Swarm secret - Uses the production Docker context from
.env
- Never hardcode secrets in source code
- Never commit
.env(it's gitignored) - Never commit
*.agekeyprivate keys (gitignored) - Secrets flow through
entrypoint.sh— app code readsprocess.env - Use
dotconfig saveorsopsto edit encrypted files — never decrypt to a file other than.env