MDX Limo
Plan: Consolidate Env Vars with dotenv-vault

Plan: Consolidate Env Vars with dotenv-vault

Context

The monorepo has 3 services (web, agents, messaging) each with their own .env files, plus Vercel and Railway dashboards with separately-managed secrets. This creates 5 places to keep ~89 env vars in sync, with critical coupling (ENCRYPTION_KEY must match across all services). New team members must manually set up 3 env files. The goal: one .env.vault at root as the single source of truth, with npx dotenv-vault pull as the only local setup step.

Vault project already exists: vlt_ca047e220ceda5dd255bcf562c69cd8aa4bc5b054a852bc01d468b53047d9f88


Current State

ServiceEnv FileLoaderDeploy PlatformEnv Source (Prod)
apps/webapps/web/.env.localNext.js auto-loadVercelVercel dashboard
apps/agentsapps/agents/.env.localdotenv -e .env.local -- wrapperRailwayRailway dashboard
apps/messagingapps/messaging/.envBun auto-loadRailwayRailway dashboard

Key packages already installed:

  • dotenv@17.2.3 (root dependency) - supports .env.vault natively
  • dotenv-cli@11.0.0 (root devDependency) - used by agents scripts

Root .env Organization Strategy

The consolidated root .env replaces 3 per-app files and config/env.example. It uses a consistent notation system so any developer can quickly find, understand, and modify variables.

Notation Conventions

Each variable gets a scope tag in its inline comment showing which service reads it:

TagMeaning
[all]Read by all 3 services
[web]Only apps/web
[agents]Only apps/agents
[gateway]Only apps/messaging
[web,agents]Read by web and agents
[agents,gateway]Read by agents and gateway
[platform]Auto-injected by deployment platform (not set manually)

Secrets are tagged # SECRET in their comment. Public/client-safe vars are tagged # PUBLIC.

Section Layout

The file is organized by domain (not by service), with sections ordered from most critical to most optional. Vars that differ between dev and prod are marked DEV/PROD to flag them for the vault push.

Environment marker convention:

  • No marker = same value in dev and prod (push once, done)
  • DEV/PROD = value differs between environments (must set separately in each vault env)
