Managing Secrets Across Multiple Environments
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:
- Duplicate manually: Set the same value in each environment
- Script it: Write a shell script that sets common secrets across environments
- 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.