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 TypeExamplesRisk If Leaked
API KeysOpenAI, DataForSEO, AnthropicUnauthorized usage, charges
Database CredentialsSupabase service role keyFull data access
Auth SecretsJWT secret, session keyAccount hijacking
Third-party TokensGitHub, Railway tokensSystem 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:

  1. Go to your project in Railway
  2. Click on the service
  3. Go to "Variables" tab
  4. Add each secret

Railway secrets are:

  • Encrypted at rest
  • Injected at runtime
  • Never visible in logs
  • Shared across deployments

Per-Environment Secrets

EnvironmentWhere Secrets Live
Local.env.local file
StagingRailway staging service
ProductionRailway production service

Never copy production secrets to local development if you can avoid it.

Secret Rotation

When a secret might be compromised:

Rotation Checklist

  1. Generate new secret in the provider's dashboard
  2. Update in Railway (or other secret store)
  3. Verify app works with new secret
  4. Revoke old secret in provider's dashboard
  5. Check for exposure — search for the secret in logs, code

When to Rotate

EventAction
Team member leavesRotate secrets they had access to
Suspected compromiseRotate immediately
Secret in logsRotate immediately
AnnuallyGood practice for critical secrets

Common Mistakes

Committing secrets to Git

What happens: Secret is in Git history forever, even if you delete it.

Fix:

  1. Rotate the secret immediately (old one is compromised)
  2. Remove from code
  3. Add to .gitignore
  4. Consider using git-filter-repo to clean history

Prevention:

  • Use .env.example as 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

InsecureSecure
Slack/EmailPassword manager
Shared docRailway variables
ScreenshotEncrypted 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:

LevelExamplesProtection
CriticalProduction DB, payment APIMinimal access, rotation schedule
SensitiveAI API keys, auth secretsTeam access, environment separation
StandardAnalytics, loggingBroader 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

FileCommit?Contains
.env.exampleYesVariable names only
.env.localNoLocal dev secrets
.env.productionNoIf used, production secrets

Secret Exposure Response

  1. Assume compromised — don't hope it wasn't seen
  2. Rotate immediately — get new secret from provider
  3. Update everywhere — Railway, local, any copies
  4. Revoke old secret — in provider dashboard
  5. Audit — check for unauthorized usage

Provider Key Management

ProviderWhere to Manage Keys
OpenAIplatform.openai.com → API Keys
Anthropicconsole.anthropic.com → API Keys
SupabaseProject Settings → API
DataForSEOapp.dataforseo.com → API Access
RailwayProject → Variables