1# ══════════════════════════════════════════════════════════════════ 2# CONSUL AGENT — ENVIRONMENT CONFIGURATION 3# ══════════════════════════════════════════════════════════════════ 4# Single source of truth for all services (web, agents, gateway). 5# 6# Setup: npx dotenv-vault pull (download dev from vault) 7# Push: npx dotenv-vault push (upload dev changes) 8# Build: npx dotenv-vault build (rebuild .env.vault) 9# 10# Scope tags: [web] [agents] [gateway] [all] [platform] 11# DEV/PROD = value differs between environments 12# SECRET = sensitive, never expose to client 13# PUBLIC = safe for client-side / browser 14# ══════════════════════════════════════════════════════════════════ 15 16# ── 1. CORE DATABASE (Supabase) ────────────────────────────────── 17NEXT_PUBLIC_SUPABASE_URL= # [all] PUBLIC — same project in both envs 18NEXT_PUBLIC_SUPABASE_ANON_KEY= # [web,agents] PUBLIC 19NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= # [web] PUBLIC — alias for anon key 20SUPABASE_SERVICE_ROLE_KEY= # [agents,gateway] SECRET — full DB access, server-only 21 22# ── 2. AI / LLM ────────────────────────────────────────────────── 23OPENAI_API_KEY= # [agents] SECRET — same key both envs 24 25# ── 3. SECURITY & ENCRYPTION ───────────────────────────────────── 26ENCRYPTION_KEY= # [all] SECRET — AES-256-GCM, 64 hex chars, MUST match across services 27GATEWAY_SECRET= # [all] SECRET — inter-service auth (web↔agents↔gateway), 32+ chars in prod 28JWT_SECRET= # [web,agents] SECRET 29OAUTH_STATE_SECRET= # [web,agents] SECRET — OAuth CSRF protection 30OAUTH_METRICS_SECRET= # [agents] SECRET 31ARTIFACT_TOKEN_SECRET= # [web] SECRET — artifact verification (fallback: GATEWAY_SECRET) 32ONBOARDING_TOKEN_SECRET= # [web] SECRET — signup/referral HMAC 33CRON_SECRET= # [web] SECRET — scheduled job auth 34MESSAGING_GATEWAY_SECRET= # [gateway] SECRET 35 36# ── 4. SERVICE URLS ─────────────────────────────────────────────── 37# These MUST differ between dev (localhost) and prod (HTTPS required in prod). 38NEXT_PUBLIC_APP_URL=http://localhost:3000 # [web,agents] PUBLIC — DEV/PROD 39WEB_APP_URL=http://localhost:3000 # [agents,gateway] DEV/PROD — must be https:// in prod (enforced) 40APP_URL=http://localhost:3000 # [gateway] DEV/PROD — alias for WEB_APP_URL 41NEXT_PUBLIC_MASTRA_API_URL=http://localhost:4111 # [web] PUBLIC — DEV/PROD 42MASTRA_API_URL=http://localhost:4111 # [gateway] DEV/PROD — must be https:// in prod (enforced) 43AGENTS_URL=http://localhost:4111 # [gateway] DEV/PROD — for Supabase edge functions 44MESSAGING_GATEWAY_URL=http://localhost:4112 # [agents,web] DEV/PROD — Railway internal URL in prod 45 46# ── 5. GOOGLE OAUTH ────────────────────────────────────────────── 47GOOGLE_CLIENT_ID= # [web,agents] PUBLIC — same app both envs 48GOOGLE_CLIENT_SECRET= # [web,agents] SECRET — same app both envs 49GOOGLE_CLOUD_PROJECT_ID= # [agents,gateway] — GCP project 50GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/gmail/callback # [web,agents] DEV/PROD 51GOOGLE_UNIFIED_REDIRECT_URI=http://localhost:3000/api/auth/google-unified/callback # [web,agents] DEV/PROD 52GOOGLE_CALENDAR_REDIRECT_URI= # [agents] DEV/PROD 53GOOGLE_CONTACTS_REDIRECT_URI= # [agents] DEV/PROD 54GOOGLE_DOCS_REDIRECT_URI= # [agents] DEV/PROD 55GOOGLE_DRIVE_REDIRECT_URI= # [agents] DEV/PROD 56GRANOLA_REDIRECT_URI= # [agents] DEV/PROD 57 58# ── 6. STRIPE (Billing) ────────────────────────────────────────── 59STRIPE_SECRET_KEY= # [web] SECRET — DEV/PROD (sk_test_ in dev, sk_live_ in prod — HARD ENFORCED) 60STRIPE_WEBHOOK_SECRET= # [web] SECRET — DEV/PROD (different endpoint per env) 61STRIPE_PROMO_LINK= # [web] 62STRIPE_TEST_LINK= # [web] 63STRIPE_METERED_PRICE_ID= # [web] 64STRIPE_METERED_CREDITS_PRICE_ID= # [web] 65 66# ── 7. ONBOARDING & PRICING ────────────────────────────────────── 67ONBOARDING_PRICE_ANCHOR_CENTS=20000 # [web] 68ONBOARDING_PRICE_CURRENT_CENTS=5000 # [web] 69ONBOARDING_PRICE_CURRENCY=usd # [web] 70ONBOARDING_PRICE_INTERVAL=month # [web] 71ONBOARDING_PRICE_BADGE=Founding member pricing # [web] 72ONBOARDING_FOUNDER_DEMO_ENABLED=false # [web] DEV/PROD — false in dev (safety), explicitly set in prod 73ONBOARDING_FOUNDER_DEMO_TO= # [web] 74 75# ── 8. MESSAGING (Photon / iMessage / SMS) ─────────────────────── 76PHOTON_SERVER_URL= # [agents,gateway] 77PHOTON_NUMBER= # [agents,gateway] 78PHOTON_LOG_LEVEL=info # [agents,gateway] 79PHOTON_SESSION_TTL_MINUTES=5 # [agents,gateway] 80CONSUL_VERIFY_NUMBER= # [web] 81CONSUL_IMESSAGE_NUMBER= # [web] 82 83# ── 9. EMAIL (AgentMail) ───────────────────────────────────────── 84AGENTMAIL_API_KEY= # [agents,gateway] SECRET 85AGENTMAIL_INBOX_ID= # [agents,gateway] 86AGENTMAIL_POD_ID= # [agents] 87AGENTMAIL_WEBHOOK_SECRET= # [gateway] SECRET 88 89# ── 10. GMAIL WEBHOOKS ─────────────────────────────────────────── 90GMAIL_PUBSUB_TOPIC= # [agents] 91GMAIL_PUBSUB_SKIP_VERIFY=true # [gateway] DEV ONLY — omit in prod (verification always on) 92 93# ── 11. STORAGE (Turso / Redis) ────────────────────────────────── 94TURSO_DATABASE_URL= # [agents] DEV/PROD — optional in dev (falls back to file:./local.db), REQUIRED in prod 95TURSO_AUTH_TOKEN= # [agents] SECRET — DEV/PROD — REQUIRED in prod 96UPSTASH_REDIS_REST_URL= # [agents,gateway] 97UPSTASH_REDIS_REST_TOKEN= # [agents,gateway] SECRET 98 99# ── 12. WORKFLOWS (Inngest) ────────────────────────────────────── 100INNGEST_EVENT_KEY= # [agents] SECRET — not needed for local dev (SDK auto-connects localhost:8288) 101INNGEST_SIGNING_KEY= # [agents] SECRET — prod only 102 103# ── 13. SLACK ───────────────────────────────────────────────────── 104SLACK_CLIENT_ID= # [agents] 105SLACK_CLIENT_SECRET= # [agents] SECRET 106SLACK_FEEDBACK_WEBHOOK_URL= # [agents] 107SLACK_REDIRECT_URI= # [agents] DEV/PROD 108SLACK_SIGNING_SECRET= # [agents] SECRET 109 110# ── 14. CLAY ────────────────────────────────────────────────────── 111CLAY_WEBHOOK_URL= # [agents] 112CLAY_WEBHOOK_SECRET= # [agents] SECRET 113 114# ── 15. OBSERVABILITY (Sentry / PostHog) ───────────────────────── 115# Note: Sentry sampling rates are code-controlled (100% dev, 10-20% prod) via NODE_ENV check 116SENTRY_DSN= # [all] 117SENTRY_AUTH_TOKEN= # [web] SECRET — source map upload 118SENTRY_ORG= # [web] 119SENTRY_PROJECT= # [web] 120NEXT_PUBLIC_POSTHOG_HOST= # [web] PUBLIC 121NEXT_PUBLIC_POSTHOG_KEY= # [web] PUBLIC 122POSTHOG_API_KEY= # [web] SECRET — server-side 123POSTHOG_HOST= # [web,agents] 124POSTHOG_PUBLIC_KEY= # [web] 125 126# ── 16. SECURITY (Cloudflare Turnstile) ────────────────────────── 127TURNSTILE_SECRET_KEY= # [web] SECRET 128NEXT_PUBLIC_TURNSTILE_SITE_KEY= # [web] PUBLIC 129 130# ── 17. FEATURE FLAGS & DEV ────────────────────────────────────── 131ENABLE_BRIEF_DISPATCH=false # [agents] DEV/PROD — false in dev (safety), true in prod 132API_MEDIA_POLICY_MODE=warn # [gateway] DEV/PROD — warn in dev, enforce in prod 133OUTBOUND_PRIVATE_HOSTS= # [all] — network policy 134NODE_ENV=development # [all] DEV/PROD — development | production 135 136# ── 18. PLATFORM-INJECTED (do not set manually) ────────────────── 137# VERCEL_GIT_COMMIT_SHA= # [platform] auto-set by Vercel 138# RAILWAY_GIT_COMMIT_SHA= # [platform] auto-set by Railway 139# CI= # [platform] auto-set in CI 140# PORT= # [platform] auto-set by Railway 141# NEXT_RUNTIME= # [platform] auto-set by Vercel

