pediatric-ai-scribe-v3/.env.example
Daniel a0d81789ff
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 2m2s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
feat: My Resources — anyone can generate teaching material, privately
Learning is moderator-owned: content published into categories that everyone
sees, behind router.use(moderatorMiddleware). That is right for institutional
material and wrong as the only way in — an ordinary user could not generate
anything at all.

So this is a separate pathway rather than a loosening of that one. Learning is
untouched; the moderator gate stays exactly where it was. A signed-in user can
generate a deck or an article for their own use, keep it, refine it and export
it, and nobody else ever sees it.

Private by construction. Every statement filters on the owner and there is no
route that returns another person's work, which a test asserts statement by
statement rather than trusting. The foreign key cascades, so deleting an account
takes its drafts with it. There is no category, no publish state and no sharing:
adding sharing later should be a deliberate feature, not something that leaks
out of a forgotten WHERE clause.

Markdown is the artifact. Every format is rendered from it on demand — pptx and
docx by pandoc, both carrying the house reference deck, and PDF by Gotenberg,
whose LibreOffice preserves a deck's layout in a way rendering from markdown
would not. That is what makes "add a slide on when to admit" a text edit rather
than a binary patch.

Gotenberg was published on the host but on a network of its own, so reaching it
from a container went out and back through the host gateway. It now joins
danvics_convert, owned by danvics-net like the others. PDF is the one export
allowed to fail: if that service is down, the deck and the document still
download and the error says which.

Verified end to end as a plain user: the moderator route still refuses with 403,
generation returned a deck grounded on 12 corpus excerpts, the library lists
only their own, pptx/docx/pdf all downloaded valid, "add a slide on when to
admit" put the slide in the right place and left References last, and an
unauthenticated request gets 401 while someone else's id gets 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 14:50:54 +02:00

295 lines
14 KiB
Text

