Environment Variable Hygiene
Best practices for managing environment variables across deployment platforms.
Triggers
Invoke when user mentions:
-
"env var", "environment variable", "production deploy"
-
"webhook secret", "API key", "token"
-
"Invalid character in header", "ERR_INVALID_CHAR"
-
"silent failure", "webhook not working"
-
"Vercel", "Convex", "deployment", "secrets"
Core Principles
- Trailing Whitespace Kills
Env vars with \n or trailing spaces cause cryptic errors:
-
"Invalid character in header content" (HTTP headers)
-
Webhook signature mismatch
-
Silent authentication failures
Root Cause: Copy-paste or echo introduces invisible characters.
Prevention:
✅ Use printf, not echo
printf '%s' 'sk_live_xxx' | vercel env add STRIPE_SECRET_KEY production
✅ Trim when setting
bunx convex env set --prod KEY "$(echo 'value' | tr -d '\n')" # or: npx convex ...
❌ Don't use echo directly
echo "sk_live_xxx" | vercel env add KEY production # May add \n
- Cross-Platform Parity
Shared tokens (webhook secrets, auth tokens) must be identical across platforms:
-
Vercel ↔ Convex
-
Frontend ↔ Backend
-
Dev ↔ Staging ↔ Prod (within each platform)
Common Pitfall: Set token on one platform, forget the other.
Prevention:
Generate token once
TOKEN=$(openssl rand -hex 32)
Set on ALL platforms
bunx convex env set --prod CONVEX_WEBHOOK_TOKEN "$(printf '%s' "$TOKEN")" # or: npx convex ... printf '%s' "$TOKEN" | vercel env add CONVEX_WEBHOOK_TOKEN production
- Validate Format Before Use
API keys have specific formats. Validate before deployment:
Service Pattern Example
Stripe Secret sk_(test|live)_[A-Za-z0-9]+
sk_live_xxx
Stripe Public pk_(test|live)_[A-Za-z0-9]+
pk_live_xxx
Stripe Webhook whsec_[A-Za-z0-9]+
whsec_xxx
Stripe Price price_[A-Za-z0-9]+
price_xxx
Clerk Secret sk_(test|live)_[A-Za-z0-9]+
sk_live_xxx
- Dev ≠ Prod
Separate deployments have separate env var stores:
-
Setting .env.local doesn't affect production
-
Convex dev and prod are separate deployments
-
Vercel has per-environment variables
Always verify prod separately:
Convex
bunx convex env list --prod # or: npx convex ...
Vercel
vercel env ls --environment=production
- CLI Environment Gotcha
CONVEX_DEPLOYMENT=prod:xxx npx convex data may return dev data.
Always use explicit flags:
❌ Unreliable
CONVEX_DEPLOYMENT=prod:xxx npx convex data
✅ Reliable
bunx convex run --prod module:function # or: npx convex ... bunx convex env list --prod
- Env Load Semantics (Restart Required)
Many runtimes load .env.local once at process start.
If code logs "missing X" but .env.local has X:
-
you edited .env.local after server started. Restart dev server.
-
your shell env overrides dotenv (incl empty string): printenv KEY then unset KEY .
Quick Reference
Setting Env Vars Safely
Convex:
Dev
bunx convex env set KEY "value" # or: npx convex ...
Prod (use --prod flag)
bunx convex env set --prod KEY "$(printf '%s' 'value')"
Vercel:
Production
printf '%s' 'value' | vercel env add KEY production
With explicit environment
vercel env add KEY production --force
Checking Env Vars
Convex:
bunx convex env list # dev bunx convex env list --prod # prod
Vercel:
vercel env ls # all vercel env ls --environment=production # prod only
Detecting Issues
Trailing whitespace:
Check Convex prod
bunx convex env list --prod | while IFS= read -r line; do if [[ "$line" =~ [[:space:]]$ ]]; then echo "WARNING: $(echo "$line" | cut -d= -f1) has trailing whitespace" fi done
Format validation:
Validate Stripe key format
value=$(bunx convex env list --prod | grep "^STRIPE_SECRET_KEY=" | cut -d= -f2-) [[ "$value" =~ ^sk_(test|live)_[A-Za-z0-9]+$ ]] || echo "Invalid format"
References
See references/ directory:
-
format-patterns.md
-
Regex patterns for common services
-
platform-specifics.md
-
Vercel, Convex, Railway platform details
-
hygiene-checklist.md
-
Pre-deployment validation checklist
-
parity-verification.md
-
Cross-platform token verification
Related Commands
-
/pre-deploy
-
Comprehensive pre-deployment checklist
-
/env-parity-check
-
Cross-platform token verification
-
/stripe-check
-
Stripe-specific environment audit
Based on 2026-01-17 incident: Trailing \n in STRIPE_SECRET_KEY caused "Invalid character in header" error. Token mismatch between Vercel and Convex caused silent webhook failures.