Configuration¶
All configuration is environment variables. This page is the reference for
every variable the application reads;
.env.example
is the worksheet you copy to .env — a fill-in block for the two required
values, then one line per optional variable, with the explanations here rather
than there.
Where they are read from, in order: the ENV_PATH file if that variable is
set, otherwise .env in the repo root (the recommended location), otherwise
backend/.env. Actual environment variables always win.
The Default column below is the built-in default that applies when a
variable is absent — not necessarily what .env.example writes into a fresh
.env. Where the two differ, the table says so.
Application¶
| Variable | Default | Notes |
|---|---|---|
APP_ENV |
development |
production disables the API docs and refuses unsafe configurations. Set it for anything real. |
APP_MAX_UPLOAD_MB |
20 |
Rejected with 413 before the file is buffered. Scans are large; raise it if you process long ones. |
APP_MAX_TEXT_CHARS |
500000 |
Extracted-text limit. Guards against a pathological OCR result flooding the LLM. |
APP_ALLOW_INSECURE_CONTENT_LOGGING |
false |
Dev only. Allows document content in logs, prints a loud startup warning, and is refused in production. |
APP_CORS_ORIGINS |
http://localhost:3000,http://localhost:5173 |
Only relevant for local development; in Docker the frontend proxies same-origin. |
Result retention¶
How long a finished result stays in the backend's memory so the review UI can apply corrections without re-running detection. Nothing is written to disk at any setting — but while an entry lives, a copy of the document is in the process's memory, so these are retention controls, not performance tuning. Read Data retention before raising them.
| Variable | Default | Notes |
|---|---|---|
RESULT_CACHE_TTL_MINUTES |
15 |
Lifetime of a fresh result. Short on purpose: most documents are reviewed and exported within minutes, and an expired result costs only a re-run. |
RESULT_CACHE_EXTENSION_MINUTES |
60 |
What one press of Verlängern grants, from the moment it is pressed. Repeatable, so a reviewer who keeps working keeps the result. |
RESULT_CACHE_MAX_LIFETIME_MINUTES |
720 (12 h) |
The ceiling no amount of extending can cross, measured from when the result was produced. This is the number your retention statement rests on. |
RESULT_CACHE_MAX_ENTRIES |
100 |
How many results may be in memory at once; the oldest is dropped beyond it. Bounds the worst case regardless of the durations. |
Tightening it¶
The defaults are chosen for a research prototype: they keep the tool pleasant to use, and the app was built so that restricting them is a configuration change rather than a code change. Pick what your setting needs:
| Goal | Setting |
|---|---|
| Shorter windows | Lower RESULT_CACHE_TTL_MINUTES / RESULT_CACHE_EXTENSION_MINUTES |
| A defensible outer bound | Lower RESULT_CACHE_MAX_LIFETIME_MINUTES — a shift length is a reasonable anchor |
| No extending at all | RESULT_CACHE_MAX_LIFETIME_MINUTES = RESULT_CACHE_TTL_MINUTES. The API then reports can_extend: false from the start and the UI never offers the button |
| Fewer documents resident | Lower RESULT_CACHE_MAX_ENTRIES |
| The strictest usable setting | TTL=1, MAX_LIFETIME=1, MAX_ENTRIES=1 |
All of these cost only speed. An expired result is recomputed from the text the browser still holds, so nothing is lost — a correction after expiry just pays for a fresh detection pass. A ceiling configured below the TTL is allowed and simply shortens every window to the ceiling: misconfiguration fails toward less retention, never more.
Tightening also applies to what is already in memory. The bounds are read once at startup, so a restart with stricter values immediately drops whatever no longer fits.
Sign-in (OIDC)¶
Off by default: the app assumes it sits behind your own authenticating proxy. Turning it on makes the app itself require a sign-in at your identity provider before any route that touches a document answers. It is a gate, not an authorisation model — everyone who can sign in gets the same, whole application. Full setup, including what to register at the provider: Single sign-on.
| Variable | Default | Notes |
|---|---|---|
OIDC_ENABLED |
false |
Turns the gate on. With it on, the five values below are required — the backend refuses to start without them, in every environment. |
OIDC_ISSUER |
(empty) | The provider's base URL. The app reads {issuer}/.well-known/openid-configuration; the value must match the iss the provider puts in its tokens. |
OIDC_CLIENT_ID |
(empty) | From the client you register at the provider. |
OIDC_CLIENT_SECRET |
(empty) | Confidential client: the secret never reaches the browser. |
OIDC_SESSION_SECRET |
(empty) | Signs the session cookie. At least 32 characters — openssl rand -hex 32. Rotating it signs everyone out; treat it like a private key. |
APP_PUBLIC_URL |
(empty) | The origin browsers actually use, e.g. https://deid.klinik.de. The redirect URI is derived from it, and an https:// value is what lets the session cookie be Secure. |
OIDC_SCOPES |
openid profile email |
Space-separated. openid is always requested even if you leave it out. |
OIDC_SESSION_MINUTES |
480 (8 h) |
How long a sign-in lasts. Absolute — it does not renew on activity, so the session ends on schedule whether or not the tab stayed open. |
OIDC_END_SESSION |
false |
Also end the session at the provider on sign-out, when it advertises an end_session_endpoint. Off because it signs the user out of every application, not just this one. |
OIDC_HTTP_TIMEOUT_SECONDS |
10 |
Timeout for the calls to the provider (discovery, token exchange, keys). |
OIDC_ENABLED=true
OIDC_ISSUER=https://keycloak.klinik.de/realms/intranet
OIDC_CLIENT_ID=deidentifier
OIDC_CLIENT_SECRET=…
OIDC_SESSION_SECRET=… # openssl rand -hex 32
APP_PUBLIC_URL=https://deid.klinik.de
Deployment banner¶
A bar above the header for a deployment-wide notice — "Research Use Only!", "Test system — do not use with patient data". The text is yours and is shown verbatim in every interface language, so write it in the language your users read (or keep it short and unambiguous).
| Variable | Default | Notes |
|---|---|---|
BANNER_ENABLED |
false |
Turns the banner on. Enabled with an empty BANNER_TEXT shows nothing. |
BANNER_TEXT |
(empty) | The line to display, shown as written. |
BANNER_COLOR |
amber |
amber, red, blue, green or gray. An unrecognized value falls back to amber rather than failing startup. |
Detectors¶
| Variable | Default | Notes |
|---|---|---|
DETECTORS |
rules |
Comma-separated: rules, llm, mock. .env.example ships rules,llm with empty endpoint values — a fresh copy deliberately refuses to start until OPENAI_API_BASE/LLM_MODEL are filled in. rules alone finds structured identifiers but no names; use it only for a first look without an LLM endpoint. |
A detector that is listed but cannot run makes the request fail with 503 rather
than returning a partial result, as does a name the build does not know.
mock is for tests and offline development and is refused in production.
Detection LLM¶
| Variable | Default | Notes |
|---|---|---|
OPENAI_API_BASE |
(empty) | Any OpenAI-compatible endpoint: Ollama, vLLM, LM Studio, a gateway. Include the /v1 suffix most servers expect. |
OPENAI_API_KEY |
(empty) | Empty is fine for most local servers. |
LLM_MODEL |
(empty) | Required when llm is enabled. |
LLM_REQUEST_TIMEOUT_SECONDS |
600 |
Per request. |
LLM_CHUNK_CHARS |
16000 |
Chunk size for long documents. Keep it well inside the model's context. |
LLM_CHUNK_OVERLAP |
500 |
Overlap so entities are not cut at a boundary. |
LLM_DETECTION_PASSES |
2 |
Independent passes whose results are unioned. Recall-first; doubles cost. 1 is faster, 3 catches a little more. |
LLM_MAX_CONCURRENT_REQUESTS |
4 |
Total in-flight requests across all documents. The main throughput/pressure dial. |
LLM_RECHECK_ENABLED |
true |
The audit of the anonymized output. One extra call per document; produces warnings only. |
localhost means the container, not your machine
The backend runs in its own container, so http://localhost:11434/v1
points at the backend itself and the request fails with "Der
KI-Erkennungsdienst ist nicht erreichbar". From a container, reach:
| The LLM runs… | Use |
|---|---|
| as a service in the same compose project | http://vllm:8000/v1 — the service name |
| on the Docker host (a local Ollama, say) | http://host.docker.internal:11434/v1 |
| on another machine | its hostname or IP |
On Linux, host.docker.internal resolves only if you add it to the
backend service in compose.yml:
localhost is correct only when you run the backend directly on the host,
as in the local development setup.
Details and model recommendations: LLM endpoints.
Extraction & OCR¶
| Variable | Default | Notes |
|---|---|---|
DOCLING_SERVE_URL |
(empty) | Optional, and empty by default — nothing is contacted. Unset or unreachable falls back to local pypdf extraction. |
DOCLING_MIN_EXTRACTED_CHARS_PDF |
100 |
Below this per page, a PDF counts as scanned. |
PDF_MAX_PAGES_FOR_TEXT_PROBE |
5 |
How many pages the probe samples. |
OCR_ENGINE |
none |
none, docling_tesseract, llm_vision, mistral_ocr (not implemented). none rejects scanned PDFs with a clear message. |
Engine-specific variables (MISTRAL_*, VISION_OCR_*), including the
Unlimited-OCR recipe: OCR engines.
Compose-level variables¶
Read from the same .env by compose.yml itself rather than by the
application, so they only apply to the Docker deployment.
| Variable | Default | Notes |
|---|---|---|
FRONTEND_PORT |
8080 |
The published port of the frontend container — the only published port of the stack. |
DEIDENTIFIER_IMAGE_TAG |
latest |
Image tag for both services. Pin a release rather than tracking latest. |
APP_ENV |
production |
compose.yml overrides the application default of development. |
Changing configuration¶
Settings are read once at startup — there is no admin UI and no runtime override, deliberately: the set of endpoints document content may reach is a deployment decision, not a user decision.
A safe starting point¶
APP_ENV=production
DETECTORS=rules,llm
OPENAI_API_BASE=http://vllm:8000/v1
LLM_MODEL=your-model
OCR_ENGINE=none # until you have an OCR endpoint you trust
Then verify in the UI: the header must show no external-endpoint warning,
and GET /api/v1/status must report every configured detector as ready.