pediatric-ai-scribe-v3/docs/configuration.md
Daniel 67e416c6d9
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m38s
Forgejo Docker Build / Build Docker image (push) Successful in 12s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
docs: merge the duplicate pairs and correct them against the running app
Three pairs of docs described the same thing twice, and the copies had drifted
apart. Merged each into one file, keeping the unique content from both:

- ARCHITECTURE.md -> architecture.md (its operational map: ownership, request
  flow, runtime boundaries, source of truth, deployment shape)
- DEVELOPMENT.md -> developer-guide.md (change workflow, Clinical Assistant
  high-risk areas, frontend rendering rules, deployment checks)
- transcription-options.md -> speech.md (the clinic setup table, and the list
  of browser-Whisper paths that must stay removed)

Then audited what remained against the code and the live database rather than
against the previous docs. Corrected:

- Google Vertex was still documented as a provider across nine files. The SDK
  is gone; AI_PROVIDER=vertex now logs an advisory and falls back to
  OpenRouter, and Gemini is reached through LiteLLM. Fixed the provider
  selection order to match src/utils/ai.js, which starts from LITELLM_API_BASE.
- promptSafe was documented on 8 routes; it is on 13.
- Node 20 -> 24, "24 vanilla JS modules" -> no fixed count, and
  transcribe.js/tts.js -> sttProvider.js/ttsProvider.js, which is what exists.
- STT/TTS are LiteLLM-only; README listed direct Google, AWS Transcribe and
  ElevenLabs paths that are not in the runtime.
- Learning Hub PPTX export was documented as pptxgenjs, which is not a
  dependency. It is pandoc against a reference deck.
- POST /api/admin/milestones/seed does not exist; it is /bulk-import.
- NEXTCLOUD_URL and NTFY_TOPIC are not read anywhere. Nextcloud is per-user in
  the users table, and the ntfy topic is derived as pedscribe-{userId}.
- A prose paragraph sat inside the Clinical Assistant settings table, so half
  the rows rendered as text.

Filled the gaps the audit exposed:

- database.md was missing 12 of 29 tables, including user_resources,
  personal_notes, login_codes, registration_invites and generated_image_jobs.
- developer-guide.md was missing 11 routers and 10 frontend modules.
- api-reference.md detailed 121 of 244 endpoints and said so, but whole
  features were absent. Added an endpoint index covering Clinical Assistant,
  My Resources, Notes, Diagrams, ED Encounters, invites and sign-in codes.
- configuration.md was missing METRICS_TOKEN, REDIS_URL, API_RATE_LIMIT_MAX,
  the LITELLM_* model variables, the DB_* ones maintenance.js reads, and the
  per-purpose S3 resolution scheme.
- clinical-assistant.md documented 2 of its 17 environment variables.
- features-explained.md had no entry for My Resources or Clinical Assistant.

Renamed the three remaining SHOUTING filenames to kebab-case, which is what the
docs viewer's prettyName() was working around, and rewrote README's index,
which listed architecture.md twice and omitted nine files.

Noted but not changed: the Turnstile site key is hardcoded in index.html rather
than read from TURNSTILE_SITE_KEY, and /api/health/detailed can report
tts: 'elevenlabs' though no ElevenLabs path exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 04:57:35 +02:00

11 KiB

Configuration

Runtime configuration sources, in override order (later wins for overlapping keys):

  1. .env file / container environment variables (startup only)
  2. app_settings table (live, editable from Admin Panel with 2-minute cache)

Environment variables

Core (production-required)

Variable Purpose
APP_URL Public base URL. Enables production mode — fail-closed CORS, HSTS, secure cookies.
JWT_SECRET HMAC key for JWT signing and OIDC state. Server refuses to start without it in production.
DATA_ENCRYPTION_KEY AES-256-GCM key for PHI at rest (Nextcloud tokens, audio backups). 64 hex chars (openssl rand -hex 32). Refuses to start without it in production.
AUDIO_BACKUPS_S3_ENDPOINT, _BUCKET, _REGION Optional. Keeps the 24-hour recording copies in object storage instead of the audio_backups column; metadata stays in Postgres either way. See docs/speech.md.
AUDIO_BACKUPS_S3_ACCESS_KEY_FILE, _SECRET_KEY_FILE Credentials for the above, read from files so they never appear in the process environment. AUDIO_BACKUPS_S3_ACCESS_KEY / _SECRET_KEY are the inline fallback.
DB_PASSWORD / DATABASE_URL Postgres password or full connection string.
DB_HOST, DB_PORT, DB_USER, DB_NAME Read by scripts/maintenance.js when DATABASE_URL is not set (defaults postgres / 5432 / pedscribe / pedscribe). The app itself takes its connection from DATABASE_URL / DB_PASSWORD.
REDIS_URL Redis for operational cache and prompt suggestions. Absent means those features degrade, not that the app fails.
SITE_NAME Name used in emails and page titles (default Pediatric AI Scribe).
PORT HTTP listen port (default 3000).
NODE_ENV production forces prod-only guards on even without APP_URL.