# ============================================================
# OPENBAO (optional — recommended for production)
# ============================================================
# When these three are set, the container fetches everything else below
# from OpenBao at kv/ped-ai/prod and ignores the equivalent .env values.
# Leave them unset (or blank) to fall back to .env-only (local dev, e2e).
#
# OPENBAO_ADDR=https://app.danvics.com
# OPENBAO_ROLE_ID=<from: bao read auth/approle/role/ped-ai/role-id>
# OPENBAO_SECRET_ID=<from: bao write -f auth/approle/role/ped-ai/secret-id>
# OPENBAO_KV_PATH=kv/ped-ai/prod # override path if needed
# ============================================================
# Everything below is sourced from OpenBao when OPENBAO_ADDR is set.
# Only fill these in for local dev / e2e / when running without vault.
# ============================================================
# ============================================================
# AI PROVIDER (choose one)
# ============================================================
# Option 1: OpenRouter (default, cheapest, NOT HIPAA)
AI_PROVIDER=openrouter
OPENROUTER_API_KEY=sk-or-v1-your-key
# Option 2: AWS Bedrock (HIPAA compliant with BAA)
# AI_PROVIDER=bedrock
# AWS_BEDROCK_REGION=us-east-1
# AWS_ACCESS_KEY_ID=AKIA...
# AWS_SECRET_ACCESS_KEY=...
# (Or use IAM role — no keys needed if running on EC2/ECS)
# Option 3: Azure OpenAI (HIPAA compliant with BAA)
# AI_PROVIDER=azure
# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
# AZURE_OPENAI_API_KEY=your-key
# AZURE_DEPLOYMENT_NAME=gpt-4o-mini
# AZURE_OPENAI_API_VERSION=2024-02-01
# Option 4: Google Vertex AI (HIPAA compliant with BAA)
# AI_PROVIDER=vertex
# Google models (Gemini and friends) are reached through LiteLLM, not the
# Google SDK — that dependency was removed. Configure them on the gateway
# and select them by their LiteLLM model id.
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
# (Or use default credentials if running on GCE/GKE/Cloud Run)
#
# Google STT — Gemini inline audio (auto-detected when GOOGLE_VERTEX_PROJECT set)
# TRANSCRIBE_PROVIDER=google
# GOOGLE_STT_MODEL=gemini-2.0-flash # or gemini-2.5-flash for better accuracy
#
# Google TTS — Google Cloud Text-to-Speech (auto-detected when GOOGLE_VERTEX_PROJECT set)
# TTS_PROVIDER=google
# GOOGLE_TTS_VOICE=en-US-Journey-F # female | en-US-Journey-D = male
# Other options: en-US-Studio-O, en-US-Neural2-C, en-US-Neural2-J
# Option 5: LiteLLM Proxy (self-hosted, routes to any provider)
# AI_PROVIDER=litellm
# LITELLM_API_BASE=http://localhost:4000
# LITELLM_API_KEY=sk-litellm-your-key
# Admin can discover available models via the admin panel
#
# LiteLLM Speech-to-Text
# TRANSCRIBE_PROVIDER=litellm
# LITELLM_STT_MODEL=whisper-1 # Use the model name from your LiteLLM model_list
# If your LiteLLM config uses full paths as model names, use the full path:
# LITELLM_STT_MODEL=openai/whisper-1
# NOTE: vertex_ai/chirp does NOT work via LiteLLM audio proxy.
# For Vertex AI speech, use TRANSCRIBE_PROVIDER=google (Gemini inline audio).
#
# LiteLLM TTS
# TTS_PROVIDER=litellm (auto-detected when LITELLM_API_BASE set)
# LITELLM_TTS_MODEL=tts-1 # Use model name from your LiteLLM model_list
# If your config uses full paths: LITELLM_TTS_MODEL=vertex_ai/google-tts
# LITELLM_TTS_VOICE=en-US-Journey-F # Google Cloud voice name (or alloy/nova for OpenAI)
# ============================================================
# TRANSCRIPTION (speech-to-text)
# ============================================================
# Option A: OpenAI Whisper (default if no AWS configured)
OPENAI_API_KEY=sk-your-openai-key
# Option B: Amazon Transcribe (HIPAA eligible, no S3 needed)
# Uses same AWS credentials as Bedrock above.
# Set TRANSCRIBE_PROVIDER=aws to force AWS even if OPENAI_API_KEY is set.
# Leave unset to auto-detect (uses AWS when AWS_BEDROCK_REGION is configured).
# TRANSCRIBE_PROVIDER=aws
# Option C: Local Whisper (privacy-first, no cloud API needed)
# Requires whisper.cpp or faster-whisper installed on the server.
# TRANSCRIBE_PROVIDER=local
# WHISPER_MODEL_SIZE=small # tiny, base, small, medium, large
# WHISPER_BINARY=whisper-cpp # or: whisper, faster-whisper
# WHISPER_MODEL_PATH= # custom path to .bin model file
# WHISPER_LANGUAGE=en
# WHISPER_THREADS=4 # defaults to CPU count - 1
# Amazon Transcribe Medical — better accuracy for clinical dictation
# Knows drug names, diagnoses, procedures, SOAP terminology
# HIPAA eligible (ensure your AWS account has a BAA)
# AWS_TRANSCRIBE_MEDICAL=true
# AWS_TRANSCRIBE_SPECIALTY=PRIMARYCARE
# Other options: CARDIOLOGY, NEUROLOGY, ONCOLOGY, RADIOLOGY, UROLOGY
# Optional
ELEVENLABS_API_KEY=
# Push Notifications (ntfy — self-hosted, optional)
# NTFY_URL=https://ntfy.yourdomain.com
# NTFY_TOKEN=tk_your_token_here
# App
PORT=3000
APP_URL=https://your-domain.com
# Cloudflare Turnstile (anti-bot on registration, optional)
# TURNSTILE_SITE_KEY=your-site-key
# TURNSTILE_SECRET_KEY=your-secret-key
JWT_SECRET=generate-a-random-64-char-string-here
# Application-layer encryption key for PHI at rest (Nextcloud tokens, audio backups)
# Generate with: openssl rand -hex 32
# REQUIRED in production. Rotating invalidates existing encrypted data.
DATA_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32
# Email (for verification & password reset)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@yourdomain.com
# Nextcloud (optional)
NEXTCLOUD_URL=https://cloud.yourdomain.com
# S3 Document Storage (optional — works with AWS S3, Backblaze B2, MinIO)
# S3_BUCKET=your-bucket-name
# S3_REGION=us-east-1
# S3_PREFIX=documents/
#
# For AWS S3: uses same AWS credentials as Bedrock above, or set S3-specific keys:
# S3_ACCESS_KEY_ID=...
# S3_SECRET_ACCESS_KEY=...
#
# For Backblaze B2:
# S3_ENDPOINT=https://s3.us-west-004.backblazeb2.com
# S3_REGION=us-west-004
# S3_ACCESS_KEY_ID=your-b2-application-key-id
# S3_SECRET_ACCESS_KEY=your-b2-application-key
#
# For MinIO (self-hosted):
# S3_ENDPOINT=http://minio:9000
# S3_REGION=us-east-1
# S3_ACCESS_KEY_ID=minio-access-key
# S3_SECRET_ACCESS_KEY=minio-secret-key
# S3_FORCE_PATH_STYLE=true
# ============================================================
# EMBEDDINGS (for Learning Hub semantic search)
# ============================================================
# Enables vector-based semantic search in Learning Hub
# Requires pgvector extension: apt-get install postgresql-16-pgvector
# Default model (Vertex AI text-embedding-005, 768 dims, English + code optimized)
EMBEDDING_MODEL=vertex_ai/text-embedding-005
EMBEDDING_DIMENSIONS=768
# Other Vertex AI embedding models:
# - vertex_ai/text-embedding-005 → 768 dims, English + code (recommended)
# - vertex_ai/gemini-embedding-001 → up to 3072 dims, multilingual + code
# - vertex_ai/text-multilingual-embedding-002 → 768 dims, multilingual focus
#
# LiteLLM usage (if using LiteLLM proxy):
# EMBEDDING_MODEL=text-embedding-005 # LiteLLM will route to configured provider
#
# OpenAI fallback (NOT HIPAA-eligible):
# Uses text-embedding-3-small if OPENAI_API_KEY is set and no Vertex/LiteLLM configured
# ============================================================
# DATABASE
# ============================================================
DATABASE_URL=postgresql://pedscribe:<password>@postgres:5432/pedscribe
DB_PASSWORD=pedscribe_secret_change_me
# ── Audio backups (optional) ────────────────────────────────────────────────
# Every recording is kept for 24 hours, whether its transcription succeeded or
# not. Unset, the audio lives in the audio_backups table; set, it goes to a
# bucket instead and only metadata stays in Postgres. The generated-images key
# is scoped to that bucket and cannot be reused here — create a bucket and a
# user of its own, and give the bucket a 24h expiry rule as a backstop.
# Audio is gzipped and AES-256-GCM encrypted before it is stored, either way.
# AUDIO_BACKUPS_S3_ENDPOINT=http://assets:9000
# AUDIO_BACKUPS_S3_BUCKET=audio-backups
# AUDIO_BACKUPS_S3_REGION=us-east-1
# Prefer the _FILE variants: credentials then stay out of the process environment.
# AUDIO_BACKUPS_S3_ACCESS_KEY_FILE=/run/secrets/audio-backups-access-key
# AUDIO_BACKUPS_S3_SECRET_KEY_FILE=/run/secrets/audio-backups-secret-key
# AUDIO_BACKUPS_S3_ACCESS_KEY=
# AUDIO_BACKUPS_S3_SECRET_KEY=
# ============================================================
# EVERY REMAINING VARIABLE THE APP READS
# Completed 2026-09-11: this file listed 18 of the 67 variables src/ actually
# reads. Each entry below shows the default the code falls back to, so an
# unset line means "this default", not "broken".
# ============================================================
# ── Mail ────────────────────────────────────────────────────────────────────
# SMTP_SECURE=false # true for implicit TLS (port 465)
# ── Admin lockdown ──────────────────────────────────────────────────────────
# With several admins, everything in the admin panel is editable by all of
# them. Set this and configuration becomes read-only in the panel and refused
# by the server: prompts, model policy, retrieval limits and budgets, TTS/STT,
# embeddings, SMTP and email templates. Day-to-day operation stays available —
# announcements, registration and invitations, feature flags, site details.
# A setting added later is locked until it is deliberately added to the
# editable list, rather than defaulting to open.
#
# It is deliberately an environment variable: a setting could be switched off
# by the very admin it restrains, so lifting it needs host access and a
# restart. Read access is unaffected — locked settings stay visible.
# ADMIN_LOCKDOWN=false
# ── Identity and limits ─────────────────────────────────────────────────────
# SITE_NAME=Pediatric AI Scribe
# API_RATE_LIMIT_MAX=200 # requests per window across /api
# LOGIN_RATE_LIMIT_MAX=10 # login attempts per 15 minutes
# NODE_ENV=production # with APP_URL, puts the app in production mode:
# refuses to start without JWT_SECRET or a CORS origin
# CORS_ORIGINS= # extra allowed origins, comma-separated, beyond APP_URL
# ── Services ────────────────────────────────────────────────────────────────
# REDIS_URL=redis://ped-ai-redis:6379
# LOKI_URL=http://monitoring-loki:3100 # log shipping; unset disables it
# MCP_SERVER_URL= # legacy single MCP URL
# ── LiteLLM gateway ─────────────────────────────────────────────────────────
# LITELLM_MASTER_KEY= # falls back to OPENAI_API_KEY, then 'sk-litellm'
# LITELLM_DEFAULT_MODEL= # falls back to OPENAI_MODEL
# LITELLM_FALLBACK_MODEL= # tried when the default model fails
# OPENAI_MODEL= # legacy name for the default chat model
# LITELLM_TTS_VOICES= # comma-separated voices offered in the pickers
# ── Clinical Assistant: retrieval ───────────────────────────────────────────
# CLINICAL_ASSISTANT_MCP_URL=http://mcp:8000/mcp
# CLINICAL_ASSISTANT_MCP_URLS= # comma-separated, tried in order
# CLINICAL_ASSISTANT_SEARCH_TOOL=clinical_semantic_search # the only accepted value
# CLINICAL_ASSISTANT_MCP_INITIALIZE_TIMEOUT_MS=30000
# CLINICAL_ASSISTANT_MCP_REQUEST_TIMEOUT_MS=90000
# CLINICAL_ASSISTANT_MCP_SESSION_TTL_MS=600000
# CLINICAL_ASSISTANT_MCP_WARMUP= # open a session at boot
# CLINICAL_ASSISTANT_MCP_WARMUP_DELAY_MS=
# ── Clinical Assistant: models and prompts ──────────────────────────────────
# CLINICAL_ASSISTANT_IMAGE_MODEL=openai-gpt-image-1
# CLINICAL_ASSISTANT_PROMPT_MODEL= # model that writes the starter prompts
# CLINICAL_ASSISTANT_PROMPT_POOL_TARGET=1000
# CLINICAL_ASSISTANT_PROMPT_POOL_KEY=clinical-assistant:prompt-pool:v2
# CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS=
# CLINICAL_ASSISTANT_PROMPT_POOL_WARMUP_DELAY_MS=
# CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS=600000
# PATIENT_TAKEHOME_BEHAVIOR= # overrides the admin-editable take-home guidance
# ============================================================
# OPENBAO (optional)
# The entrypoint fetches secrets from OpenBao only when OPENBAO_ADDR is set;
# unset, it uses this file and nothing else. A value set here or in the compose
# environment always wins over the OpenBao copy, so an override needs no
# OpenBao change.
#
# To use OpenBao: set OPENBAO_ADDR, OPENBAO_ROLE_ID, OPENBAO_SECRET_ID
# (and OPENBAO_KV_PATH if not kv/ped-ai/prod)
# To stop using it: unset OPENBAO_ADDR, and put the keys it held in this
# file. List them without printing values:
# bao kv get -format=json kv/ped-ai/prod | jq 'keys'
# ============================================================
# OPENBAO_ADDR=
# OPENBAO_ROLE_ID=
# OPENBAO_SECRET_ID=
# OPENBAO_KV_PATH=kv/ped-ai/prod
# Optional. Lets something outside this host scrape /metrics with
# `Authorization: Bearer <token>`. Unset by default, and an unset token can
# never match — Prometheus scrapes pediatric-ai-scribe:3000 directly over the
# Docker network, which needs no token. Requests arriving through the reverse
# proxy (they carry X-Forwarded-For) get a 404 either way.
METRICS_TOKEN=
# Gotenberg (LibreOffice behind an HTTP API), used to turn a generated deck or
# document into PDF. Defaults to http://gotenberg:3000 on the danvics_convert
# network. PDF is the one export allowed to fail: if this is unreachable the
# PowerPoint and Word downloads still work.
GOTENBERG_URL=