Dev vs Prod — Complete Diff Reference

When pushing the production vault environment, these vars MUST be changed from their dev values:

VariableDev ValueProd ValueHard Enforced?
URLs (section 4)
NEXT_PUBLIC_APP_URLhttp://localhost:3000https://your-domain.com
WEB_APP_URLhttp://localhost:3000https://your-domain.comYes — crashes if not HTTPS
APP_URLhttp://localhost:3000https://your-domain.com
NEXT_PUBLIC_MASTRA_API_URLhttp://localhost:4111https://api.your-domain.com
MASTRA_API_URLhttp://localhost:4111https://api.your-domain.comYes — crashes if not HTTPS
AGENTS_URLhttp://localhost:4111https://api.your-domain.com
MESSAGING_GATEWAY_URLhttp://localhost:4112Railway internal URL
Google Redirects (section 5)
GOOGLE_REDIRECT_URIhttp://localhost:3000/api/auth/gmail/callbackhttps://your-domain.com/api/auth/gmail/callback
GOOGLE_UNIFIED_REDIRECT_URIlocalhost variantprod domain variant
All GOOGLE_*_REDIRECT_URIlocalhost variantsprod domain variants
SLACK_REDIRECT_URIlocalhost variantprod domain variant
Stripe (section 6)
STRIPE_SECRET_KEYsk_test_...sk_live_...Yes — crashes if test key in prod
STRIPE_WEBHOOK_SECRETdev endpoint secretprod endpoint secret
Feature Flags (section 17)
ENABLE_BRIEF_DISPATCHfalsetrueSkips dispatch if not true
API_MEDIA_POLICY_MODEwarnenforce
NODE_ENVdevelopmentproductionControls all env-gated behavior
Storage (section 11)
TURSO_DATABASE_URLoptional / local filerequiredYes — crashes if missing
TURSO_AUTH_TOKENoptionalrequiredYes — crashes if missing
Dev-Only Vars
GMAIL_PUBSUB_SKIP_VERIFYtrueomit entirelyProd always verifies

