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
| Service | Env File | Loader | Deploy Platform | Env Source (Prod) |
|---|---|---|---|---|
apps/web | apps/web/.env.local | Next.js auto-load | Vercel | Vercel dashboard |
apps/agents | apps/agents/.env.local | dotenv -e .env.local -- wrapper | Railway | Railway dashboard |
apps/messaging | apps/messaging/.env | Bun auto-load | Railway | Railway dashboard |
Key packages already installed:
dotenv@17.2.3(root dependency) - supports.env.vaultnativelydotenv-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:
| Tag | Meaning |
|---|---|
[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 VercelDev vs Prod — Complete Diff Reference
When pushing the production vault environment, these vars MUST be changed from their dev values:
| Variable | Dev Value | Prod Value | Hard Enforced? |
|---|---|---|---|
| URLs (section 4) | |||
NEXT_PUBLIC_APP_URL | http://localhost:3000 | https://your-domain.com | |
WEB_APP_URL | http://localhost:3000 | https://your-domain.com | Yes — crashes if not HTTPS |
APP_URL | http://localhost:3000 | https://your-domain.com | |
NEXT_PUBLIC_MASTRA_API_URL | http://localhost:4111 | https://api.your-domain.com | |
MASTRA_API_URL | http://localhost:4111 | https://api.your-domain.com | Yes — crashes if not HTTPS |
AGENTS_URL | http://localhost:4111 | https://api.your-domain.com | |
MESSAGING_GATEWAY_URL | http://localhost:4112 | Railway internal URL | |
| Google Redirects (section 5) | |||
GOOGLE_REDIRECT_URI | http://localhost:3000/api/auth/gmail/callback | https://your-domain.com/api/auth/gmail/callback | |
GOOGLE_UNIFIED_REDIRECT_URI | localhost variant | prod domain variant | |
All GOOGLE_*_REDIRECT_URI | localhost variants | prod domain variants | |
SLACK_REDIRECT_URI | localhost variant | prod domain variant | |
| Stripe (section 6) | |||
STRIPE_SECRET_KEY | sk_test_... | sk_live_... | Yes — crashes if test key in prod |
STRIPE_WEBHOOK_SECRET | dev endpoint secret | prod endpoint secret | |
| Feature Flags (section 17) | |||
ENABLE_BRIEF_DISPATCH | false | true | Skips dispatch if not true |
API_MEDIA_POLICY_MODE | warn | enforce | |
NODE_ENV | development | production | Controls all env-gated behavior |
| Storage (section 11) | |||
TURSO_DATABASE_URL | optional / local file | required | Yes — crashes if missing |
TURSO_AUTH_TOKEN | optional | required | Yes — crashes if missing |
| Dev-Only Vars | |||
GMAIL_PUBSUB_SKIP_VERIFY | true | omit entirely | Prod 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
| Aspect | Old (config/env.example) | New (root .env) |
|---|---|---|
| Scope info | Buried in comments at bottom | Inline [tag] on every var |
| Organization | By priority/service mix | By domain (consistent sections) |
| Missing vars | ~15 vars not documented | All 89 vars from turbo.json included |
| Duplicate docs | Separate checklists, security notes | Kept lean — reference config/env.example for setup guides |
| Secret marking | Inconsistent | Every secret tagged SECRET |
Files to Replace/Remove
- Delete
config/env.example— replaced by the root.envstructure (vault is source of truth, the.envitself serves as documentation when pulled) - Delete
apps/agents/.env.example— redundant - Keep
turbo.jsonglobalEnv— still needed for cache invalidation, should match the vars in the.env
Implementation Plan
Phase 1: Populate the Vault
- Create root
.envusing 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) - Connect to vault —
npx dotenv-vault@latest new vlt_ca047e22...(at repo root) - Login —
npx dotenv-vault@latest login - Push development env —
npx dotenv-vault@latest push(pushes root.envas "development") - 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_KEY→sk_live_...STRIPE_WEBHOOK_SECRET→ prod endpoint secretNODE_ENV=productionENABLE_BRIEF_DISPATCH=trueAPI_MEDIA_POLICY_MODE=enforceTURSO_DATABASE_URL+TURSO_AUTH_TOKEN→ prod Turso credentials- Remove
GMAIL_PUBSUB_SKIP_VERIFYentirely - All
*_REDIRECT_URI→ prod domain variants
- Push production env —
npx dotenv-vault@latest push production - Build vault file —
npx dotenv-vault@latest build(generates encrypted.env.vaultwith both envs) - Verify —
npx dotenv-vault@latest keysto 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 └──────────┬──────────────────┘
6 │
7 ┌────────────────┼────────────────┐
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:
| Variable | In Vault (production) | intelligence override | gateway override |
|---|---|---|---|
PORT | N/A (Railway assigns) | Set by Railway automatically | Set by Railway automatically |
MESSAGING_GATEWAY_URL | https://gateway.railway.internal | No override needed (agents reads this to call messaging) | Not used by messaging |
MASTRA_API_URL | https://intelligence.railway.internal | Not used by agents | No 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 productionAlso add dotenv as a dependency in apps/messaging/package.json.
Railway dashboard changes (both services):
- Set
DOTENV_KEY(fromnpx dotenv-vault keys production) — this is the decryption key - Keep
PORTas-is (Railway auto-manages this) - After verifying vault decryption works, remove all other individual env vars from dashboard
- 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_KEYneeded 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.mePhase 5: Cleanup & Documentation
- Remove per-app env files after verifying everything works
- Keep
config/env.exampleas documentation (describes all vars with comments) - Remove
apps/agents/.env.example(redundant with vault + config/env.example) - Update CLAUDE.md with new workflow:
- Setup:
npx dotenv-vault pullat root - Adding vars: edit root
.env,npx dotenv-vault push,npx dotenv-vault build - Deploy: commit
.env.vault, push, setDOTENV_KEYon platforms
- Setup:
Files to Modify
| File | Change |
|---|---|
.env (root, new) | Create consolidated env file using organized section layout above |
apps/agents/package.json | Update 6 dotenv paths from .env.local to ../../.env |
apps/web/package.json | Add dotenv -e ../../.env -- to dev and build scripts |
apps/messaging/package.json | Add dotenv -e ../../.env -- to dev, add dotenv dependency |
apps/messaging/src/index.ts | Add import "dotenv/config" as first line |
apps/agents/railway.json | Change startCommand to node -r dotenv/config .mastra/output/index.mjs |
package.json (root) | Add env:pull/push/build scripts, update test script dotenv paths |
.gitignore | Add !.env.vault exception and .env.me ignore |
.claude/CLAUDE.md | Update env setup docs |
Files to Delete
| File | Reason |
|---|---|
config/env.example | Replaced by organized root .env (pulled from vault) |
apps/agents/.env.example | Redundant with vault |
apps/web/.env.local | Replaced by root .env |
apps/agents/.env.local | Replaced by root .env |
apps/messaging/.env | Replaced by root .env |
Manual Steps (Not Code Changes)
Initial Setup
- Run
npx dotenv-vault@latest new vlt_ca047e22...at repo root - Consolidate all env vars from
apps/web/.env.local+apps/agents/.env.local+apps/messaging/.envinto a single root.env npx dotenv-vault loginnpx dotenv-vault push(pushes root.envas "development" environment)
Production Environment
- Create
.env.productionwith production values (copy root.env, update URLs/keys)- Set
MESSAGING_GATEWAY_URLto Railway gateway internal URL - Set
MASTRA_API_URLto Railway intelligence internal URL - Set
WEB_APP_URL/NEXT_PUBLIC_APP_URLto production domain - Set all production API keys (Stripe live keys, prod Sentry DSN, etc.)
- Set
npx dotenv-vault push productionnpx dotenv-vault build(generates encrypted.env.vault)- Commit
.env.vaultto repo
Platform Configuration
- Vercel:
npx dotenv-vault integrations→ set up "Sync to Vercel" (auto-syncs on push) - Railway (intelligence): Set
DOTENV_KEY= output ofnpx dotenv-vault keys production - Railway (gateway): Set same
DOTENV_KEYon the gateway service - Deploy and verify, then remove individual env vars from Railway dashboard
Verification
- Local dev: Delete per-app env files,
npx dotenv-vault pullat root,tilt up— all 3 services should start and pass health checks - Agents prod (intelligence): Push to Railway, check logs for
Loading env from encrypted .env.vault, verify/healthreturns 200 - Messaging prod (gateway): Push to Railway, verify
/healthreturns 200, test a webhook delivery - Web prod (Vercel): Deploy, verify
NEXT_PUBLIC_*vars render on client (check Supabase connection, PostHog) - Cross-service: Test an end-to-end flow (e.g., send iMessage) to confirm
MESSAGING_GATEWAY_URLandMASTRA_API_URLare correct - Encryption: Verify OAuth token decrypt works (proves
ENCRYPTION_KEYmatches 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
| Risk | Mitigation |
|---|---|
Railway cwd not at repo root for .env.vault | dotenv walks up directories; verify in Railway logs. Fallback: set DOTENV_CONFIG_PATH env var |
Vercel build needs NEXT_PUBLIC_* at build time | Vercel Sync integration pushes vars to dashboard natively — no runtime decryption needed |
Team uses stale local .env | Add 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 vars | Both URLs are in the same vault but only read by the service that needs them — no conflict |
| Rollback if vault decryption fails | Re-add individual env vars to Railway dashboard (keep a backup export) |