Local dev secrets like MANA_STT_API_KEY had no persistent home — they lived only in the gitignored, generator-overwritten per-app .env files. Every `pnpm setup:env` wiped them, so devs had to re-paste keys after any env regeneration. Same recurring friction for MANA_LLM_API_KEY, MANA_AUTH_KEK, OAuth keys, etc. New layer: `.env.secrets` at the repo root. - Gitignored, optional, never required for the build to pass - Read by generate-env.mjs AFTER .env.development; non-empty values override the matching key, so the merged result drives every per-app .env the generator writes - Empty values fall through to the .env.development defaults — a freshly-copied .env.secrets.example is a no-op - One source of truth for all dev secrets, propagated to every app with one `pnpm setup:env` Files: - `.env.secrets.example` — committed template documenting all known secret keys (mana-stt, mana-llm, auth KEK, sync JWT, MinIO, third- party APIs). Devs `cp .env.secrets.example .env.secrets` and fill in. - `.gitignore` — ignores .env.secrets, allows .env.secrets.example - `scripts/generate-env.mjs` — loads .env.secrets if present, prints "Loaded N secrets from .env.secrets" so devs see the override taking effect - `scripts/setup-secrets.mjs` + `pnpm setup:secrets` — convenience script that SSHes to mana-server, greps the prod .env for the keys defined in .env.secrets.example, and writes them locally. Confirms before overwriting an existing .env.secrets unless --force is set; reports which keys couldn't be found on the remote so devs know what's left to fill manually - `docs/LOCAL_DEVELOPMENT.md` + `docs/ENVIRONMENT_VARIABLES.md` — walk-through and architecture diagram update Verified end-to-end: - `rm .env.secrets apps/mana/apps/web/.env && pnpm setup:env` → STT key empty (no regression for devs who haven't opted in) - `pnpm setup:secrets --force && pnpm setup:env` → STT key propagated, "Loaded 3 secrets from .env.secrets" in output - POST /api/v1/voice/transcribe with a real audio file → full transcript back via gpu-stt.mana.how, end-to-end working Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Environment Variables Guide
This document explains the centralized environment variable system for the Mana monorepo.
Quick Start
# After cloning the repo, install dependencies (auto-generates .env files)
pnpm install
# Or manually generate .env files
pnpm setup:env
That's it! All app-specific .env files are generated from .env.development.
How It Works
.env.development # Central config (committed, no secrets)
│
├── .env.secrets # Optional gitignored override (your API keys)
▼
scripts/generate-env.mjs # Merges + transforms variables
│
▼
apps/**/apps/**/.env # Generated files (gitignored)
The generator reads .env.development first, then layers .env.secrets (if it exists) on
top — non-empty values in .env.secrets override the matching key in .env.development.
This is where personal dev secrets like MANA_STT_API_KEY live, so you don't have to
re-paste them into per-app .env files after every pnpm setup:env.
To populate .env.secrets from the Mac Mini in one shot, run pnpm setup:secrets (see
docs/LOCAL_DEVELOPMENT.md for
the full walk-through).
The generator then creates app-specific .env files with the correct prefixes for each platform:
| Platform | Prefix | Example |
|---|---|---|
| Expo (mobile) | EXPO_PUBLIC_ |
EXPO_PUBLIC_SUPABASE_URL |
| SvelteKit (web) | PUBLIC_ |
PUBLIC_SUPABASE_URL |
| Hono/Bun (server) | None | DATABASE_URL |
File Locations
Source File
.env.development- Single source of truth, committed to git
Generated Files (gitignored)
services/mana-auth/.envapps/chat/apps/server/.envapps/chat/apps/mobile/.envapps/chat/apps/web/.envapps/mana/apps/mobile/.envapps/mana/apps/web/.envapps/cards/apps/server/.envapps/cards/apps/web/.envapps/*/apps/server/.env(all apps with compute servers)apps/*/apps/web/.env(all web apps)apps/*/apps/mobile/.env(all mobile apps)
Variable Reference
Shared Variables
| Variable | Description | Used By |
|---|---|---|
MANA_AUTH_URL |
Auth service URL | All apps |
JWT_PRIVATE_KEY |
JWT signing key | mana-auth |
JWT_PUBLIC_KEY |
JWT verification key | All backends |
POSTGRES_USER |
Database user | Docker, backends |
POSTGRES_PASSWORD |
Database password | Docker, backends |
REDIS_HOST |
Redis host | mana-auth |
REDIS_PORT |
Redis port | mana-auth |
REDIS_PASSWORD |
Redis password | mana-auth |
Mana Auth Service
| Variable | Description | Default |
|---|---|---|
MANA_AUTH_PORT |
Service port | 3001 |
MANA_AUTH_DATABASE_URL |
PostgreSQL connection string | - |
JWT_ACCESS_TOKEN_EXPIRY |
Access token TTL | 15m |
JWT_REFRESH_TOKEN_EXPIRY |
Refresh token TTL | 7d |
JWT_ISSUER |
JWT issuer claim | mana |
JWT_AUDIENCE |
JWT audience claim | mana |
STRIPE_SECRET_KEY |
Stripe secret key | - |
STRIPE_PUBLISHABLE_KEY |
Stripe publishable key | - |
STRIPE_WEBHOOK_SECRET |
Stripe webhook secret | - |
CORS_ORIGINS |
Allowed CORS origins | - |
RATE_LIMIT_TTL |
Rate limit window (seconds) | 60 |
RATE_LIMIT_MAX |
Max requests per window | 100 |
Chat Project
| Variable | Description | Default |
|---|---|---|
CHAT_BACKEND_PORT |
Backend service port | 3002 |
CHAT_DATABASE_URL |
PostgreSQL connection string | - |
AZURE_OPENAI_ENDPOINT |
Azure OpenAI endpoint URL | - |
AZURE_OPENAI_API_KEY |
Azure OpenAI API key | - |
AZURE_OPENAI_API_VERSION |
API version | 2024-12-01-preview |
CHAT_SUPABASE_URL |
Supabase project URL | - |
CHAT_SUPABASE_ANON_KEY |
Supabase anonymous key | - |
Mana Project
| Variable | Description |
|---|---|
MANA_SUPABASE_URL |
Supabase project URL |
MANA_SUPABASE_ANON_KEY |
Supabase anonymous key |
Cards Project
| Variable | Description | Default |
|---|---|---|
CARDS_BACKEND_PORT |
Backend service port | 3004 |
CARDS_SUPABASE_URL |
Supabase project URL | - |
CARDS_SUPABASE_ANON_KEY |
Supabase anonymous key | - |
Speech-to-Text (mana-stt)
Used by the unified Mana web app's voice features (Memoro recording, Dreams voice capture, Notes
voice memos, Todo voice quick-add, etc). The browser never talks to mana-stt directly — requests
go through the SvelteKit server-side proxy at /api/v1/voice/transcribe which attaches the API
key from MANA_STT_API_KEY. Keep that key out of the browser bundle.
| Variable | Description | Default |
|---|---|---|
STT_URL |
Public mana-stt URL — generates MANA_STT_URL for the web app |
https://gpu-stt.mana.how |
MANA_STT_API_KEY |
API key for mana-stt. Never commit a real value. | (empty) |
Where to obtain a key:
- Production (Mac Mini):
MANA_STT_API_KEYis read from~/projects/mana-monorepo/.envon the Mac Mini and injected into themana-webcontainer bydocker-compose.macmini.yml(themana-webservice block, alongsideMANA_STT_URL=https://gpu-stt.mana.how). To rotate, update the.envvalue and recreate the container withdocker compose -f docker-compose.macmini.yml up -d --no-deps --force-recreate mana-web. - Local dev: paste the dev key into your local
apps/mana/apps/web/.envafter runningpnpm setup:env(the generator only writes an empty placeholder). Ask in#mana-devor pull from the team's password manager undermana-stt → web-key. - Source of truth:
services/mana-stt/.envon the Windows GPU box, in theAPI_KEYSvariable. Each entry is<random>:<name>and gets rate-limited per key. - Adding a new key: SSH to the Windows GPU box (
ssh mana-gpu), append a new entry toC:\mana\services\mana-stt\.envAPI_KEYS, restart theManaSTTscheduled task. Use a fresh key per consumer (mana-web,chat-server, etc.) so they can be revoked individually.
Endpoint: https://gpu-stt.mana.how — Cloudflare Tunnel mana-gpu-server (token-managed,
runs as a Windows Service on the GPU box, not as a route in the Mac Mini's cloudflared).
The tunnel terminates at localhost:3020 on the Windows host.
Health check:
curl https://gpu-stt.mana.how/health
# → {"status":"healthy","whisper_loaded":true,"whisperx":true,...}
If this returns 502, see "GPU Tunnel" in docs/MAC_MINI_SERVER.md for the standard
debug ladder.
LLM gateway (mana-llm)
Used by the unified Mana web app's voice quick-add features to turn transcripts into structured
data: /api/v1/voice/parse-task (todo titles + due dates + priorities) and /api/v1/voice/parse-habit
(habit picker for voice logging). Both proxies live server-side and degrade gracefully — if
mana-llm is unreachable or unauthorized, the endpoints return a fallback shape and voice quick-add
still works, just without LLM enrichment.
| Variable | Description | Default |
|---|---|---|
MANA_LLM_URL |
mana-llm gateway URL (server-side, never exposed) | http://localhost:3025 |
MANA_LLM_API_KEY |
API key — required when pointing at the GPU LLM proxy. Never commit a real value. | (empty) |
PUBLIC_MANA_LLM_URL |
Same URL exposed to the browser for direct use (status page, playground) | mirrors MANA_LLM_URL |
Local dev: leave MANA_LLM_URL=http://localhost:3025 and run mana-llm in Docker. If your local
mana-llm has no models loaded (curl http://localhost:3025/v1/models returns {"data":[]}), point
at the public proxy with MANA_LLM_URL=https://gpu-llm.mana.how and set MANA_LLM_API_KEY to a key
from services/mana-llm/.env on the GPU box.
Endpoints: http://localhost:3025 (Docker), https://llm.mana.how (Mac Mini, no auth),
https://gpu-llm.mana.how (GPU server, X-API-Key required).
Adding New Variables
Step 1: Add to .env.development
# In .env.development
MY_NEW_PROJECT_API_KEY=your-api-key
MY_NEW_PROJECT_URL=https://api.example.com
Step 2: Update the Generator Script
Edit scripts/generate-env.mjs and add your app config:
// In APP_CONFIGS array
{
path: 'apps/my-project/apps/mobile/.env',
vars: {
// For Expo, add EXPO_PUBLIC_ prefix
EXPO_PUBLIC_API_KEY: (env) => env.MY_NEW_PROJECT_API_KEY,
EXPO_PUBLIC_API_URL: (env) => env.MY_NEW_PROJECT_URL,
},
},
{
path: 'apps/my-project/apps/web/.env',
vars: {
// For SvelteKit, add PUBLIC_ prefix
PUBLIC_API_KEY: (env) => env.MY_NEW_PROJECT_API_KEY,
PUBLIC_API_URL: (env) => env.MY_NEW_PROJECT_URL,
},
},
{
path: 'apps/my-project/apps/server/.env',
vars: {
// For Hono/Bun servers, no prefix needed
API_KEY: (env) => env.MY_NEW_PROJECT_API_KEY,
API_URL: (env) => env.MY_NEW_PROJECT_URL,
},
},
Step 3: Regenerate
pnpm setup:env
Local Overrides
If you need to override variables locally without affecting others:
- The generated
.envfiles are gitignored - You can manually edit them after generation
- Or create
.env.localfiles (also gitignored) that some frameworks auto-load
Note: Running pnpm setup:env will overwrite your changes, so use .env.local for persistent overrides.
Docker Integration
The root .env.development is also used by Docker Compose:
# Start all services with shared env
pnpm docker:up:all
Docker services read from:
- Root
.env.developmentfor shared values - Service-specific
.envfiles for service-specific values
Troubleshooting
"Variable is undefined" Error
- Check if the variable exists in
.env.development - Run
pnpm setup:envto regenerate - Restart your dev server (env changes require restart)
Generated File Has Wrong Value
- Check the mapping in
scripts/generate-env.mjs - Ensure the source variable name matches exactly
- Run
pnpm setup:envagain
New App Not Getting Generated
- Add app config to
APP_CONFIGSinscripts/generate-env.mjs - Ensure the target directory exists
- Run
pnpm setup:env
Expo Not Picking Up Changes
Expo caches environment variables. Clear the cache:
cd apps/<project>/apps/mobile
npx expo start -c
Security Notes
.env.developmentcontains development-only values- Never put production secrets in this file
- The JWT keys in
.env.developmentare for local development only - Use separate secrets management for production (1Password, AWS Secrets Manager, etc.)
Migration from Old System
If you have existing .env files with real values:
- Copy important values to
.env.development - Delete the old
.envfiles - Run
pnpm setup:env - Verify apps still work
The old .env.example files can remain as documentation.