Everything else (Supabase, OpenAI, encryption keys, secrets, Photon, AgentMail, Redis, Inngest, Slack, Clay, Sentry, PostHog, Turnstile, pricing config) uses the same value in both environments.

What Changes from config/env.example

AspectOld (config/env.example)New (root .env)
Scope infoBuried in comments at bottomInline [tag] on every var
OrganizationBy priority/service mixBy domain (consistent sections)
Missing vars~15 vars not documentedAll 89 vars from turbo.json included
Duplicate docsSeparate checklists, security notesKept lean — reference config/env.example for setup guides
Secret markingInconsistentEvery secret tagged SECRET

Files to Replace/Remove

  • Delete config/env.example — replaced by the root .env structure (vault is source of truth, the .env itself serves as documentation when pulled)
  • Delete apps/agents/.env.example — redundant
  • Keep turbo.json globalEnv — still needed for cache invalidation, should match the vars in the .env

Implementation Plan

Phase 1: Populate the Vault

  1. Create root .env using the organized structure above, merging actual dev values from the 3 per-app env files (localhost URLs, test Stripe key, NODE_ENV=development, feature flags off)
  2. Connect to vaultnpx dotenv-vault@latest new vlt_ca047e22... (at repo root)
  3. Loginnpx dotenv-vault@latest login
  4. Push development envnpx dotenv-vault@latest push (pushes root .env as "development")
  5. Create .env.production — Copy root .env, then change the ~20 DEV/PROD vars per the diff table above:
    • All URLs → HTTPS production domains
    • STRIPE_SECRET_KEYsk_live_...
    • STRIPE_WEBHOOK_SECRET → prod endpoint secret
    • NODE_ENV=production
    • ENABLE_BRIEF_DISPATCH=true
    • API_MEDIA_POLICY_MODE=enforce
    • TURSO_DATABASE_URL + TURSO_AUTH_TOKEN → prod Turso credentials
    • Remove GMAIL_PUBSUB_SKIP_VERIFY entirely
    • All *_REDIRECT_URI → prod domain variants
  6. Push production envnpx dotenv-vault@latest push production
  7. Build vault filenpx dotenv-vault@latest build (generates encrypted .env.vault with both envs)
  8. Verifynpx dotenv-vault@latest keys to see decryption keys for each env

