How We Manage Secrets
Environment variables, API keys, and keeping sensitive data secure
How We Manage Secrets
Secrets are the keys to our kingdoms: API keys, database passwords, authentication tokens. Leaking them can be catastrophic. This guide covers how we handle them safely.
Golden rule: Never commit secrets to Git. Ever.
What Are Secrets?
| Secret Type | Examples | Risk If Leaked |
|---|---|---|
| API Keys | OpenAI, DataForSEO, Anthropic | Unauthorized usage, charges |
| Database Credentials | Supabase service role key | Full data access |
| Auth Secrets | JWT secret, session key | Account hijacking |
| Third-party Tokens | GitHub, Railway tokens | System access |
Environment Variables
We store secrets in environment variables, not in code.
Local Development: .env.local
# .env.local (NEVER commit this file)
# Database
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# AI Services
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
# SEO APIs
DATAFORSEO_LOGIN=your@email.com
DATAFORSEO_PASSWORD=your-password
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
BETTER_AUTH_SECRET=random-32-char-string
.env.example (Commit This)
# .env.example (template for team, SAFE to commit)
# Database
SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# AI Services
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
# SEO APIs
DATAFORSEO_LOGIN=
DATAFORSEO_PASSWORD=
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
BETTER_AUTH_SECRET=
.gitignore
Ensure secrets are never committed:
# Environment files
.env
.env.local
.env.*.local
# Exception: template is safe
!.env.example
Accessing Secrets in Code
NextJS (Server-Side)
// Server components, API routes, server actions
const apiKey = process.env.ANTHROPIC_API_KEY;
// Validate secrets exist at startup
if (!process.env.SUPABASE_URL) {
throw new Error('Missing SUPABASE_URL environment variable');
}
NextJS (Client-Side)
Only NEXT_PUBLIC_* variables are exposed to the browser:
// Available in client code
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
// NOT available in client (undefined)
const apiKey = process.env.ANTHROPIC_API_KEY; // undefined!
FastAPI (Python)
import os
from dotenv import load_dotenv
load_dotenv() # Load from .env file
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise ValueError("Missing OPENAI_API_KEY")
Production Secrets
Railway
Railway has built-in secrets management:
- Go to your project in Railway
- Click on the service
- Go to "Variables" tab
- Add each secret
Railway secrets are:
- Encrypted at rest
- Injected at runtime
- Never visible in logs
- Shared across deployments
Per-Environment Secrets
| Environment | Where Secrets Live |
|---|---|
| Local | .env.local file |
| Staging | Railway staging service |
| Production | Railway production service |
Never copy production secrets to local development if you can avoid it.
Secret Rotation
When a secret might be compromised:
Rotation Checklist
- Generate new secret in the provider's dashboard
- Update in Railway (or other secret store)
- Verify app works with new secret
- Revoke old secret in provider's dashboard
- Check for exposure — search for the secret in logs, code
When to Rotate
| Event | Action |
|---|---|
| Team member leaves | Rotate secrets they had access to |
| Suspected compromise | Rotate immediately |
| Secret in logs | Rotate immediately |
| Annually | Good practice for critical secrets |
Common Mistakes
Committing secrets to Git
What happens: Secret is in Git history forever, even if you delete it.
Fix:
- Rotate the secret immediately (old one is compromised)
- Remove from code
- Add to .gitignore
- Consider using git-filter-repo to clean history
Prevention:
- Use
.env.exampleas template - Review diffs before committing
- Use pre-commit hooks to scan for secrets
Logging secrets
// WRONG: Logs the API key
console.log('Config:', { apiKey: process.env.API_KEY });
// WRONG: Full error might contain secrets
console.error('Failed:', error);
// RIGHT: Mask sensitive values
console.log('Config:', { apiKey: '***' });
console.error('Failed:', error.message); // Just message, not full object
Hardcoding secrets
// WRONG: Secret in code
const client = new OpenAI({ apiKey: 'sk-abc123...' });
// RIGHT: From environment
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
Sharing secrets insecurely
| Insecure | Secure |
|---|---|
| Slack/Email | Password manager |
| Shared doc | Railway variables |
| Screenshot | Encrypted file |
Using production secrets locally
Risk: Accidental data modification, unclear which environment you're affecting.
Fix: Use separate API keys for development when possible. Most providers allow multiple keys.
Secret Hierarchy
Not all secrets are equal:
| Level | Examples | Protection |
|---|---|---|
| Critical | Production DB, payment API | Minimal access, rotation schedule |
| Sensitive | AI API keys, auth secrets | Team access, environment separation |
| Standard | Analytics, logging | Broader access acceptable |
Validation at Startup
Fail fast if secrets are missing:
// lib/config.ts
function getEnvVar(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
export const config = {
supabase: {
url: getEnvVar('SUPABASE_URL'),
anonKey: getEnvVar('SUPABASE_ANON_KEY'),
serviceRoleKey: getEnvVar('SUPABASE_SERVICE_ROLE_KEY'),
},
anthropic: {
apiKey: getEnvVar('ANTHROPIC_API_KEY'),
},
// ...
};
This catches missing secrets at startup, not during runtime.
Secrets in CI/CD
GitHub Actions
Store secrets in GitHub repository settings:
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: railway up
Never Echo Secrets
# WRONG: Prints secret to logs
- run: echo ${{ secrets.API_KEY }}
# RIGHT: Use without printing
- run: some-command --key ${{ secrets.API_KEY }}
Evaluation Checklist
Your secrets management is working if:
- No secrets in Git history
- .env.local is gitignored
- .env.example documents all required vars
- Production secrets are in Railway, not files
- App fails fast if secrets missing
- Secrets never appear in logs
- You know how to rotate compromised secrets
Your secrets management needs work if:
- Secrets are in code or Git
- Team shares secrets via Slack
- No .env.example exists
- Same secrets used for dev and prod
- You're not sure where secrets are stored
- No rotation process exists
Quick Reference
Environment File Rules
| File | Commit? | Contains |
|---|---|---|
.env.example | Yes | Variable names only |
.env.local | No | Local dev secrets |
.env.production | No | If used, production secrets |
Secret Exposure Response
- Assume compromised — don't hope it wasn't seen
- Rotate immediately — get new secret from provider
- Update everywhere — Railway, local, any copies
- Revoke old secret — in provider dashboard
- Audit — check for unauthorized usage
Provider Key Management
| Provider | Where to Manage Keys |
|---|---|
| OpenAI | platform.openai.com → API Keys |
| Anthropic | console.anthropic.com → API Keys |
| Supabase | Project Settings → API |
| DataForSEO | app.dataforseo.com → API Access |
| Railway | Project → Variables |