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:

EnvironmentPurposeWhat goes here
developmentLocal developer machinesLocal database URL, test API keys (Stripe test, SendGrid sandbox), localhost callback URLs
stagingPre-production testing, CI pipelinesStaging database, test API keys with higher limits, staging service URLs, CI-specific tokens
productionLive application serving real usersProduction 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

Terminal
# 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 production

Pulling secrets for a specific environment

Terminal
# 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 production

Running your app with the right environment

Terminal
# 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.js

Tip: Add environment-specific scripts to your package.json so the whole team uses consistent commands:

package.json
{
  "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:

Terminal
# 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 staging

Promotion 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:

.github/workflows/ci.yml
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 deploy

Notice 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:

Roledevelopmentstagingproduction
ViewerReadReadNo access
DeveloperRead / WriteRead / WriteNo access
AdminRead / WriteRead / WriteRead / Write
OwnerFull controlFull controlFull 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:

Terminal
# 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