Phase 2: Update Local Dev Scripts

All services will read from the single root .env instead of per-app files.

apps/agents/package.json - Update dotenv paths:

1- "dev": "dotenv -e .env.local -- mastra dev" 2+ "dev": "dotenv -e ../../.env -- mastra dev" 3 4- "start:build": "pnpm exec dotenv -e .env.local -- sh -c '...'" 5+ "start:build": "pnpm exec dotenv -e ../../.env -- sh -c '...'" 6 7- "populate-sales-knowledge": "dotenv -- tsx ..." 8+ "populate-sales-knowledge": "dotenv -e ../../.env -- tsx ..." 9 10- "delete-threads": "dotenv -- tsx ..." 11+ "delete-threads": "dotenv -e ../../.env -- tsx ..." 12 13- "photon-relay": "dotenv -- tsx ..." 14+ "photon-relay": "dotenv -e ../../.env -- tsx ..." 15 16- "test:tokens": "dotenv -e .env.local -- tsx ..." 17+ "test:tokens": "dotenv -e ../../.env -- tsx ..."

apps/web/package.json - Add dotenv wrapper:

1- "dev": "next dev --turbopack" 2+ "dev": "dotenv -e ../../.env -- next dev --turbopack" 3 4- "build": "pnpm --filter @consul/shared build && next build" 5+ "build": "pnpm --filter @consul/shared build && dotenv -e ../../.env -- next build"

apps/messaging/package.json - Add dotenv wrapper:

1- "dev": "bun run --watch src/index.ts" 2+ "dev": "dotenv -e ../../.env -- bun run --watch src/index.ts"

Root package.json - Update test scripts:

1- "test:tokens": "tsx scripts/test-token-refresh.ts" 2+ "test:tokens": "dotenv -e .env -- tsx scripts/test-token-refresh.ts"

(Same for test:connections, test:auth, test:my, renew:watches)

Add convenience scripts to root package.json:

1"env:pull": "npx dotenv-vault@latest pull", 2"env:push": "npx dotenv-vault@latest push", 3"env:build": "npx dotenv-vault@latest build"

Phase 3: Update Production Deployments

How Env Vars Flow to Each Deployment Target

1dotenv-vault (source of truth) 2 ┌─────────────────────────────┐ 3 │ development env (89 vars) │ 4 │ production env (89 vars) │ 5 └──────────┬──────────────────┘ 67 ┌────────────────┼────────────────┐ 8 ▼ ▼ ▼ 9 ┌────────────┐ ┌─────────────┐ ┌─────────────┐ 10 │ Vercel │ │ Railway │ │ Railway │ 11 │ (web) │ │ (intelligence)│ │ (gateway) │ 12 ├────────────┤ ├─────────────┤ ├─────────────┤ 13 │ Auto-sync │ │ DOTENV_KEY │ │ DOTENV_KEY │ 14 │ integration│ │ decrypts │ │ decrypts │ 15 │ pushes all │ │ .env.vault │ │ .env.vault │ 16 │ vars to │ │ at runtime │ │ at runtime │ 17 │ dashboard │ │ │ │ │ 18 │ │ │ + overrides:│ │ + overrides:│ 19 │ │ │ PORT=8080 │ │ PORT=8080 │ 20 │ │ │ (Railway │ │ (Railway │ 21 │ │ │ sets this) │ │ sets this) │ 22 └────────────┘ └─────────────┘ └─────────────┘

Service-Specific Env Vars (Railway Overrides)

The vault contains ALL vars with their production values. However, 3 vars have different values per Railway service because they describe inter-service routing:

