Skip to content

Secrets Management Strategy

Status: Active / Golden
Decision Date: 2026-01-18
Scope: All environments (local, Elastic Muscle, CI/CD, production)


🎯 Core Principle

ALL secrets MUST be managed through Doppler.

This is a non-negotiable architectural decision to ensure: - ✅ Single source of truth - ✅ Consistent across all environments - ✅ Secure (no credentials in code/scripts) - ✅ Easy to rotate - ✅ Auditable


🚫 Anti-Patterns (DO NOT DO)

❌ Manual Environment Variable Syncing

# WRONG - Don't do this!
ssh server "echo 'export SECRET=value' >> ~/.zshrc"

❌ Hardcoded Secrets in Scripts

# WRONG - Don't do this!
export SENTRY_DSN="https://..."
npx tsx script.ts

❌ .env Files in Version Control

# WRONG - Don't do this!
git add .env

❌ Passing Secrets as Arguments

# WRONG - Don't do this!
./script.sh --token="secret-value"

✅ Correct Patterns

1. Local Development

# Load secrets from Doppler
doppler run -- pnpm dev

# Or for one-off commands
doppler run -- npx tsx scripts/some-script.ts

2. Elastic Muscle (Remote Server)

# Setup once (installs Doppler CLI and configures project)
./scripts/setup-elastic-muscle.sh

# Then always use Doppler
ssh elastic-muscle
cd singular-dream
doppler run -- npx tsx scripts/refactor-directory.ts

3. Dispatch to Remote

# Dispatch script automatically uses Doppler
./scripts/dispatch-to-elastic-muscle.sh refactor-directory-overnight

4. CI/CD (GitHub Actions, Vercel, etc.)

# GitHub Actions
- name: Run tests
  env:
    DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
  run: doppler run -- pnpm test

📋 Setup Checklist

Local Machine

  • [x] Doppler CLI installed (brew install dopplerhq/cli/doppler)
  • [x] Authenticated (doppler login)
  • [x] Project configured (doppler setup)
  • [x] All scripts use doppler run --

Elastic Muscle

  • [x] Doppler CLI installed (via setup-elastic-muscle.sh)
  • [x] Service token configured in ~/.zshrc
  • [x] Project configured (doppler setup --project singular-dream --config dev)
  • [x] All scripts use doppler run --

CI/CD

  • [ ] DOPPLER_TOKEN added to GitHub Secrets
  • [ ] DOPPLER_TOKEN added to Vercel environment variables
  • [ ] All workflows use doppler run --

🔐 Doppler Configuration

Project Structure

singular-dream (project)
├── dev (config)
│   ├── SENTRY_DSN
│   ├── UPSTASH_REDIS_REST_URL
│   ├── UPSTASH_REDIS_REST_TOKEN
│   ├── UPSTASH_VECTOR_REST_URL
│   ├── UPSTASH_VECTOR_REST_TOKEN
│   ├── UPSTASH_SEARCH_REST_URL
│   ├── UPSTASH_SEARCH_REST_TOKEN
│   └── ... (all other secrets)
├── stg (config)
│   └── ... (same secrets, staging values)
└── prd (config)
    └── ... (same secrets, production values)

Access Tokens

Personal Token (for local development): - Used for: doppler login - Scope: Read/write access to all projects - Location: Stored by Doppler CLI

Service Token (for servers/CI): - Used for: Elastic Muscle, GitHub Actions, Vercel - Scope: Read-only access to specific config (e.g., dev) - Location: Environment variable DOPPLER_TOKEN


🛠️ Common Operations

Adding a New Secret

# Add via CLI
doppler secrets set NEW_SECRET="value" --config dev

# Or via dashboard
# https://dashboard.doppler.com/workplace/singular-dream/projects/singular-dream/configs/dev

Rotating a Secret

# Update in Doppler
doppler secrets set UPSTASH_REDIS_REST_TOKEN="new-token" --config dev

# Changes are immediately available everywhere
# No need to restart services (Doppler injects at runtime)

Viewing Secrets

# List all secrets
doppler secrets

# View specific secret
doppler secrets get SENTRY_DSN

# Download all as .env (for debugging)
doppler secrets download --no-file --format env

🏗️ Integration Examples

TypeScript Script

// scripts/some-script.ts
// NO NEED to import dotenv or read .env files
// Doppler injects all secrets as environment variables

const sentryDsn = process.env.SENTRY_DSN;
const redisUrl = process.env.UPSTASH_REDIS_REST_URL;

// Just use them directly!

Shell Script

#!/bin/bash
# scripts/some-script.sh

# NO NEED to source .env or export variables
# Doppler injects all secrets as environment variables

echo "Sentry DSN: $SENTRY_DSN"
echo "Redis URL: $UPSTASH_REDIS_REST_URL"

# Just use them directly!

Next.js

// next.config.mjs
// Doppler injects secrets at build time

export default {
  env: {
    // These are automatically available from Doppler
    NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
  },
};

🚨 Emergency Procedures

If Doppler is Down

  1. DO NOT hardcode secrets as a workaround
  2. DO use Doppler's fallback mode:
    doppler secrets download --no-file --format env > .env.emergency
    source .env.emergency
    
  3. DO remove .env.emergency after Doppler is back up

If Service Token is Compromised

  1. Revoke the token in Doppler dashboard
  2. Generate a new service token
  3. Update DOPPLER_TOKEN in:
  4. Elastic Muscle ~/.zshrc
  5. GitHub Secrets
  6. Vercel environment variables

📚 References

  • Doppler Dashboard: https://dashboard.doppler.com/workplace/singular-dream
  • Doppler Docs: https://docs.doppler.com
  • Setup Script: scripts/setup-elastic-muscle.sh
  • Dispatch Script: scripts/dispatch-to-elastic-muscle.sh

⚖️ Enforcement

This is a Golden Architecture decision. Any deviation requires: 1. Documented justification 2. Architecture review 3. Update to this ADR

Violations will be flagged in code review.


🎯 Benefits Realized

Benefit Before After
Secret Rotation Manual, error-prone One update in Doppler
Environment Parity Drift between local/remote Guaranteed identical
Onboarding Share .env files doppler login + doppler setup
Audit Trail None Full history in Doppler
Security Secrets in scripts/files Never in code

Last Updated: 2026-01-18
Next Review: When adding new environments or services