Back to Blog
best-practicestutorialenvironments

Managing Secrets Across Multiple Environments

October 28, 2024 5 min read | By Redshift Team
Share:

If you've ever truncated a production table because your local app was pointed at the wrong database, you already know why this matters. The fix isn't complicated, but most teams don't set up proper environment separation until after something goes wrong.

The Multi-Environment Problem

The root issue is that DATABASE_URL means something completely different depending on context. Locally, it's your throwaway Postgres container. In CI, it's a shared test instance. In production, it's the thing that page-alerts you at 3am when it goes down. Same variable name, wildly different consequences if you mix them up.

Beyond the obvious "dev pointed at prod" disaster, environments tend to drift apart quietly. Someone updates a key in staging but forgets production. A new hire spends half a day figuring out which Slack thread has the current dev credentials. Six months in, nobody's confident that any two environments actually match.

Redshift Environment Model

Redshift keeps each environment in its own namespace under a project. The structure looks like this:

Project
├── development
│   ├── DATABASE_URL
│   ├── API_KEY
│   └── DEBUG=true
├── staging
│   ├── DATABASE_URL
│   ├── API_KEY
│   └── DEBUG=false
└── production
    ├── DATABASE_URL
    ├── API_KEY
    └── DEBUG=false

Same secret names, different values, no chance of cross-contamination. The -e flag is the only thing that determines which set you get.

Setting Up Environments

The setup wizard walks you through it. You can always add more later, but starting with the environments you know you need saves you from the "I'll organize this eventually" trap:

$ redshift setup
? Project name: my-api
? Create environment: development
? Create another environment? yes
? Create environment: staging
? Create another environment? yes
? Create environment: production
? Create another environment? no

Created project "my-api" with 3 environments

Adding Secrets Per Environment

The -e flag targets a specific environment. Nothing surprising here, but seeing it spelled out makes the isolation concrete:

# Development - local database, verbose logging
$ redshift secrets set DATABASE_URL postgres://localhost:5432/myapp_dev -e development
$ redshift secrets set LOG_LEVEL debug -e development

# Staging - shared test database
$ redshift secrets set DATABASE_URL postgres://staging.db.internal:5432/myapp -e staging
$ redshift secrets set LOG_LEVEL info -e staging

# Production - production database, minimal logging
$ redshift secrets set DATABASE_URL postgres://prod.db.internal:5432/myapp -e production
$ redshift secrets set LOG_LEVEL warn -e production

Running with Environment Secrets

This is the part your scripts and CI configs actually care about:

# Local development
$ redshift run -e development -- npm run dev

# Test against staging
$ redshift run -e staging -- npm test

# Production deployment (in CI)
$ redshift run -e production -- npm start

Environment Naming Conventions

Stick to consistent, predictable names:

Environment Purpose Access
development Local development All developers
test Automated testing CI systems
staging Pre-production testing Dev team
production Live system Restricted

Sharing Secrets Across Environments

Sometimes a secret genuinely is the same everywhere -- a third-party API key that auto-detects the environment, or a shared analytics token. You have a few options:

  1. Duplicate manually: Set the same value in each environment
  2. Script it: Write a shell script that sets common secrets across environments
  3. Use inheritance: Coming in a future Redshift release

Duplication is annoying but at least it's explicit. We're building inheritance so you can define base values that environments override selectively, but that's not shipped yet.

Preventing Environment Mistakes

The goal here is to make it hard to accidentally run against the wrong environment. No single trick solves this, but a few habits stacked together make it pretty unlikely.

Use Descriptive Values

If your database URL contains the word "staging" and you see it in a production log, that's an instant red flag. Bake the environment name into values wherever it makes sense:

# Include environment in URLs where possible
DATABASE_URL=postgres://localhost:5432/myapp_development
DATABASE_URL=postgres://staging.internal:5432/myapp_staging

Require Explicit Environment

This one matters more than people think. If redshift run without a -e flag defaulted to production, you'd eventually fat-finger it. Redshift requires you to specify, and you should do the same in your npm scripts:

# In package.json scripts
"start:prod": "redshift run -e production -- node server.js"

Auditing Environment Access

Redshift Cloud includes 7-day audit logs showing who accessed which environment, when secrets were read or modified, and what changed. Useful when you're trying to figure out why staging broke after someone "didn't touch anything."

On the free tier, you can use separate Nostr identities per environment tier for coarse access control.

Migration from .env Files

If you're sitting on a pile of .env.development, .env.staging, .env.production files, the migration is mechanical:

# Import each environment
$ redshift secrets upload .env.development -e development
$ redshift secrets upload .env.staging -e staging
$ redshift secrets upload .env.production -e production

# Then delete the .env files (they contain plaintext secrets!)
$ rm .env.*

Your secrets are now encrypted and accessible from anywhere, not just machines with the right dotfiles.

One Last Thing

If you take one thing from this post: set up the separation before you need it. It takes five minutes during redshift setup and saves you from the 2am "wait, which database is this connected to" panic. The quickstart covers the full flow, or just run redshift setup and follow the prompts.

Ready to try Redshift?

Own your secrets with decentralized, censorship-resistant secret management.