CORS

Variable Purpose
CORS_ORIGINS Comma-separated additional allowed origins beyond APP_URL.

AI provider

Variable Purpose
AI_PROVIDER litellm / openrouter / bedrock / azure. If unset, the default is litellm when LITELLM_API_BASE is set, otherwise openrouter; the startup loader then lets the last initialized non-OpenRouter client win. vertex is not a provider any more — it logs an advisory and falls back to OpenRouter. Reach Gemini through LiteLLM.
OPENROUTER_API_KEY OpenRouter key (not HIPAA-eligible).
AWS_BEDROCK_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY Bedrock chat provider.
AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_DEPLOYMENT_NAME, AZURE_OPENAI_API_VERSION Azure OpenAI.
LITELLM_API_BASE, LITELLM_API_KEY OpenAI-compatible AI gateway (Bifrost, LiteLLM, or similar). LITELLM_MASTER_KEY then OPENAI_API_KEY are tried if LITELLM_API_KEY is unset.
LITELLM_DEFAULT_MODEL Default chat model for the LiteLLM provider; falls back to OPENAI_MODEL.
LITELLM_FALLBACK_MODEL Fallback model within LiteLLM. There is no implicit cross-provider fallback.

Speech-to-text

Variable Purpose
TRANSCRIBE_PROVIDER Use litellm; auto mode uses LiteLLM when configured.
TTS_PROVIDER Same idea for speech output; litellm is the supported value.
LITELLM_STT_MODEL Model name for LiteLLM-routed STT.

Text-to-speech

Variable Purpose
LITELLM_TTS_MODEL, LITELLM_TTS_VOICE LiteLLM-routed TTS model and default voice.
LITELLM_TTS_VOICES Comma-separated LiteLLM-compatible voices exposed in voice search and user preferences.

Embeddings

Variable Purpose
EMBEDDING_MODEL LiteLLM embedding model name (default openai-text-embedding-3-large).
EMBEDDING_DIMENSIONS Vector dimensions (default 3072).

Email (SMTP)

Variable Purpose
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM SMTP config for verification + password reset emails. Overridable per-instance via app_settings.

Security / external

Variable Purpose
TURNSTILE_SECRET_KEY Cloudflare Turnstile. The verification is a no-op when this is unset. The matching site key is currently hardcoded in public/index.html (data-sitekey on the register and forgot-password widgets), not read from the environment — a different deployment has to edit that file.
LOKI_URL Optional Loki ingest URL for shipping audit/api/access logs.
NTFY_URL, NTFY_TOKEN Optional ntfy push for new-login / password-change notifications. The topic is not configurable — src/utils/notify.js derives it per user as pedscribe-{userId}.
METRICS_TOKEN Lets /metrics be scraped from off-host with Authorization: Bearer <token>. Without it, any request carrying X-Forwarded-For gets a 404, so only same-host scrapes succeed.
API_RATE_LIMIT_MAX General request ceiling per IP per minute (default 200).
LOGIN_RATE_LIMIT_MAX Sign-in attempts per IP per 15 min (default 10). Raised in the e2e stack so multi-worker Playwright runs do not trip it.
LOGIN_CODE_RATE_LIMIT_MAX Emailed sign-in codes per IP per hour (default 5). Lower than the sign-in limit because each request sends mail to somebody else's address. See docs/authentication.md.

Integrations

