Polish-pass on top of the bulk-import rollout. Five contained items. #8 + #9 — Dexie v60 schema cleanup - Drop articleImportJobs.leasedBy + .leasedUntil. They were defined on the original v57 schema as a soft-lease handshake, but the worker uses pg_try_advisory_xact_lock and never wrote them. Local-* type + projection row stripped. - Drop the standalone `state` index on articleImportItems. [jobId+state] covers the worker's hot query; the state-solo index had no call site. Both changes lossless — Dexie just removes the column declarations from new rows; existing rows still carry the dead nulls (zombies) until the next full row-rewrite. Not worth a hard migration for two never-written columns. #15 — MAX_URLS_PER_JOB hard cap (200) articleImportsStore.createJob() throws if the URL list exceeds the cap. BulkImportForm surfaces the limit in the live counter chip and disables the submit when over. The worker can chew through any N, but at high counts the UI gets unwieldy (no virtualisation) and wall-clock duration climbs into multi-hour. 200 is a pragmatic ceiling — Pocket-export dumps average 50–150. #13 — Filter-Tabs in JobsList Pill-style tabs above the list: Alle / Aktiv / Fertig / Mit Fehlern, each with the row count. Disabled when the bucket is empty so the user only sees actionable filters. The "Mit Fehlern" filter (errorCount > 0) is the most valuable for triage. #18 — apps/mana/CLAUDE.md - Articles row added to the Tool Coverage table (5 propose + 1 auto, including the new auto-policy import_articles_from_urls). - New "Articles bulk-import" section after the AI Workbench part: pipeline diagram, table list, actor + metrics + cap pointers. #20 — ARTICLES_IMPORT_WORKER_DISABLED env var documented New row under "Mana API — Articles Bulk-Import Worker" in docs/ENVIRONMENT_VARIABLES.md. Plan: docs/plans/articles-bulk-import.md. Co-Authored-By: Claude Opus 4.7 (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 |
Mana API — Articles Bulk-Import Worker
| Variable | Description | Default |
|---|---|---|
ARTICLES_IMPORT_WORKER_DISABLED |
Set to true to skip starting the bulk-import worker on this apps/api instance. Useful for tests, or when running multiple apps/api replicas and you want to designate a specific one as the worker. The worker uses pg_try_advisory_xact_lock so multiple instances are safe by default — this env-var is the explicit opt-out. |
false |
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.