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
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
226 lines
11 KiB
Markdown
226 lines
11 KiB
Markdown
# 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`](speech.md#where-object-storage-settings-come-from).
|
|
|
|
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 URL** — `LITELLM_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 key** — `LITELLM_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 container** — `docker 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](retrieval-tuning.md).
|