Variable Purpose
GOTENBERG_URL Document conversion service for PDF export (default http://gotenberg:3000). PowerPoint and Word are produced in-process and keep working when this is unreachable; only PDF fails.
S3_BUCKET, S3_REGION, S3_PREFIX, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE Document object storage (S3_PREFIX defaults to documents/). S3_FORCE_PATH_STYLE=true for MinIO, Backblaze B2, most non-AWS providers.

Object storage is resolved per purpose — documents, generated-images, audio-backups — by src/utils/objectStorage.js: the purpose's own variables first, then the shared S3_* ones, then a per-purpose bucket name. Any credential also accepts a _FILE suffix naming a file to read it from, and the _FILE form always wins. Full rules in speech.md.

Nextcloud has no environment variables: each user enters their own URL, username and token in Settings, and those are stored encrypted on the users row.

app_settings — live runtime configuration

Key-value rows in the app_settings table. Read via config.get(key, default) with 2-minute in-memory cache. Writes invalidate the cache immediately.

Registration & site

Key Purpose
registration_enabled true/false. Gate new signups.
site.name Display name.
site.auto_delete_days Days before encounters auto-expire (default 7).

Announcements

Key Purpose
announcement.text Banner text. Empty = banner hidden.
announcement.type info / warning / error / success.

SMTP overrides (override env)

smtp.host, smtp.port, smtp.user, smtp.pass, smtp.from.

Email templates

email.{flow}.subject, email.{flow}.body where {flow} is verify / reset / new_login / password_changed.

OIDC / SSO

Key Purpose
oidc.enabled Toggle SSO.
oidc.issuer OIDC issuer URL.
oidc.client_id, oidc.client_secret OAuth client credentials.
oidc.button_label Login-page button text (default "Sign in with SSO").
oidc.disable_local_auth Hide local login form when SSO is enabled.
oidc.allowed_ips CIDR whitelist for SSO (optional).

AI / models / prompts

Key Purpose
models.default Default model ID.
models.disabled JSON array of disabled model IDs.
models.custom JSON array of admin-added models.
ai.allow_model_fallback Enable silent fallback to secondary model on primary failure. Default false — fallback could spill to a non-BAA provider.
stt.model, tts.model, tts.voice System-wide STT/TTS defaults (users can override per-account).
prompt.{name} Prompt overrides. Any template in src/utils/prompts.js can be replaced live.
embeddings.model, embeddings.dimensions Override embedding config.

Feature flags

feature.* — any key matching this prefix can be consulted via config.get('feature.foo').

Internal migration flags

Key Purpose
migration.text_indexes_c Set to 'true' once lookup-critical text indexes have been converted to COLLATE "C". Prevents re-running.

Admin panel

The Admin Panel (/admin route, admin-only) exposes everything above plus:

  • User list: verify, disable, delete, promote to admin/moderator.
  • Session viewer: active sessions per user, admin-revoke.
  • Logs: audit / api / access tables with filtering.
  • Detailed health: /api/health/detailed reports configured providers (admin-only; the public /api/health returns only {ok: true} to avoid leaking stack info).
  • Model management: enable/disable, add custom, set default, discover from provider.
  • Prompt editor: live-edit any PROMPTS.* key.
  • Test SMTP / test STT / test TTS.

Switching AI gateways

The LITELLM_API_BASE and LITELLM_API_KEY variables work with any OpenAI-compatible gateway — LiteLLM, Bifrost, or other proxies.

Migration steps

  1. Set the base URLLITELLM_API_BASE should include /v1 if the gateway serves on that path (e.g., https://gateway.example.com/v1). The application normalizes double /v1 paths internally for TTS, STT, and embedding endpoints.

  2. Set the API keyLITELLM_API_KEY accepts any key format the gateway issues (virtual keys, bearer tokens, etc.).

  3. Update model names — Different gateways use different naming conventions. Bifrost requires provider/model format (e.g., openrouter/gpt-4.1), while LiteLLM can use deployment aliases (e.g., openrouter-gpt-4.1). Update model names in:

    • Admin Panel → Models (chat models)
    • Admin Panel → Settings → stt.model (speech-to-text)
    • Admin Panel → Settings → tts.model (text-to-speech)
    • LITELLM_TTS_MODEL env var (if set)
  4. Embedding model — Set via Admin Panel → Settings → embeddings.model. The embedding vector column is VECTOR(768), so any model producing 768 dimensions works without re-embedding (e.g., vertex/text-embedding-005). Switching to a model with different dimensions requires altering the column and re-embedding all content.

  5. Restart the containerdocker compose up -d --force-recreate to pick up .env changes (a plain restart does not re-read .env).

Verified gateways

Gateway Model format Notes
Bifrost provider/model Virtual keys, semantic caching, MCP gateway
LiteLLM Custom aliases Requires PostgreSQL + Redis
Any OpenAI-compatible Varies Must serve /v1/chat/completions, /v1/audio/speech, /v1/audio/transcriptions, /v1/embeddings

Retrieval sizing

How many corpus excerpts the Clinical Assistant, the Learning Hub and My Resources each receive, and the reranker cap that overrides all three: retrieval-tuning.md.