VariableIn Vault (production)intelligence overridegateway override
PORTN/A (Railway assigns)Set by Railway automaticallySet by Railway automatically
MESSAGING_GATEWAY_URLhttps://gateway.railway.internalNo override needed (agents reads this to call messaging)Not used by messaging
MASTRA_API_URLhttps://intelligence.railway.internalNot used by agentsNo override needed (messaging reads this to call agents)

Key insight: These URL vars don't actually conflict — MESSAGING_GATEWAY_URL is only read by agents, and MASTRA_API_URL is only read by messaging. So a single vault with both values works fine. PORT is auto-assigned by Railway and overrides any env var.

The vault production env should contain both:

1MESSAGING_GATEWAY_URL=https://<gateway-railway-internal-url> 2MASTRA_API_URL=https://<intelligence-railway-internal-url>

If Railway internal URLs change, update the vault and rebuild.

Railway Setup (intelligence = agents, gateway = messaging)

apps/agents/railway.json - Preload dotenv for vault decryption:

1- "startCommand": "node .mastra/output/index.mjs" 2+ "startCommand": "node -r dotenv/config .mastra/output/index.mjs"

apps/messaging/src/index.ts - Add dotenv import as first line:

1import "dotenv/config"; // Must be first - decrypts .env.vault in production

Also add dotenv as a dependency in apps/messaging/package.json.

Railway dashboard changes (both services):

  1. Set DOTENV_KEY (from npx dotenv-vault keys production) — this is the decryption key
  2. Keep PORT as-is (Railway auto-manages this)
  3. After verifying vault decryption works, remove all other individual env vars from dashboard
  4. Rollback plan: If vault decryption fails, Railway services will crash. Re-add individual env vars to restore service.

Vercel Setup (web)

  • Set up dotenv-vault's "Sync to Vercel" integration: npx dotenv-vault@latest integrations
  • This auto-syncs vault vars to Vercel's dashboard on every vault push
  • NEXT_PUBLIC_* vars are available at build time (Vercel injects them natively)
  • No code changes needed for Vercel — it gets individual env vars via the sync, same as today
  • No DOTENV_KEY needed on Vercel — the sync handles everything

Phase 4: Update .gitignore

1# local env files 2 .env 3 .env.local 4+# dotenv-vault (encrypted, safe to commit) 5+!.env.vault 6+# dotenv-vault auth (personal, never commit) 7+.env.me

Phase 5: Cleanup & Documentation

  1. Remove per-app env files after verifying everything works
  2. Keep config/env.example as documentation (describes all vars with comments)
  3. Remove apps/agents/.env.example (redundant with vault + config/env.example)
  4. Update CLAUDE.md with new workflow:
    • Setup: npx dotenv-vault pull at root
    • Adding vars: edit root .env, npx dotenv-vault push, npx dotenv-vault build
    • Deploy: commit .env.vault, push, set DOTENV_KEY on platforms

Files to Modify

FileChange
.env (root, new)Create consolidated env file using organized section layout above
apps/agents/package.jsonUpdate 6 dotenv paths from .env.local to ../../.env
apps/web/package.jsonAdd dotenv -e ../../.env -- to dev and build scripts
apps/messaging/package.jsonAdd dotenv -e ../../.env -- to dev, add dotenv dependency
apps/messaging/src/index.tsAdd import "dotenv/config" as first line
apps/agents/railway.jsonChange startCommand to node -r dotenv/config .mastra/output/index.mjs
package.json (root)Add env:pull/push/build scripts, update test script dotenv paths
.gitignoreAdd !.env.vault exception and .env.me ignore
.claude/CLAUDE.mdUpdate env setup docs

Files to Delete

FileReason
config/env.exampleReplaced by organized root .env (pulled from vault)
apps/agents/.env.exampleRedundant with vault
apps/web/.env.localReplaced by root .env
apps/agents/.env.localReplaced by root .env
apps/messaging/.envReplaced by root .env

Manual Steps (Not Code Changes)

