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
11 KiB
Configuration
Runtime configuration sources, in override order (later wins for overlapping keys):
.envfile / container environment variables (startup only)app_settingstable (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/detailedreports configured providers (admin-only; the public/api/healthreturns 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
-
Set the base URL —
LITELLM_API_BASEshould include/v1if the gateway serves on that path (e.g.,https://gateway.example.com/v1). The application normalizes double/v1paths internally for TTS, STT, and embedding endpoints. -
Set the API key —
LITELLM_API_KEYaccepts any key format the gateway issues (virtual keys, bearer tokens, etc.). -
Update model names — Different gateways use different naming conventions. Bifrost requires
provider/modelformat (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_MODELenv var (if set)
-
Embedding model — Set via Admin Panel → Settings →
embeddings.model. The embedding vector column isVECTOR(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. -
Restart the container —
docker compose up -d --force-recreateto pick up.envchanges (a plainrestartdoes 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.