Configuration System
Env var inventory across web (Vercel), voice (EC2), and mobile (EAS). Security classification, cron definition, rotation runbook, known gaps.
Status: Current as of 2026-04-18 (repo commit 06d84cb).
Audience: New contributors setting up a dev environment; ops engineer rotating secrets; BfArM reviewer (security classification).
This document enumerates every environment variable across the three fragJulia runtimes, flags .env.example gaps, and classifies each secret by exposure scope.
1. Runtimes at a glance
| Runtime | Platform | Config source |
|---|---|---|
| Web | Vercel (Next.js 16) | Vercel dashboard env + apps/web/.env.example for dev |
| Voice | EC2 g6.xlarge (eu-central-1) | /root/knotencheck-voice.env + IAM instance role (Bedrock) |
| Mobile | Expo / EAS | apps/mobile/eas.json env per channel (dev / preview / production) |
Cron jobs: apps/web/vercel.json defines one β /api/chat/cleanup at 0 3 * * * UTC, guarded by CRON_SECRET.
2. Web (Vercel) β 22 vars in apps/web/.env.example
Exposure legend:
- π server-only β never reach the browser
- π client-exposed β prefixed
NEXT_PUBLIC_*, visible in client bundle
Supabase (4)
| Var | Scope | Required | Purpose |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | π | β | Project REST endpoint |
NEXT_PUBLIC_SUPABASE_ANON_KEY | π | β | Client-side anon role |
SUPABASE_SERVICE_ROLE_KEY | π | β | Admin / RLS-bypass (used by audit logger, cron cleanup, admin routes) |
SUPABASE_JWT_SECRET | π | β | Present in .env.example but no active process.env.SUPABASE_JWT_SECRET consumer as of last sweep β verify before removing |
Site URLs (2 + 1 optional)
| Var | Scope | Required |
|---|---|---|
NEXT_PUBLIC_SITE_URL | π | β |
NEXT_PUBLIC_APP_URL | π | β |
NEXT_PUBLIC_DEV_SUPABASE_REDIRECT_URL | π | dev-only (auth callback on localhost) |
Stripe (4)
| Var | Scope | Required |
|---|---|---|
STRIPE_SECRET_KEY | π | β
(prod: sk_live_*) |
STRIPE_WEBHOOK_SECRET | π | β
β signature verification of /api/stripe/webhook |
STRIPE_PRICE_PLUS_MONTHLY | π | β |
STRIPE_PRICE_PREMIUM_MONTHLY | π | β |
Resend (1)
| Var | Scope | Required |
|---|---|---|
RESEND_API_KEY | π | β β transactional + newsletter email |
LiveKit β web caller (3)
| Var | Scope | Required |
|---|---|---|
LIVEKIT_URL | π | β β WebRTC gateway (wss://) |
LIVEKIT_API_KEY | π | β |
LIVEKIT_API_SECRET | π | β β used to mint session tokens for voice pipeline |
Knotencheck test mode (2)
| Var | Scope | Required |
|---|---|---|
KNOTENCHECK_TEST_SECRET | π | β test bypass |
NEXT_PUBLIC_KNOTENCHECK_TEST_SECRET | π | β must match server value |
Security note: this pair intentionally lets a test UI bypass auth. Do not enable in a production-public environment. Consider gating on NODE_ENV !== 'production'.
Upstash Redis (rate limits) (2)
| Var | Scope | Required |
|---|---|---|
KV_REST_API_URL | π | β |
KV_REST_API_TOKEN | π | β |
Security (2)
| Var | Scope | Required |
|---|---|---|
CRON_SECRET | π | β β cron route guard |
HMAC_SECRET | π | β β signed-link / webhook payloads |
Known gaps (still missing from .env.example as of 2026-04-18)
- Turnstile (planned in #358):
TURNSTILE_SITE_KEY,TURNSTILE_SECRET_KEY - Bedrock (planned in #358):
AWS_REGION=eu-central-1+ access via IAM role for web runtime, or explicitAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYif not using instance role - OpenAI (used today):
OPENAI_API_KEYis implicit (AI SDK reads it from env) but not documented in.env.exampleβ fix.
3. Voice (EC2) β 5 vars in voice/.env.example
Live file: /root/knotencheck-voice.env on the EC2 host. Committed example: voice/.env.example.
| Var | Required | Purpose |
|---|---|---|
LIVEKIT_API_KEY | β | Generated via docker run --rm livekit/livekit-server generate-keys |
LIVEKIT_API_SECRET | β | Paired with API key |
MISTRAL_API_KEY | β | Bridge / fallback while Voxtral license is pending |
VOXTRAL_VOICE_ID | β | Default julia_knotencheck |
DEEPGRAM_API_KEY | β | Legacy β kept for STT fallback; verify still in use before rotating |
Hardcoded in voice/docker-compose.yml (not env-var driven)
LIVEKIT_URL=ws://localhost:7880LLAMA_GUARD_ENDPOINT=http://localhost:8000/v1VOXTRAL_LOCAL_ENDPOINT=http://localhost:8001/v1FASTER_WHISPER_MODEL_PATH=/models/faster-whisper-large-v3NVIDIA_VISIBLE_DEVICES=all
If you move off host networking or relocate model files, these move into env.
Bedrock access
No env var β the voice-agent container assumes the EC2 instance's IAM role has bedrock:InvokeModel on eu-central-1. Document IAM role name + policy when BfArM reviewer asks.
Caddy / TLS
Cert renewal state in the caddy_data named volume (see docker-compose). No env vars.
4. Mobile (Expo / EAS) β 3 vars per environment
Source: apps/mobile/eas.json. No .env.example file in apps/mobile/ β env is configured per-channel in eas.json and injected at build time by EAS.
| Var | Scope | Channels |
|---|---|---|
EXPO_PUBLIC_API_URL | π client | dev β staging.fragjulia.de, preview β staging.fragjulia.de, production β fragjulia.de |
EXPO_PUBLIC_SUPABASE_URL | π client | dev β staging-supabase.fragjulia.de, preview β staging-supabase.fragjulia.de, production β supabase.fragjulia.de |
APP_ENV | runtime | development / staging / production |
Note: EXPO_PUBLIC_SUPABASE_ANON_KEY is not in eas.json. Either consumed from a separate secrets source (EAS Secrets) or missing. Verify with native client before shipping next mobile release.
iOS / Android identifiers (in app.json, not env)
- Bundle identifier:
de.fragjulia.app - Expo scheme:
fragjulia:// - Microphone usage string (German, DSGVO-relevant)
Submit credentials (in eas.json.submit.production.ios)
All REPLACE_WITH_* placeholders as of 2026-04-18:
appleId,ascAppId,appleTeamIdβ fill before first store submission
5. Cron
Only one, defined in apps/web/vercel.json:
{
"crons": [
{ "path": "/api/chat/cleanup", "schedule": "0 3 * * *" }
]
}- Schedule: 03:00 UTC daily
- Guard: route handler checks
authorization: Bearer ${CRON_SECRET}header (Vercel sets this automatically when invoking scheduled cron) - Purpose: prune old chat messages past tier-specific retention (7 days for free, unlimited for plus/premium)
6. Security classification summary
| Class | Count (web) | Examples |
|---|---|---|
| π Server-only secrets | 15 | SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, RESEND_API_KEY, LIVEKIT_API_SECRET, CRON_SECRET, HMAC_SECRET, β¦ |
| π Client-exposed (public) | 7 | NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_SITE_URL, site/app URLs, test-mode key |
Rules of thumb:
- Anything
NEXT_PUBLIC_*ends up in the JS bundle. Never put secrets there. - Anything
EXPO_PUBLIC_*ends up in the RN bundle + OTA update payloads. Same rule. - Keep Stripe price IDs as server-only β they're not secrets cryptographically but moving them client-side makes price changes much harder to revert.
7. Gaps to fix (tracked here, should become follow-up issues)
- Mobile
.env.examplemissing. Createapps/mobile/.env.exampleeven if most env lives ineas.jsonβ documents the shape of EAS secrets. OPENAI_API_KEYundocumented. Present in production (chat route uses it), missing from.env.example. Adds onboarding friction.SUPABASE_JWT_SECRETpotentially dead. No obvious consumer β verify then either remove or document its use.- Turnstile vars planned (#358) β must land in
.env.exampleon same PR that introduces the runtime dependency. - Voice
.env.examplelacks Bedrock config notes. Even if the live path uses IAM roles, document the expected IAM policy + fallbackAWS_*vars for local dev. - EAS submit placeholders.
appleId/ascAppId/appleTeamIdβ fill before first App Store submission.
8. How to rotate a secret (runbook)
- Generate the new value (platform-specific β Stripe dashboard, Supabase settings,
openssl rand -base64 32for HMAC-like) - Update Vercel project settings (all environments: production, preview, development) β NOT
.env.example - For voice (EC2): edit
/root/knotencheck-voice.env, restartvoice-agentcontainer (docker compose restart voice-agent) - For mobile: update EAS Secret via
eas secret:createor web UI; trigger a new build - Log the rotation in whatever audit path applies (future:
audit.config_rotationevent β see audit-system) - Revoke the previous value at the provider only after confirming the new one works in each environment
9. Related
| # | Relevance |
|---|---|
| #579 | Parent docs epic |
| #586 | Pillar B parent |
| #358 | Adds Turnstile + Bedrock vars (see Β§2 gaps) |
apps/web/.env.example | Source for Β§2 |
voice/.env.example | Source for Β§3 |
apps/mobile/eas.json | Source for Β§4 |
apps/web/vercel.json | Source for Β§5 |
Changelog
- 2026-04-18 β Initial version. Counts differ from
bubbly-bubbling-quilt.mdΒ§2: web has 22 vars (quilt said 30+), voice has 5 (quilt said 8), mobile has 3 per EAS channel (quilt said 5). Turnstile still absent,OPENAI_API_KEYstill undocumented. Single cron confirmed at 03:00 UTC.
Subscription, Tiers & Feature Gating
Tier definitions (free/plus/premium), TIER_FEATURES map, FeatureGate contract, server-side enforcement, Stripe integration, Supabase schema.
API Documentation
17 route domains, auth patterns, rate limiting, tier-gating matrix, mobile-app surface, streaming pointer, security headers. No per-endpoint table β see Β§9.