Initial Setup

  1. Run npx dotenv-vault@latest new vlt_ca047e22... at repo root
  2. Consolidate all env vars from apps/web/.env.local + apps/agents/.env.local + apps/messaging/.env into a single root .env
  3. npx dotenv-vault login
  4. npx dotenv-vault push (pushes root .env as "development" environment)

Production Environment

  1. Create .env.production with production values (copy root .env, update URLs/keys)
    • Set MESSAGING_GATEWAY_URL to Railway gateway internal URL
    • Set MASTRA_API_URL to Railway intelligence internal URL
    • Set WEB_APP_URL / NEXT_PUBLIC_APP_URL to production domain
    • Set all production API keys (Stripe live keys, prod Sentry DSN, etc.)
  2. npx dotenv-vault push production
  3. npx dotenv-vault build (generates encrypted .env.vault)
  4. Commit .env.vault to repo

Platform Configuration

  1. Vercel: npx dotenv-vault integrations → set up "Sync to Vercel" (auto-syncs on push)
  2. Railway (intelligence): Set DOTENV_KEY = output of npx dotenv-vault keys production
  3. Railway (gateway): Set same DOTENV_KEY on the gateway service
  4. Deploy and verify, then remove individual env vars from Railway dashboard

Verification

  1. Local dev: Delete per-app env files, npx dotenv-vault pull at root, tilt up — all 3 services should start and pass health checks
  2. Agents prod (intelligence): Push to Railway, check logs for Loading env from encrypted .env.vault, verify /health returns 200
  3. Messaging prod (gateway): Push to Railway, verify /health returns 200, test a webhook delivery
  4. Web prod (Vercel): Deploy, verify NEXT_PUBLIC_* vars render on client (check Supabase connection, PostHog)
  5. Cross-service: Test an end-to-end flow (e.g., send iMessage) to confirm MESSAGING_GATEWAY_URL and MASTRA_API_URL are correct
  6. Encryption: Verify OAuth token decrypt works (proves ENCRYPTION_KEY matches across services)

Ongoing Workflow

1Adding a new var (same value in dev & prod): 2 1. Add to root .env 3 2. pnpm env:push (syncs dev to vault) 4 3. npx dotenv-vault open production (opens UI to add to prod too) 5 OR: add to .env.production && npx dotenv-vault push production 6 4. pnpm env:build (rebuilds .env.vault with both envs) 7 5. Add to turbo.json globalEnv (cache invalidation) 8 6. git commit .env.vault turbo.json && git push 9 10Adding a new var (DIFFERENT value in dev & prod): 11 1. Add to root .env with dev value 12 2. pnpm env:push (syncs dev to vault) 13 3. Add to .env.production with prod value 14 4. npx dotenv-vault push production (syncs prod to vault) 15 5. pnpm env:build (rebuilds .env.vault) 16 6. Add to turbo.json globalEnv 17 7. git commit .env.vault turbo.json && git push 18 19Changing an existing var: 20 1. Edit root .env (dev) and/or .env.production (prod) 21 2. Push the changed env(s): pnpm env:push / npx dotenv-vault push production 22 3. pnpm env:build && git commit .env.vault && git push 23 24New team member setup: 25 1. Clone repo 26 2. npx dotenv-vault login 27 3. npx dotenv-vault pull (gets dev .env at root) 28 4. tilt up (all services read root .env)

Risks

RiskMitigation
Railway cwd not at repo root for .env.vaultdotenv walks up directories; verify in Railway logs. Fallback: set DOTENV_CONFIG_PATH env var
Vercel build needs NEXT_PUBLIC_* at build timeVercel Sync integration pushes vars to dashboard natively — no runtime decryption needed
Team uses stale local .envAdd env:pull to dev startup or document clearly
Vault service downtime.env.vault is committed to repo — no runtime dependency on vault service, only on DOTENV_KEY
Railway service-specific URL varsBoth URLs are in the same vault but only read by the service that needs them — no conflict
Rollback if vault decryption failsRe-add individual env vars to Railway dashboard (keep a backup export)
Plan: Consolidate Env Vars with dotenv-vault | MDX Limo