ADR-0010 — Doppler over HashiCorp Vault and AWS SSM Parameter Store¶
Status: Accepted
Date: 2026-04-21 (workspace populated; decision crystallised by 2026-04-25)
Author(s): Platform engineering (handover documented in memory reference_doppler_workspace.md)
Context¶
The Evospin codebase is three sibling repos (ebit-api, ebit-fe, ebit-admin-fe) each with multiple environments (dev, stg, prd, plus a perf-specific config dev_perf). Each repo has 12–120 environment variables — Postgres credentials, Redis passwords (cache + bot), JWT secrets, OAuth client secrets, payment-provider API keys, KYC vendor tokens, SendGrid template ids, OpenTelemetry endpoints, etc.
Pre-Doppler state:
- Local development used
.envfiles copied from.example.env/.local.env. These templates lived in git; secrets were filled in manually per developer machine. - There was no shared canonical store of secrets. New engineers re-discovered values from Slack, README files, and live deployments.
- CI / Terraform had no programmatic access to secrets. The perf-test stack at
terraform/perf/could not bootstrap without manual env-file injection on each VM.
Pressures:
- Phase 2 perf-test bootstrap required containers to fetch secrets at runtime, not at build time. The
terraform/perf/secrets/directory needed scoped tokens for each project to pull only its own config. - Multi-environment hygiene — the same JWT secret in dev and prd is a known security anti-pattern. Secrets need per-environment isolation enforceable from outside the codebase.
- Operability — two-person team, limited capacity to operate Vault. The store must be turn-key.
Decision¶
- Use Doppler as the single source of truth for environment variables across all three repos.
- Workspace
ebit-devops(slug4d59619bf8c6f858715b) holds three projects:ebit-api,ebit-fe,ebit-admin-fe. - Per-project, per-config service tokens for runtime injection. Tokens are scoped to one project + one config; cannot read across boundaries.
- Containers run via
doppler run --token <service-token> -- <cmd>to inject env vars at process start. No.envfile lands on disk in production. - Service tokens land in
terraform/perf/secrets/(gitignored). Rotated on workspace lockout. - Local dev continues to use
.envfiles (developer convenience), with Doppler as the canonical source. Devsdoppler secrets download --format env > .envto refresh.
Considered alternatives¶
A. HashiCorp Vault (self-hosted)¶
The industry-standard secrets manager. Strong access policies, dynamic secrets, Kubernetes integration. Rejected on operational cost:
- Self-hosted Vault requires a Raft cluster (3+ nodes) for HA, or a single-node accepting downtime risk.
- Auto-unseal requires AWS KMS / GCP KMS integration — another moving part.
- Token / role / policy authoring is non-trivial; mistakes lock teams out.
- The team is two engineers; Vault ops would consume disproportionate capacity.
- Vault Cloud / HCP Vault would remove ops cost but at much higher $/month than Doppler at our scale.
B. AWS SSM Parameter Store¶
Native AWS service, free at low scale, KMS-encrypted, IAM-integrated. Rejected on UX + scoping:
- No per-config token model. Access is via IAM roles, which are per-IAM-principal not per-deployment. A leaked deployment role unlocks all parameters under its prefix.
- Weaker CLI ergonomics. Reading a config requires aware-of-prefix path queries; bulk diffs and
doppler runstyle env injection are unsupported natively (third-party wrappers exist). - No native multi-env model. You hand-roll the prefix scheme (
/ebit-api/dev/,/ebit-api/prd/, etc.); Doppler ships this out of the box. - AWS-only. The dev workstation, the FE Vercel deploy (when wired), and any non-AWS environment would require their own auth path.
C. AWS Secrets Manager¶
Sister product to SSM Parameter Store; first-class secret rotation. Rejected on cost: $0.40 per secret per month × ~140 secrets ≈ $56/mo before any GET costs, against Doppler's free tier for our workspace size. Same UX limitations as SSM.
D. Sealed .env files in git via SOPS / Mozilla age¶
Encrypt secrets at rest with KMS or age keys; decrypt at deploy time. Rejected on workflow cost:
- Every secret rotation requires a git commit.
- Bulk diffs across environments require unsealing first.
- Web UI for non-engineers (ops, security review) is awkward — they need the decryption key.
- No real ACL — anyone with the master key reads everything.
E. Doppler¶
Picked. Strengths:
- Per-project + per-config service tokens (
dp.st.dev_perf.*) — scoped tokens cannot read across configs or projects. doppler runCLI injects env vars cleanly at process start; works identically in local dev, Docker, and Terraform-provisioned VMs.- Free tier covers a single workspace with up to 5 users + small secret count — fits our team and footprint.
- Web UI is usable by non-engineers for sign-off (e.g. confirming that a JWT secret in
prdhas been rotated). - Branch configs (e.g.
dev_perfbranched fromdev) let us model perf-test-specific overrides without duplicating the whole config.
Trade-offs we accepted:
- Vendor dependency. Doppler going down or going through an acquisition would force a migration. Mitigation: weekly export of all secrets to encrypted local backup (
doppler secrets download --format env) so we can rebuild against any alternative provider. - Token sprawl. One service token per project per environment, plus the user's personal admin token, accumulates fast. Mitigation: tokens live in
secrets/(gitignored) and~/.config/doppler/token(chmod 600).
Consequences¶
Operational¶
- Workspace lockout is critical. Loss of admin access to
ebit-devopsis a P0. Mitigations: the personal admin token is stored at~/.config/doppler/token(recovered from.bashrcexport), and a copy of recentdoppler secrets --format jsonis held in encrypted local backup. - CI / Terraform must read service tokens from a non-git location.
terraform/perf/secrets/is gitignored; CI fetches tokens from Doppler-the-runtime via a bootstrap secret injected by the CI environment (chicken-and-egg solved by the CI provider's own secret store). - Local dev still uses
.envfor developer convenience. Treat the.envfile as a cache of Doppler — refresh weekly.
Security¶
- Per-token scoping means a leaked perf-test token cannot read prd. The blast radius of a leaked token is one config.
- Audit log — Doppler logs every read / write per token. Use it post-incident.
- Rotation — single-secret rotation is
doppler secrets set KEY=newvalue --project X --config prd; container restart picks up the new value at nextdoppler run.
Compliance / customer-facing¶
- Customers running Evospin on their own infra may prefer a different secrets provider (their own Vault, their AWS account's SSM). The
doppler runinjection model is replaceable — any tool that exposes env vars works the same. The architectural commitment is to env-var-injected secrets at process start, not to Doppler specifically. - For a customer-self-hosted deployment, document the secret schema (env-var names + which env each is required in) and let them fill it via their preferred provider. See
docs/recipes/customize-branding.mdanddocs/env-reference.md.
Known TBDs¶
- MCP-server wiring — Doppler ships an MCP server for AI workflows; not yet wired (
memoryreference_doppler_workspace.md``). When wired, agents can read/write secrets without leaking tokens to chat. - Runtime-token lifecycle —
dp.st.dev_perf.*tokens have no automatic expiry. Schedule a quarterly rotation reminder.
References¶
- Memory:
reference_doppler_workspace.md— workspace + project + token inventory. terraform/perf/secrets/— service token storage (gitignored).terraform/perf/.gitignore— confirmssecrets/exclusion..example.env/.local.envper repo — schema templates (no secret values).docs/env-reference.md— every env var across the three repos with description.docs/recipes/customize-branding.md§6 — references SendGrid template ids living in Doppler.- Doppler docs — Concepts.
- Sibling ADR: this is the secrets-management peer to the broader infra story; cross-link
docs/perf-run-checklist.mdfor the perf-test bootstrap that consumes the service tokens.