Architecture Guide
Local vs Production Secrets
Your development database and your production database should not share credentials. Your local Stripe key should be a test key, not a live one. Your staging API endpoint should point to a sandbox, not the real service. This guide covers how to structure environment-specific secrets and manage them without losing your mind.
Why They Should Be Different
Using the same secrets across environments is convenient until it causes an incident. There are three concrete reasons to keep them separate:
Security isolation
If a developer's laptop is compromised, the attacker gets access to development credentials — not production. This is the difference between a security incident and a catastrophe.
Data isolation
A bug in development code running against a production database can corrupt real user data. Separate databases with separate credentials make this structurally impossible.
Third-party sandboxes
Services like Stripe, Twilio, and SendGrid provide test keys that behave like real ones but don't charge money or send real messages. Using live keys in development wastes money and sends real emails to test addresses.
The Environment Model
Most applications need three environments. Some need more, but three covers the vast majority of use cases:
| Environment | Purpose | What goes here |
|---|---|---|
| development | Local developer machines | Local database URL, test API keys (Stripe test, SendGrid sandbox), localhost callback URLs |
| staging | Pre-production testing, CI pipelines | Staging database, test API keys with higher limits, staging service URLs, CI-specific tokens |
| production | Live application serving real users | Production database, live API keys, real webhook URLs, production signing secrets |
The key insight: the same DATABASE_URL variable exists in every environment, but with a different value. Your application code references process.env.DATABASE_URL everywhere — the environment determines which actual database it connects to.
Common Anti-Patterns
These mistakes are common and often invisible until something breaks:
Same database for dev and production
A developer runs a migration locally and it drops a column in the production database. Or test data ends up visible to real users. This is surprisingly common in early-stage teams.
Copying production secrets to local dev
A developer copies production credentials to debug an issue, forgets to switch back, and the .env file with production secrets gets committed to Git.
No staging environment
Code goes directly from a developer's laptop to production. There is no intermediate step to catch configuration errors, missing secrets, or environment-specific bugs.
Hardcoded environment detection
Code like if (NODE_ENV === "production") ? liveKey : testKey is brittle. It bakes environment logic into application code and makes it impossible to add new environments without code changes.
Environment Management with secr
secr creates three default environments when you initialize a project: development, staging, and production. Each environment has its own set of secrets, its own access controls, and its own audit trail.
Setting secrets per environment
# Development secrets (local dev machines)
secr set DATABASE_URL="postgresql://localhost:5432/myapp_dev" -e development
secr set STRIPE_SECRET_KEY="sk_test_abc123" -e development
secr set APP_URL="http://localhost:3000" -e development
# Staging secrets (CI and preview deploys)
secr set DATABASE_URL="postgresql://staging-db.example.com:5432/myapp" -e staging
secr set STRIPE_SECRET_KEY="sk_test_xyz789" -e staging
secr set APP_URL="https://staging.myapp.com" -e staging
# Production secrets (live application)
secr set DATABASE_URL="postgresql://prod-db.example.com:5432/myapp" -e production
secr set STRIPE_SECRET_KEY="sk_live_real_key" -e production
secr set APP_URL="https://myapp.com" -e productionPulling secrets for a specific environment
# Pull development secrets (default when no -e flag is specified)
secr pull
# Pull staging secrets
secr pull -e staging
# Pull production secrets (requires admin or owner role)
secr pull -e productionRunning your app with the right environment
# Local development — uses development secrets by default
secr run -- npm dev
# Run against staging for testing
secr run -e staging -- npm dev
# Production (typically done in CI/CD, not locally)
secr run -e production -- node dist/server.jsTip: Add environment-specific scripts to your package.json so the whole team uses consistent commands:
{
"scripts": {
"dev": "secr run -- next dev",
"dev:staging": "secr run -e staging -- next dev",
"test": "secr run -e staging -- vitest",
"start": "secr run -e production -- node dist/server.js"
}
}Promoting Secrets Between Environments
When you test a new API key in staging and it works, you need to move it to production. secr provides a promotion command that copies a secret's value from one environment to another, with a full audit trail:
# Promote a single secret from staging to production
secr promote STRIPE_WEBHOOK_SECRET --from staging --to production
# Promote from development to staging after local testing
secr promote NEW_API_KEY --from development --to stagingPromotion is a deliberate, audited action. It is different from setting the same value in two places — the audit log records that the production value came from staging, who promoted it, and when.
When not to promote: Some secrets should never be the same across environments. Database URLs, signing secrets, and encryption keys should always be unique per environment. Promotion is for values like API keys, webhook endpoints, and feature flags.
Environment-Specific CI/CD
Different pipelines should use different environments. Here is a common pattern for GitHub Actions:
name: CI/CD
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# Use staging secrets for tests
- name: Pull staging secrets
uses: secr-dev/secr-action@v1
with:
token: ${{ secrets.SECR_TOKEN }}
org: my-org
project: my-app
environment: staging
- run: npm ci
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Use production secrets for deployment
- name: Pull production secrets
uses: secr-dev/secr-action@v1
with:
token: ${{ secrets.SECR_DEPLOY_TOKEN }}
org: my-org
project: my-app
environment: production
- run: npm ci
- run: npm run build
- run: npm run deployNotice the use of different tokens: SECR_TOKEN for CI (staging access only) and SECR_DEPLOY_TOKEN for deployment (production access). This enforces least privilege — even if the CI token is exposed, it cannot access production secrets.
Access Control by Environment
Environment separation is only useful if access is controlled per environment. secr ties roles to environment visibility:
| Role | development | staging | production |
|---|---|---|---|
| Viewer | Read | Read | No access |
| Developer | Read / Write | Read / Write | No access |
| Admin | Read / Write | Read / Write | Read / Write |
| Owner | Full control | Full control | Full control |
This means a junior developer can run the app locally with secr run -- npm dev and get development secrets, but they cannot access production credentials even if they try. The access boundary is enforced by the server, not by trust.
Comparing Environments
A common source of bugs is a secret that exists in development but is missing in production. Use secr's pull command to compare environments side by side:
# List all keys in development
secr pull -e development --format keys
# List all keys in production
secr pull -e production --format keys
# Quickly diff the two
diff <(secr pull -e development --format keys) \
<(secr pull -e production --format keys)The dashboard also provides an environment comparison view that shows which keys exist in each environment and highlights any mismatches — useful before a release to verify that all required secrets are in place.
Get environment separation right from the start
secr creates development, staging, and production environments automatically. Set secrets per environment, control access by role, and promote values with a single command.
npm i -g @secr/cli
secr init
secr set DATABASE_URL="..." -e development
secr set DATABASE_URL="..." -e production
secr run -- npm dev