KodeMed KodeMed

Full documentation, downloads, and deployment tools available

Sign in with your KodeMed account to access DevOps guides, Windows installer, and Linux packages.

Sign in for full access

System Requirements

What you need to run KodeMed — the server side (where the services run) and the client side (the coder’s workstation or your HIS).

Server

Hosts the KodeMed services (Server, DataServer, Grouper, SearchServer, CodingUI) plus the database. Choose Docker Compose, native, or OpenShift (Helm).

Docker Compose recommended

Linux x86_64 (Ubuntu 22.04+ / Debian 12+ / RHEL 8+) Docker Engine 24+ & Compose 2.24+ 8 GB RAM 4 vCPU 40 GB free disk PostgreSQL 15+ (bundled or external) OIDC IdP for SSO (bundled Keycloak optional) TLS 1.2+ (Caddy overlay or own reverse proxy) Outbound HTTPS to the image registry oras CLI — pulls the classification data; the installer pre-flight requires it unzip, curl, openssl A Harbor robot account (robot$…) from KodeMed AG — a personal SSO login will not do root / sudo on the host

Native install (no Docker)

Ubuntu 22.04+ / Debian 12+ Java 21 & Node.js 20 (auto-installed) PostgreSQL 15+ (must be running) Your own reverse proxy (nginx / Apache / HAProxy)

OpenShift clusters are supported via the Helm chart. Full details in Linux Server and the DevOps Guide.

Client

How a coder reaches KodeMed: a browser, the Windows tray client, or your HIS via the in-process COM DLL.

Web browser (no install)

Any modern Chromium, Edge or Firefox HTTPS access to the CodingUI URL

Windows client (MSI)

Windows 10 (21H2+) / Windows 11, x64 WebView2 Runtime (pre-installed on Win 10 21H2+ / Win 11) HTTPS access to the server URLs

COM DLL (HIS integration)

Windows x64 .NET 9 Desktop Runtime (x64) — required for COM hosting WebView2 Runtime (embedded coding window) A 64-bit host process — a 32-bit one cannot load it (0x80040154) Installed by the KodeMed MSI in COM-only mode — it registers the object; no regsvr32, no file bundle to keep together

Step-by-step in Windows Client and Integration.

Database engines

Which engine you get, and which one you can ask for.

EngineStatusHow you get it
PostgreSQL 15+ Default · self-service Every installer deploys it — bundled as a container by the Deployment Wizard, setup.sh and kodemed-install.sh, or pointed at your own server. Our demo and TEST environments run on it.
Microsoft SQL Server 2022 Supported · with assistance The application runs on it today: Server and DataServer ship an mssql Spring profile and the JDBC driver, the SearchServers ship the driver, and the GrouperServer holds no database at all. No installer, wizard preset or Helm value offers the choice yet, so the deployment is repointed with us after generation.

If you do nothing, you get PostgreSQL. Keycloak keeps its own PostgreSQL in either case — it is a separate database in a separate container and the engine choice does not touch it, so you will still see a postgres container on a SQL Server deployment, and that is correct.

A SQL Server JDBC URL must carry sendStringParametersAsUnicode=false. The collation is Latin1_General_100_CS_AS_SC_UTF8, so varchar already stores Unicode; without that flag every parameter is sent as nvarchar, each comparison becomes an implicit conversion, and the indexes silently stop being used — everything still works, only slower, with nothing in any log to say why. Let the tooling build the URL rather than typing it.

Linux Server

Two Deployment Options

Docker Compose (recommended)Native Install
HTTPSCaddy via overlay (docker-compose.caddy.yml) or your own reverse proxyRequires your own reverse proxy (nginx, Apache, HAProxy)
DatabasePostgreSQL included in containersYou must install and configure PostgreSQL 15+ separately
SSOAny OIDC IDP (Keycloak / Azure AD / Okta / Auth0 / Ping / Authelia / …); or check “Include bundled Keycloak” in the wizard for a turnkey IDP with LDAP / AD federation built inAny OIDC IDP required (no bundled option)
Updatesdocker compose pull && docker compose up -dRe-run install-kodemed.sh
Best forMost deployments, quick setupHospitals that require native services (no Docker)

For the full picture of how the compose layers, TLS termination and the OIDC IDP (bundled Keycloak or your own) fit together, see DevOps Guide → Compose layers.

Docker Compose Deployment (recommended)

Use the Deployment Wizard to generate a ready-to-use package. All services, database, and HTTPS are included.

Docker 24+ Docker Compose 2.24 or newer Linux x86_64 4 vCPU · 8 GB RAM · 40 GB disk

Native Install (no Docker)

Contains Server, DataServer, GrouperServer, SearchServer (Lucene thesaurus search, DE/FR/IT), CodingUI, and installer scripts. The installer creates systemd services and generates all configuration files interactively.

  • kodemed-linux-2026.11.6.2662.tar.gzDownload
$ tar -xzf kodemed-linux-2026.11.6.2662.tar.gz
$ sudo ./install-kodemed.sh
Ubuntu 22.04+ / Debian 12+ PostgreSQL 15+ (must be running) Java 21 (auto-installed) Node.js 20 (auto-installed) 4 vCPU · 8 GB RAM · 40 GB disk Linux x86_64

Before You Start

  1. Install PostgreSQL and create the database:
    sudo apt install -y postgresql postgresql-contrib
    sudo systemctl enable --now postgresql
    sudo -u postgres psql -c "CREATE USER kodemed WITH PASSWORD 'YOUR_SECURE_PASSWORD';"
    sudo -u postgres psql -c "CREATE DATABASE kodemed OWNER kodemed;"
  2. Verify PostgreSQL is running before running the installer:
    sudo systemctl status postgresql
    sudo -u postgres psql -c "SELECT 1"
  3. The installer will ask for the database URL, username, and password. Have these ready.
  4. The installer will ask if you are behind a reverse proxy. For production, answer yes and provide your HTTPS URLs. HTTP is only for localhost testing.
  5. Have the classification data ready — the native installer creates the inbox but never fills it, so a fresh install comes up with an empty thesaurus and nothing says why. Pull it yourself after the install, as the robot account:
    echo 'ROBOT_SECRET' | oras login harbor.mieresit.com -u 'robot$YOUR_ROBOT' --password-stdin
     
    # classification data, into the YEAR subdirectory the DataServer scans
    sudo mkdir -p /opt/kodemed/dataserver/import/inbox/2026
    oras pull harbor.mieresit.com/kodemed/data:2026 -o /tmp/kmdata
    cd /tmp/kmdata && unzip -o '*.zip' -d /opt/kodemed/dataserver/import/inbox/2026/
     
    # grouper specs + catalogues
    oras pull harbor.mieresit.com/kodemed/grouper:2026 -o /tmp/kmgrp
     
    # the DataServer rescans every 30s; watch it land
    sudo journalctl -u kodemed-dataserver -f
    The inbox path is whatever KODEMED_IMPORT_INBOX_DIR says in kodemed-dataserver.env — read it there rather than assuming, and note the files go in a year subdirectory, not flat. Once the import finishes, restart the three searchservers so Lucene indexes data that exists.
  6. Database engine: this installer configures PostgreSQL only. It now rejects a non-PostgreSQL JDBC URL at the prompt rather than installing cleanly and failing at every service start.

After Installation — Verify

# Check all services are running
sudo systemctl status kodemed-server kodemed-dataserver kodemed-grouperserver kodemed-ui kodemed-searchserver-de kodemed-searchserver-fr kodemed-searchserver-it
 
# Check server logs (should show "Started KodeMedServerApplication")
sudo journalctl -u kodemed-server -n 20
 
# Test API (should return JSON)
curl -s http://localhost:8080/actuator/health | head -1
 
# Test UI (should return HTML)
curl -s http://localhost:3000 | head -5

First startup takes 2–5 minutes while the database schema is created and classification data is imported. Check progress with journalctl -u kodemed-server -f.

License File — per-service requirement

The native installer creates one systemd unit per service, each with its own WorkingDirectory (/opt/kodemed/server/, /opt/kodemed/dataserver/, /opt/kodemed/grouperserver/). The license validator resolves ./kodemed.license relative to the running service’s WorkingDirectory, not to the install root. A single license at $INSTALL_DIR/kodemed.license will be ignored and services will fail with HTTP 402.

Choose one of these options:

  1. Copy the same kodemed.license into each service directory:
    sudo cp kodemed.license /opt/kodemed/server/
    sudo cp kodemed.license /opt/kodemed/dataserver/
    sudo cp kodemed.license /opt/kodemed/grouperserver/
    sudo systemctl restart kodemed-server kodemed-dataserver kodemed-grouperserver
  2. OR set KODEMED_LICENSE_FILE to an absolute path in /etc/kodemed/kodemed.env so all three services read the same file:
    sudo install -d /etc/kodemed
    sudo cp kodemed.license /etc/kodemed/
    echo 'KODEMED_LICENSE_FILE=/etc/kodemed/kodemed.license' | sudo tee -a /etc/kodemed/kodemed.env
    sudo systemctl restart kodemed-server kodemed-dataserver kodemed-grouperserver

Verify with journalctl -u kodemed-server | grep -i license. A successful load logs "License loaded: customer=… expires=…". HTTP 402 on any endpoint means the license was not found.

Support

Technical support and documentation: support.kodemed.ch

Deployment Wizard Docker Compose

Not sure which mode fits your network? Deployment modes — which one to use is a one-page decision tree: the deciding question is who terminates TLS. Read it before filling this in; changing mode afterwards means regenerating the package.

This wizard generates a ready-to-use deployment package for your hospital’s on-premise server. Fill in the fields below and click Download ZIP to get:

  • docker-compose.yml — orchestrates all KodeMed services (Server, DataServer, Grouper, SearchServer DE/FR/IT, CodingUI, PostgreSQL)
  • .env — passwords, URLs, and settings (keep this file secure)
  • runtime-config.js — browser configuration for the CodingUI
1. Public URLs (browser-accessible — used by CodingUI and DLL clients)

These are the URLs your users will access from their browser or DLL client. They must be reachable externally (via reverse proxy, load balancer, or direct).

2. Internal Service URLs (Docker network — usually keep defaults)

These are the internal Docker network URLs used in docker-compose.yml. Keep the defaults unless you use an external reverse proxy or custom network.

3. Database

Use the bundled PostgreSQL container, or point the stack at an existing PostgreSQL 15+ server of your own — untick the box and give its host and port.

This wizard generates PostgreSQL deployments only. The JDBC URL and driver it writes are PostgreSQL’s, so a Microsoft SQL Server host entered here produces a package that cannot connect. KodeMed does run on SQL Server 2022 at the application layer — see Database engines below — but that path is not self-service yet; ask us and we will configure it with you.

No password is asked for here, by design. For the bundled PostgreSQL, setup.sh generates one on your server and writes it to .env (mode 0600) — it is an internal detail of a container, and nobody ever types it. If you point KodeMed at your own database, set the password in the environment on your server when you run the installer: export KODEMED_DB_PASSWORD='...'. That way your production credential is never typed into a web page and never travels inside a downloaded ZIP.

4. Authentication (OIDC / SSO)

KodeMed authenticates every request against an OIDC-compliant Identity Provider. Point it at the one you already run (Keycloak, Azure AD, Okta, Auth0, Ping, Authelia, …), or include the bundled Keycloak below.

Bundled Keycloak is included so hospitals with an existing LDAP or Active Directory get SSO for KodeMed without standing up a separate IDP — Keycloak federates straight into the directory and users sign in with their hospital credentials. Already running Keycloak / Azure AD / Okta / …? Leave the box unchecked and point the Issuer URI above at your existing IDP.

They let you sign in the minute the stack comes up. They also share one documented password and include an administrator, so a deployment going live should either untick this or delete them in the Keycloak console. Unticked, the kodemed realm ships with no users and you create the first one in the admin console, or connect your directory.

5. Security & Advanced Options

Optional settings for GDPR compliance, CORS, and logging. Sensible defaults are provided — only change if your infrastructure requires it.

Quick Start

Recommended — one command

Every generated ZIP ships kodemed-install.sh. Unzip it on the server and run it: it checks the host, logs in to the registry (both the container engine and oras), pulls and unpacks the classification + grouper data, fixes the file permissions that otherwise break the licence replacement and expose your .env, starts the stack, waits for the import before reindexing, and then verifies that the thesaurus is not empty before telling you it is done.

# run as root (or prefix each line with sudo) — /opt is not writable otherwise
$ sudo mkdir -p /opt/kodemed && sudo chown "$USER" /opt/kodemed
$ unzip kodemed-deployment.zip -d /opt/kodemed
$ cd /opt/kodemed
$ chmod +x kodemed-install.sh kodemed-update.sh # a ZIP cannot carry the +x bit
$ sudo -E ./kodemed-install.sh --dry-run # prints the whole plan — changes nothing
$ sudo -E ./kodemed-install.sh # install (add --yes for unattended)

Unattended: export the robot credentials first and it never prompts — the secret is read from the environment and piped to --password-stdin, so it never appears in your shell history or in ps:

export HARBOR_USERNAME='robot$customer-pull' HARBOR_PASSWORD='ROBOT_SECRET'
./kodemed-install.sh --yes --year 2026

It is idempotent. If a step fails, fix the cause and run it again: years already installed are kept, permissions are re-applied, and the searchservers are only restarted when the data actually changed. Useful flags: --year YYYY (repeatable), --no-data, --force-data, --tag, --splg-dir, --no-login. Run ./kodemed-install.sh --help for all of them. Docker and Podman are both supported: the engine (docker, else podman) and the compose front-end (docker compose / podman compose / podman-compose) are auto-detected, bind mounts are relabelled container_file_t on SELinux-Enforcing hosts (Rocky 9 / RHEL 9), and under rootless Podman the container-UID ownership is applied with podman unshare chown.

Got an older ZIP without the script? Fetch it from this portal once:

curl -fsSLO <this-portal>/tools/kodemed-install.sh && chmod +x kodemed-install.sh

You are never asked for a database password. POSTGRES_PASSWORD arrives empty in .env and setup.sh fills it with openssl rand -base64 24 on your server, keeping any value already there — the bundled database is a container of ours, so nobody has to invent that password and nobody will ever type it. Using your own database instead? Set it in the environment here, never in a downloaded file: export KODEMED_DB_PASSWORD='...'.

Under sudo, use sudo -E. Plain sudo builds a fresh environment and discards HARBOR_PASSWORD and KODEMED_DB_PASSWORD both, so the installer prompts as though you had set nothing.

Later, update the same install with one command too: ./kodemed-update.sh (see Updating an existing installation).

The steps below are the same thing done by hand — keep them for understanding what the script does, for an air-gapped host, or when you must run a step manually. They mirror the README.md shipped in the zip.

$ unzip kodemed-deployment.zip # includes demo license (90 days) + import-data/ specs/ catalogues/ dirs
 
# 1. Log in to Harbor with your ROBOT account (required before any pull)
$ echo 'ROBOT_SECRET' | docker login harbor.mieresit.com -u 'robot$customer-pull' --password-stdin
$ echo 'ROBOT_SECRET' | oras   login harbor.mieresit.com -u 'robot$customer-pull' --password-stdin # oras keeps its OWN credential store
# use the robot account (robot$...) KodeMed AG gives you — NOT a personal OIDC/SSO login (a personal account returns 401 from SSH until you log into the Harbor browser UI once). Robot names contain "$" so SINGLE quotes are mandatory.
 
# 2. Pull classification data (ICD/CHOP/DRG/TARPSY/ST Reha) and extract
$ oras pull harbor.mieresit.com/kodemed/data:2026 -o import-data/ # oras.land/docs/installation
$ cd import-data && mkdir -p 2026
$ e=0 n=0; for f in *.zip *.tar.gz; do [ -e "$f" ] || continue; n=1; case "$f" in *.zip) unzip -o "$f" -d 2026/ ;; *) tar -xzf "$f" -C 2026/ ;; esac && rm -f "$f" || { echo "FATAL: could not extract $f" >&2; e=1; }; done; [ "$n" = 1 ] || { echo "FATAL: no *.zip or *.tar.gz here - the oras pull above did not download it" >&2; e=1; }; [ "$e" = 0 ]
$ cd ..
$ sudo chown -R 1001:1001 import-data/
# the artifact is a single archive — DataServer matches individual icd10*.zip / dz-d-*chop*.zip etc. AND requires them in a year-named subdir (inbox/2026/), so extraction-into-2026/ is mandatory. The chown hands ownership to the container UID 1001 so the DataServer can MOVE files from inbox/<year>/ to processing/ during the scan (otherwise: AccessDeniedException, every file ends with status=ERROR)
 
# 3. Pull Grouper specs + catalogues and extract (current year)
$ oras pull harbor.mieresit.com/kodemed/grouper:2026 -o ./
$ e=0 n=0; for f in kodemed-grouper-*.zip kodemed-grouper-*.tar.gz; do [ -e "$f" ] || continue; n=1; case "$f" in *.zip) unzip -o "$f" ;; *) tar -xzf "$f" ;; esac && rm -f "$f" || { echo "FATAL: could not extract $f" >&2; e=1; }; done; [ "$n" = 1 ] || { echo "FATAL: no kodemed-grouper-*.zip or kodemed-grouper-*.tar.gz here - the oras pull above did not download it" >&2; e=1; }; [ "$e" = 0 ]
# the OCI artifact ships as a single archive — extraction lands .sgs into specs/ and .csv into catalogues/; skip it and Grouper returns loaded:false for every system
 
# 3b. (Optional) Pull the full 5-year window — lets the grouper bill prior years and compare across versions
$ for y in 2022 2023 2024 2025 2026; do oras pull harbor.mieresit.com/kodemed/grouper:$y -o ./ && ( e=0 n=0; for f in kodemed-grouper-*.zip kodemed-grouper-*.tar.gz; do [ -e "$f" ] || continue; n=1; case "$f" in *.zip) unzip -o "$f" ;; *) tar -xzf "$f" ;; esac && rm -f "$f" || { echo "FATAL: could not extract $f" >&2; e=1; }; done; [ "$n" = 1 ] || { echo "FATAL: no kodemed-grouper-*.zip or kodemed-grouper-*.tar.gz here - the oras pull above did not download it" >&2; e=1; }; [ "$e" = 0 ] ) || echo "FATAL: grouper:$y was not installed" >&2; done
# each year tag carries that year’s SwissDRG AV (Abrechnungsversion) spec + catalogue — 2022=v11.0 … 2026=v15.0; all land side by side in specs/ + catalogues/ and load lazily per case discharge year
 
# 4. (SKIP this step if you used the IP / self-signed preset — Caddy auto-generates the certs)
$ bash certs/generate-certs.sh # ONLY when tlsMode=custom: the script is in the zip; the file `certs/cert.pem` / `certs/key.pem` placeholders need real certs
 
# 5. Start the stack
$ sudo docker compose up -d # requires root, sudo, or docker group membership
$ docker compose logs -f kodemed-dataserver # watch the first-run import (~3-5 min)

Updating an existing installation

Recommended — one command. Every deployment ships kodemed-update.sh. It backs up your configuration, fetches the deployment files matching the target version, adds only the new .env keys (your values, secrets and licence are never overwritten), pulls the images, recreates the stack, verifies it, and rolls back automatically if verification fails. Data volumes are never touched.

$ cd /opt/kodemed
$ ./kodemed-update.sh --dry-run --tag RELEASE   // shows exactly what would change — changes nothing
$ ./kodemed-update.sh --tag RELEASE             // apply (add --yes for unattended)

Name the release you move to. The ZIP pins your images (IMAGE_TAG in .env), so as long as it holds a version ./kodemed-update.sh without --tag re-applies the version you already run — useful to repair an install, but it moves nothing forward. RELEASE is a version from the Image Version list of this wizard (e.g. 2026.10.22.2583); --tag moves the deployment files and the images together and rewrites IMAGE_TAG.

Catalogue data (a new year, or a re-published package for a year you already have) is detected and reported but never touched unless you ask: add --with-data to refresh it (import + reindex are sequenced for you), or --data-only --with-data for data alone. Run ./kodemed-update.sh --help for all options.

Older installs may predate the script. Fetch the operator tools once — they arrive as plain files, nothing to unpack:

echo 'ROBOT_SECRET' | oras login harbor.mieresit.com -u 'robot$customer-pull' --password-stdin
oras pull harbor.mieresit.com/kodemed/tools:latest -o tools/
chmod +x tools/*.sh

That gives you kodemed-update.sh, kodemed-install.sh, kodemed-backup.sh, collect-logs.sh and kodemed-doctor.sh. Pull into tools/, never over the deployment directory — oras pull overwrites by name, and this artifact contains the very updater your site is running.

Run that first update from tools/: bash tools/kodemed-update.sh --tag RELEASE. An install older than 2026-10-07 has the whole updater in its deployment directory instead of the small launcher into tools/, and ./kodemed-update.sh keeps running that old copy whatever an update puts in tools/. The run from tools/ puts the launcher back, so ./kodemed-update.sh works as described from then on. bash tools/kodemed-doctor.sh tells you when your install is in that state.

Before you update, take a backup: bash tools/kodemed-backup.sh. An update never deletes data — what it cannot protect is what a person put on this host by hand and nothing reproduces: the licence, the TLS certificate and its private key, the compose file the wizard generated once, the Caddyfile, runtime-config.js and splg/spitalliste.dat. It verifies what it captured and exits non-zero if a dump came out empty or a certificate path turned out to be a directory, so a half-backup cannot report itself as a good one. Check the exit code, not the absence of red text.

If anything looks wrong, before or after an update: bash tools/kodemed-doctor.sh. It is read-only — it changes nothing and is safe on a production host — and it names the cause of the failures that do not announce themselves: a licence mounted read-only (why “Replace licence” is greyed out, which no chown fixes), a request-header ceiling too low for a user carrying hundreds of IdP groups, a WebSocket URL that answers but never upgrades, and an init container that exited and took CORS with it.

Or follow the manual procedure below.

The manual procedure below is the same thing done by hand — useful to understand what the script does, or when you cannot run it. This ZIP is a snapshot of today. Container images update with docker compose pull, but the deployment files (docker-compose.yml, setup.sh, the .env keys) never update by themselves — so a newer image that expects a new environment variable your old compose does not pass starts healthy and the feature then fails silently. Hence two cases.

Preserved — an update never touches these: Docker volumes (Postgres database, thesaurus, Lucene indexes) and your own files: .env, kodemed.license, TLS certs, specs/, catalogues/, import-data/. Replaced by us: docker-compose.yml, setup.sh.

⚠ Never run docker compose down -v. The -v deletes the volumes — the database, the thesaurus and all three Lucene indexes go with it, and the next start behaves like a fresh install (3–5 min re-import, up to ~15 min re-index, coding work lost). Plain docker compose down is safe; for an update you normally need no down at all.

Case 1 — images only (no deployment change):

$ cd /opt/kodemed && cp .env .env.bak
$ docker compose pull # bump IMAGE_TAG in .env first if it is pinned to a version
$ docker compose up -d # recreates only what changed — volumes untouched
$ docker compose ps && docker volume ls # all healthy, identical volume list

Case 2 — the deployment changed (compose / setup.sh / new env vars). Use it whenever a release note mentions any of those — or when unsure. One diff, nothing destructive:

$ cd /opt/kodemed && cp docker-compose.yml docker-compose.yml.bak && cp .env .env.bak
$ mkdir -p ./bundle-new && oras pull harbor.mieresit.com/kodemed/installer-bundle:IMAGE_TAG -o ./bundle-new/
# “repository not found”? The version-pinned bundle is not published yet — instead re-run this wizard with EXACTLY the same answers, download the ZIP and: unzip -q kodemed-deployment.zip -d ./bundle-new/
 
$ diff -u docker-compose.yml bundle-new/docker-compose.yml # what changed in the deployment
$ diff <(grep -oE '^[A-Z_][A-Z0-9_]*=' .env) <(grep -oE '^[A-Z_][A-Z0-9_]*=' bundle-new/.env)
# KEYS only — the new package ships freshly generated secrets, never copy its values. Every “>” line is a NEW variable your install is missing (the silent-failure case).
 
$ cp bundle-new/docker-compose.yml ./docker-compose.yml
$ printf '\nNEW_VARIABLE=value\n' >> .env # APPEND only — never overwrite .env, it holds your secrets, URLs and license path
$ docker compose pull && docker compose up -d && rm -rf ./bundle-new

Verify: all containers healthy (docker compose ps), /actuator/health returns 200 on Server and DataServer, docker volume ls identical to before — then run one real search in the CodingUI. Healthy containers plus an empty search result is the classic stale-deployment symptom.

Rollback (loses nothing — volumes were never touched): cp docker-compose.yml.bak docker-compose.yml && cp .env.bak .env && docker compose up -d

Full procedure with the preserved/replaced matrix: §7 “Updating an existing installation” in the install runbooks — Ubuntu / Docker, RHEL 9 / Podman, Rocky 9 / Podman — and the Update & Lifecycle Policy.

Keycloak Admin Console (bundled SSO)

After deployment, manage users and SSO settings:

Admin Consolehttps://HOST:9443/admin
Usernameadmin
Passwordsee KEYCLOAK_ADMIN_PASSWORD in .env

Demo users (for KodeMed application login):

demo-admin / KodeMed2026! (administrator)
demo-coder / KodeMed2026! (medical coder)
demo-approver / KodeMed2026! (approver + coder)

Classification Data (required for coding)

Classification data is distributed as a pre-packaged OCI artifact in Harbor. Pull it with the ORAS CLI before first start:

oras pull harbor.mieresit.com/kodemed/data:2026 -o import-data/

The package contains all required files (~45 files, ~50 MB):

  • ICD-10-GM / CIM-10-GM — ClaML XML + metadata + alphabetical indexes (DE/FR/IT)
  • CHOP — systematic + alphabetical indexes (DE/FR/IT)
  • SwissDRG — Fallpauschalenkatalog + Begleitdokumente (CCL, POA, plausibility)
  • TARPSY + ST Reha — PCG/RCG catalogs + Zusatzentgelte
  • High-Cost Medications — substance lists + Technisches Begleitblatt
  • KodeMed Abbreviations — medical abbreviation dictionaries (DE/FR/IT)
  • Coding Rules — ICD-FAQ + CHOP-FAQ coding guidelines (DE/FR/IT)

The DataServer imports these files automatically on startup. Without data, KodeMed starts but search is not available.

Data sources: ICD-10-GM from BfArM, Swiss translations from BFS, DRG/TARPSY/ST Reha from SwissDRG AG. Packaged by KodeMed AG.

Docker Registry Access

Each customer receives a read-only robot account (robot$...) for the KodeMed Docker registry (harbor.mieresit.com). You must log in before running docker compose up and before any oras pull.

# Non-interactive login (recommended) — credentials stored in ~/.docker/config.json
echo 'ROBOT_SECRET' | docker login harbor.mieresit.com -u 'robot$customer-pull' --password-stdin
echo 'ROBOT_SECRET' | oras   login harbor.mieresit.com -u 'robot$customer-pull' --password-stdin # oras has its OWN credential store

Important: Robot account usernames contain a $ character (e.g., robot$customer-pull). On Linux/macOS, you must wrap the username in single quotes to prevent shell expansion.

Do not use a personal OIDC/SSO account for installs. Harbor only provisions a personal account's CLI secret after the user logs into the browser UI at least once, so docker/oras login from an SSH terminal returns 401 Unauthorized until then. A robot account uses plain name+secret and works headless.

No credentials yet? Contact support@kodemed.ch to request your read-only robot account.

Common errors: authorization failed: no basic auth credentials — run docker login first. 401 Unauthorized from SSH with a personal account — switch to the robot account. Credentials persist until docker logout.

License: A demo license (3 months) is included in the ZIP. For production, contact support@kodemed.ch for a production license. Without a valid license, all API endpoints return HTTP 402.

License file permissions — do this once after unzipping. The containers run as UID 1001, but kodemed.license lands as root:root 0644, so replacing the license from the admin UI later fails with “read-only”. Hand ownership over (no restart needed): chown 1001:1001 kodemed.license && chmod 664 kodemed.license. Verify with docker exec kodemed-server touch /app/kodemed.license — it must exit 0; before the fix it prints Permission denied. Same rule as import-data/: anything the containers must write has to be owned by 1001:1001.

All services start automatically. The CodingUI will be available on port 3000 (or on HTTPS port 443 if using the Caddy reverse proxy).

Service URLs — wizard localhost preset only

These URLs apply to the bundle generated by the wizard’s Localhost preset, which ships its own Keycloak. The canonical demo compose at kodemed.ch uses an external Keycloak (sso.kodemed.ch) and exposes services through Caddy on TLS — do not follow this table when inspecting the live demo.

CodingUIhttp://localhost:3000
Server APIhttp://localhost:8080
DataServer APIhttp://localhost:8081
GrouperServer APIhttp://localhost:8082
Keycloak Adminhttp://localhost:9080 — user admin, password = KEYCLOAK_ADMIN_PASSWORD in your generated .env (random, 20 chars)

Demo users: demo-admin, demo-coder, demo-approver, demo-viewer, demo-auditor, demo-data-admin — password: KodeMed2026!

IP + HTTPS (Caddy) — Service URLs

CodingUIhttps://SERVER_IP
Server APIhttps://SERVER_IP:8443
DataServer APIhttps://SERVER_IP:8444
GrouperServer APIhttps://SERVER_IP:8445
Keycloak Adminhttps://SERVER_IP:9443 — user admin, password = KEYCLOAK_ADMIN_PASSWORD in your generated .env (random, 20 chars)

Self-signed certificate — trusted PER ORIGIN, and every port is its own origin. Caddy generates internal TLS certificates. Accepting the warning on https://SERVER_IP does not cover the API ports: the browser silently refuses the cross-origin call to :8443/:8444/:8445, the request never leaves the browser, and the UI reports a bare “Network Error” with no HTTP status and nothing in the server log. Open each port once and accept:

  1. Open https://SERVER_IP:9443 → accept certificate (Keycloak)
  2. Open https://SERVER_IP:8443 → accept certificate (Server API)
  3. Open https://SERVER_IP:8444 → accept certificate (DataServer)
  4. Open https://SERVER_IP:8445 → accept certificate (Grouper)
  5. Open https://SERVER_IP → accept certificate and use the application

Firefox keeps its own certificate store — separate from the OS trust store and from Chrome/Edge — and remembers the exception per host and port, so these five clicks must be repeated in every browser your users work in. Importing the Caddy root CA into the OS trust store does not help Firefox. If self-signed certificates are unavoidable, prefer the single-hostname preset (setup.sh --single-domain): one hostname on port 443 behind one certificate means exactly one exception instead of five.

DLL client config (kodemed-client-config.json):

{
"serverUrl": "https://SERVER_IP:8443",
"codingUIUrl": "https://SERVER_IP",
"oauth2Url": "https://SERVER_IP:9443",
"trustSelfSignedCertificates": true
}

trustSelfSignedCertificates lets the DLL connect to a server presenting a self-signed TLS certificate. Internal testing only — it disables the check that would otherwise catch an interception, so never enable it in production.

Custom / Wildcard Certificate (production)

To use your own certificate (e.g., wildcard *.hospital.ch) instead of the self-signed certificate, select “Custom certificate” in the wizard’s TLS mode selector. The wizard generates the correct Caddyfile and docker-compose volumes automatically.

Manual setup (if you already have a deployment with self-signed certificates):

  1. Place your certificate files in a certs/ directory next to docker-compose.yml:
    • certs/cert.pem — full chain (server certificate + intermediate CA)
    • certs/key.pem — private key
  2. Add volume mounts to the caddy service in docker-compose.yml:
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
      - ./certs/cert.pem:/certs/cert.pem:ro
      - ./certs/key.pem:/certs/key.pem:ro
  3. In Caddyfile, remove local_certs and replace every tls internal { on_demand } with:
    tls /certs/cert.pem /certs/key.pem
  4. In kodemed-client-config.json, set "trustSelfSignedCertificates": false
  5. Restart: docker compose down && docker compose up -d

A wildcard certificate (e.g. *.hospital.ch) covers a hostname like kodemed.hospital.ch on all ports. The certificate must be in PEM format. Caddy does not auto-renew a custom certificate — renewal is yours to schedule, and an expired one takes the whole stack down at once.

Note: The GrouperServer requires specs/ and catalogues/ directories with SwissDRG/TARPSY/ST Reha specification files. Contact support for the grouper data package, or download from SwissDRG.org.

Windows Client

MSI Installer

No admin rights required. Installs per-user to %LOCALAPPDATA%\KodeMed. Supports silent mode for enterprise deployment (GPO, SCCM, Intune).

Download MSI

Silent install:  msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch"

COM DLL Partners · Delphi x64 / .NET COM 64-bit host process required

The host process must be 64-bit. This is an in-process COM server built for x64, registered in the 64-bit registry view, so a 32-bit process cannot see it: CreateObject("KodeMed.Coding") fails with 0x80040154 (REGDB_E_CLASSNOTREG) on a machine where the MSI installed cleanly. That rules out VB6 (it only ever produces 32-bit executables), 32-bit Office VBA (still a common default) and any 32-bit Delphi build. For those hosts use the REST integration (Integration Guide §3 Option B) instead — there is no 32-bit build of this component.

For HIS / 3rd-party Windows applications that integrate via the COM DLL path (Integration Guide §3 — Option A). The COM component is delivered by the KodeMed MSI: install it in COM-only mode (no coding client) and the installer registers the COM object for you — no extracting, no Unblock-File, no regsvr32.

⚠️ Required prerequisite — .NET Desktop Runtime 9 (x64)

The plain “.NET Runtime” (base) is NOT enough. The COM host is framework-dependent (.NET does not support self-contained COM hosting), so it needs the Microsoft .NET Desktop Runtime 9 (x64) — a different package from the plain “.NET Runtime”. Without it, New-Object "KodeMed.Coding" fails with 0x80008096 (base runtime present but Desktop framework missing / FrameworkMissingFailure), 0x80008093 or 0x80008083 (no .NET at all).

👉 Direct x64 installer: aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x64.exe
(or dotnet.microsoft.com/download/dotnet/9.0 → the “.NET Desktop Runtime 9.0.x” row, x64 — not “.NET Runtime” nor “ASP.NET Core Runtime”).

Verify with dotnet --list-runtimes — the line Microsoft.WindowsDesktop.App 9.x must appear. The MSI checks for it and warns if missing, but a silent (/qn) install suppresses that warning — the install still completes; add the runtime, then COM works (no reinstall). The Coding Client app is self-contained and needs no runtime.

  • KodeMed.msi (COM DLL base + optional Coding Client)Download MSI
  • COM DLL Integration Guide (C# / VB / Delphi — install, config, results, SPIGES)PDF
  • Sample: KodeMed.Com.Test (.NET 4.8 over COM — the VB.NET / 64-bit VBA / Delphi reference)Sources
  • Sample: KodeMed.Test.UI (.NET 9, direct reference — same flow without COM)Sources

Install (COM DLL only)

:: COM DLL only, machine-wide (recommended for a HIS/Opale server). Use /qn (silent):
:: it applies INSTALLCODINGCLIENT=0 without the UI, so the COM-only choice always sticks.
msiexec /i KodeMed.msi INSTALLCODINGCLIENT=0 ALLUSERS=1 /qn ^
        SERVERURL=https://YOUR_HOST:8443

:: COM DLL only, per-user (no admin, registers HKCU):
msiexec /i KodeMed.msi INSTALLCODINGCLIENT=0 /qn SERVERURL=https://YOUR_HOST:8443

:: Full install (COM DLL + Coding Client app):
msiexec /i KodeMed.msi

Pass SERVERURL, or the install writes no configuration at all. Without it the COM object registers correctly and then has no idea which server to talk to. Note the API port :8443, not :443 — :443 is the browser UI. Add TRUSTSELFSIGNED=1 for an evaluation deployment using the bundled self-signed certificate, and LANGUAGE=de|fr|it|en to pin the UI language. You can also skip SERVERURL and drop a config file instead — see just below — but one of the two has to happen.

The installer registers the COM object itself — per-user (HKCU) or machine-wide (HKLM) to match the install scope — so there are no HKCU/HKLM conflicts and no manual step. Uninstall via Settings › Apps removes the registration cleanly. The MSI UI also offers the same choice via the “Install the Coding Client” checkbox.

Fleet / by group: deploy the MSI per-machine via Group Policy Software Installation (assigned to the target OU / security group) with INSTALLCODINGCLIENT=0 ALLUSERS=1.

Configure the DLL (server URLs + self-signed)

Drop a kodemed-client-config.json next to KodeMed.dll — the DLL auto-loads it at startup (no code needed). Start from the bundled kodemed-client-config.example.json:

Where the MSI put KodeMed.dll depends on the install scope you chose above:
  • ALLUSERS=1 (machine-wide) → %ProgramFiles%\KodeMed\ — writing there needs an elevated prompt
  • per-user (no ALLUSERS) → %LOCALAPPDATA%\KodeMed\
If you are unsure which one you have: where /R %ProgramFiles% KodeMed.dll, then where /R %LOCALAPPDATA% KodeMed.dll. Alternatively, point the environment variable KODEMED_CLIENT_CONFIG at the absolute path of a config file, or of the folder holding it (e.g. on a user share) — it is checked first. Lookup order: KODEMED_CLIENT_CONFIG → the KodeMed.dll folder → the host application's EXE folder. The KodeMed log names the file it loaded, or lists every place it searched.

{
  "serverUrl":        "https://YOUR_HOST:8443",   // API + WebSocket — NOT :443 (that's the UI)
  "codingUIUrl":      "https://YOUR_HOST",         // the CodingUI (:443) for the embedded window
  "dataServerUrl":    "https://YOUR_HOST:8444",
  "grouperServerUrl": "https://YOUR_HOST:8445",
  "oauth2Url":        "https://YOUR_HOST:9443",
  "oauth2Realm":      "kodemed",
  "oauth2ClientId":   "kodemed-dll",
  "trustSelfSignedCertificates": true              // ONLY for self-signed / IP (eval)
}

Ports matter (IP + Caddy preset): the API and WebSocket are on :8443, not :443 (the UI). A URL without the port hits the UI and fails with a JSON/HTML or WebSocket 200 vs 101 error.

trustSelfSignedCertificates: true lets the DLL accept a self-signed cert (REST + embedded WebView2). For production, prefer a real cert from your CA (see TLS Certificate Guide) — or import the server CA into the Windows store (CurrentUser\Root needs no admin) — and leave this false. A host app can also pass the config at runtime via SendConfig(), which overrides the file.

Then create the COM object — ProgID = KodeMed.Coding, CLSID = {F6E73053-8D49-480F-AC06-4E0A4882373F}.

// Delphi
var Coding: Variant;
Coding := CreateOleObject('KodeMed.Coding');
Coding.DoCoding(spigesXml, 0);  // 0 = FormatType.SpiGes

' VBA — 64-bit Office only (the 32-bit build cannot load an x64 in-process server)
Dim Coding As Object
Set Coding = CreateObject("KodeMed.Coding")
Coding.DoCoding spigesXml, 0   ' 0 = FormatType.SpiGes

Full reference: Integration Guide §3 Option A — COM DLL Integration.

Windows 10 / 11 .NET 9 Desktop Runtime (x64) — required, see the COM DLL card ~150 MB disk space
Advanced Options — Enterprise deployment (IT administrators)

Silent Install — MSI (GPO / SCCM / Intune)

# Basic — server URL only (language auto-detected, autostart enabled)
msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch"
# Full options — all parameters
msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch" LANGUAGE="de" AUTOSTART=1
# Self-signed / IP server — accept the self-signed cert (test / eval only)
msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://192.168.1.100:8443" TRUSTSELFSIGNED=1
# Citrix / shared desktop — disable autostart
msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch" AUTOSTART=0
# Custom install directory
msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch" INSTALLDIR="D:\Apps\KodeMed"
# Silent install with log file (for troubleshooting)
msiexec /i KodeMed.msi /quiet /norestart /l*v "%TEMP%\KodeMed-install.log" SERVERURL="https://kodemed.hospital.ch"
# Uninstall
msiexec /x KodeMed.msi /quiet /norestart

MSI properties: SERVERURL (required, must be https://) • LANGUAGE — de, fr, it, en (default: auto-detect) • AUTOSTART — 1 or 0 (default: 1) • TRUSTSELFSIGNED — 1 for self-signed certs (default: 0) • INSTALLDIR (default: %LOCALAPPDATA%\KodeMed)
Config location: %LOCALAPPDATA%\KodeMed\kodemed-client-config.json (edit this file to change settings after installation)
Prerequisites: Windows 10+, WebView2 Runtime (pre-installed on Windows 10 21H2+ and Windows 11)

Server URL Format

The Server URL must use HTTPS. The format depends on your server deployment:

Docker + Caddy (IP)https://192.168.1.100:8443
Docker + Caddy (hostname)https://kodemed.hospital.ch:8443
Docker + subdomain proxyhttps://server.kodemed.hospital.ch
OpenShift / Kuberneteshttps://kodemed-server.apps.hospital.ch
Native + reverse proxyhttps://kodemed.hospital.ch (proxy forwards to port 8080)

Note: HTTP URLs are not supported except for http://localhost during development. Use TRUSTSELFSIGNED=1 only with self-signed certificates (Caddy IP mode).

Individual Files

  • KodeMed.msi (full client, or COM-only: INSTALLCODINGCLIENT=0)Download
  • KodeMed.CodingClient.exeDownload

Integration All paths

Start here — pick your integration path

KodeMed integrates five ways. Find your row, then read only the diagrams/cards it points to — you don't need to read everything below.

If you are…PathRead
Windows .NET HIS that can load a DLLA — COM DLLDLL sequence diagram + GetResults schema
Web HIS with desktop coders + CodingClientB — RESTREST flow card
HIS needing real-time pushC — WebSocketCodingClient WebSocket diagram + Message-1.0 schema
Cloud / multi-tenant HIS, own OIDC, iframe embedD — HIS RESTREST flow + Webhook Contract cards (most common)
You only need a DRG / cost weight, no coderE — Grouper DirectGrouper grouping diagram

All field names on the wire are camelCase (canonical). The schema cards near the bottom are the source of truth — validate against them. Each subsection labels what is available today vs planned.

This section documents how a Hospital Information System (HIS) integrates with KodeMed via REST + iframe + outbound webhook, against any OIDC-compliant Identity Provider (Keycloak, Azure AD, Okta, Auth0, Ping, Authelia, …). The contract is HIS-agnostic and IDP-agnostic — KodeMed is not bound to any particular IDP product. The same paths and payloads apply on a self-hosted on-premise deployment and on the cloud-hosted demo.

REST API for HIS Integration

For each case to code, the HIS opens a coding session, embeds the returned URL in an iframe, and either receives the result via webhook or pulls it from a GET endpoint.

1. HIS -> KodeMed : POST /api/v1/coding/session (case payload + Bearer JWT)
2. KodeMed -> HIS : 201 Created { sessionId, instanceId, redirectUrl, expiresAt }
3. HIS UI : embed redirectUrl in <iframe> (CSP must allow it)
4. Coder works in the embedded KodeMed UI, clicks Apply or Discard
5a. Push: KodeMed -> HIS : POST <webhook url> { eventType, sessionId, instanceId, occurredAt, data, codingResult }
5b. Pull: HIS -> KodeMed : GET /api/v1/coding/session/{sessionId}/result/data

Open a coding session

The HIS POSTs the patient SPIGES XML wrapped as data. instanceId is HIS-supplied — it correlates the submission (one POST), not each case inside it. The data payload can carry one or many SPIGES <Fall> elements; they all belong to the same coding session and share the same instanceId. The id MUST be unique — a second POST with an instanceId still in use returns 409 Conflict with the existing sessionId in the body; the id becomes free once the original session completes. KodeMed echoes it back in the webhook so the HIS matches the result against its original submission. sessionId is server-generated.

POST https://kodemed.<your-host>/api/v1/coding/session
Authorization: Bearer <coder JWT from your OIDC IDP>
Content-Type: application/json
 
{
  "data": "<SPIGES XML, one or more <Fall>>",
  "format": "spiges",
  "instanceId": "<your unique correlator for this submission>",
  "source": "API"
}

The 201 response carries sessionId (UUID v4) and redirectUrl. The HIS embeds the redirectUrl in an iframe.

Pull the result (fallback when not using a webhook)

GET https://kodemed.<your-host>/api/v1/coding/session/{sessionId}/result/data
Authorization: Bearer <coder JWT>

Two identifiers, one source of truth

  • instanceId — HIS-supplied correlator for the submitted data payload. One instanceId maps to one POST; the payload may carry one or many SPIGES <Fall> cases — all share this id. MUST be unique across active sessions (duplicate → 409 Conflict). Echoed back in the webhook for HIS-side matching.
  • sessionId — UUID v4 generated by KodeMed when accepting the session. Used by the iframe URL, the result GET, and the webhook payload. Use it for KodeMed-side tracing.

There is no separate correlationId in the contract — only the two above. The OpenAPI spec (linked below) is the source of truth.

Webhook Contract Server-side dispatch — available

For HIS REST integrations, KodeMed fires the webhook server-side when the coder clicks Apply or Discard. The URL, signing strategy, retry schedule and event filter are configured per-IntegrationProfile via the admin API; there are no env vars for this flow. The payload below is the canonical v1.0 wire and validates against WebhookPayload-1.0.schema.json.

For the legacy DLL flow, the HTTP POST is fired by the DLL itself and configured via the KODEMED_HOOK_* env vars on the Server. See the Configuration Reference → DLL Webhook for that path.

Outbound payload

POST https://your-his-host/your/webhook
Authorization: HMAC-SHA256 t=<unix>,v1=<hex> (signs "<t>.<body>"; 5-min skew)
X-KodeMed-Signature: sha256=<hex> (signs raw body; pick either header)
X-KodeMed-Delivery: <uuid> (unique per delivery attempt)
Idempotency-Key: <sessionId>:<eventType> (stable across retries; HIS dedupe)
Content-Type: application/json
 
{
  "eventType": "case.coded" | "case.discarded" | "session.expired",
  "sessionId": "<uuid v4>",
  "instanceId": "<the same correlator the HIS sent in step 1>",
  "occurredAt": "2026-06-03T08:42:11Z",
  "format": "spiges", (present when eventType=case.coded)
  "data": "<final coded SPIGES XML>", (present when eventType=case.coded)
  "codingResult": { "caseId": "...", "siteId": "45679843", "diagnoses": [...], "procedures": [...] },
  "caseResults": [
    { "id": "F-2026-00001", "siteId": "45679843", "codingResult": { ... } },
    { "id": "F-2026-00001", "siteId": "45679844", "codingResult": { ... } }
  ]
}

If you submit more than one case per POST, read caseResults[], not codingResult. One POST may carry many SPIGES <Fall> cases (up to 50 <Standort>), and the singular codingResult describes only the first one — it is byte-identical to caseResults[0].codingResult. A partner submitting ten cases and reading only codingResult books one result and silently loses nine: the coded SPIGES in data still contains all ten, so nothing errors.

The identity is the pair (siteId, caseId), not caseId alone. SPIGES scopes fall_id to a single <Standort>, so two sites of the same operator may legitimately carry the same one. Key your HIS-side booking on the pair. Both fields are additive and optional — a single-case integrator needs no change — but a receiver validating against a pinned pre-2026-09-25 copy of the canonical schemas will reject them; re-fetch the schemas when you upgrade. Full contract: HIS REST Integration Spec §5.3.

Retry schedule

  • 4 attempts: ~30s, ~2min, ~10min between attempts (configurable per profile).
  • HTTP 5xx, 408, 429, network errors → retried.
  • HTTP 4xx (except 408, 429) → terminal → payload moves to the dead-letter queue (DLQ).
  • DLQ replay through the admin API: POST /api/v1/admin/webhook-failures/{id}/replay.

Signature verification (HMAC-SHA256)

When webhook.signing=HMAC_SHA256 is set on the IntegrationProfile, the HIS reads the Authorization: HMAC-SHA256 t=<unix>,v1=<hex> header and re-computes HMAC-SHA256 over <t>.<raw body> with the shared secret. Reject the request if the timestamp is older than 5 minutes (replay protection).

Node receiver:

const crypto = require('crypto');
function verify(req, secret) {
  const auth = req.headers['authorization'] || '';
  const m = auth.match(/^HMAC-SHA256 t=(\d+),v1=([a-f0-9]+)$/i);
  if (!m) return false;
  const [, t, sig] = m;
  if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(t + '.' + req.rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig,'hex'), Buffer.from(expected,'hex'));
}

Python receiver:

import hmac, hashlib, re, time
def verify(headers, raw_body, secret):
  auth = headers.get('Authorization', '')
  m = re.match(r'^HMAC-SHA256 t=(\d+),v1=([a-f0-9]+)$', auth, re.I)
  if not m: return False
  t, sig = m.group(1), m.group(2)
  if abs(time.time() - int(t)) > 300: return False
  mac = hmac.new(secret.encode(), (t + '.' + raw_body).encode(), hashlib.sha256).hexdigest()
  return hmac.compare_digest(mac, sig)

External OIDC IDP Setup Multi-issuer

The HIS keeps its own OIDC-compliant Identity Provider — Keycloak, Azure AD, Okta, Auth0, Ping, Authelia, KodeMed’s bundled Keycloak, any vendor that implements OpenID Connect Discovery + RS256-signed JWTs. KodeMed validates the JWT against the configured issuer; no second login on the KodeMed side, no federation, no token exchange.

Issuer configuration

  • Issuer URL format: whatever your IDP publishes. Common shapes — Keycloak: https://<host>/realms/<realm>; Azure AD: https://login.microsoftonline.com/<tenant>/v2.0; Okta: https://<org>.okta.com/oauth2/default. The exact value is what appears in the iss claim of issued tokens.
  • JWKS auto-discovery via <issuer>/.well-known/openid-configuration — no manual key exchange. KodeMed reads jwks_uri from the discovery document and refreshes keys on rotation.
  • Algorithm: RS256 (asymmetric). HS256 / symmetric secrets are not accepted — they require sharing the signing key with KodeMed, which defeats the IDP boundary.
  • Audience (aud) check: optional; configurable per IntegrationProfile (oidc.audience).
  • Multi-issuer: KodeMed can validate tokens from N issuers (test + prod, or several HIS partners). Each issuer corresponds to one IntegrationProfile — different partners can run different IDP products side by side.
  • The KodeMed host needs HTTPS reachability to the issuer URL.

Required token claims

ClaimUseRequired
subStable user id; used to map the coder to a local KodeMed user record on first login.yes
issMust match the configured IntegrationProfile issuer URL.yes
exp, iatStandard JWT lifetime claims.yes
realm_access.roles or groupsRole names for authorisation (see below). KodeMed reads realm_access.roles first, falls back to groups.yes
audAudience; checked when oidc.audience is configured on the profile.conditional
preferred_username, emailUseful for the coder mapping on first login (audit trail).recommended

Required role names

Configure the following role (or group) names in your IDP so authorisation works by default. On Keycloak that’s a realm role; on Azure AD an app-role mapped through groups; on Okta a group claim. Override the names per profile if the partner cannot use these defaults.

  • kodemed-coder — access to the coding UI and POST /api/v1/coding/session.
  • kodemed-admin — access to admin endpoints (/api/v1/admin/*) including DLQ replay.
  • kodemed-viewer — read-only access (optional; not enforced today, planned).

Iframe Embed + CSP frame-ancestors

The HIS opens KodeMed’s redirectUrl in an <iframe>. KodeMed sets Content-Security-Policy: frame-ancestors to allow only the configured HIS hostnames; everything else is blocked.

It takes two settings, and only the first one unblocks the browser. The framed document is the CodingUI, served by its own proxy — and browsers honour the framed document’s CSP, not the API’s. Setting only the IntegrationProfile leaves the iframe blank with the UI still sending frame-ancestors 'none'.

  1. On the server host — this is the one that unblocks it. Put KODEMED_EMBED_ALLOWED_ORIGINS="https://your-his.example.com" in .env and re-run setup.sh. That single key writes both the UI proxy’s frame-ancestors and the allowlist for the cross-origin token-refresh channel. Unset, the behaviour is today’s: DENY everywhere. Wildcards, bare schemes and paths abort the setup rather than widening the allowlist silently. Full guide: EMBEDDING.md.
  2. Then per IntegrationProfile: embed.cspFrameAncestors: ["https://your-his-test.example.com", "https://your-his.example.com"] (admin API) — the same list, so the API origin’s CSP agrees. Keep the two consistent; GET /api/v1/integration/csp reports what the server believes.
  • Cookies on the KodeMed UI are already SameSite=None; Secure, so cross-origin iframe sessions work without changes on the HIS side.
  • Legacy: the demo deployment historically used X-Frame-Options: DENY. The new flow replaces this with the per-profile CSP frame-ancestors.

OpenAPI Specifications

The full contract is published as OpenAPI 3.1 by every service. Same paths apply on a self-hosted installation — substitute your KodeMed hostname.

Coding Server (REST + webhook) Swagger UI · raw spec
DataServer (classifications, ICD-10, CHOP, ATC) Swagger UI · raw spec
Grouper (DRG, TARPSY, ST Reha) Swagger UI · raw spec

The Authorize button on Swagger UI accepts a test JWT directly, once your realm issuer is configured.

Interconnection Matrix

Which component talks to which. Use this to size a deployment, open firewall holes, or scope a security review. Read each section for a different concern — A is what to expose externally, B is egress your network must allow, C is intra-host wiring (Docker compose / OpenShift), D is local-machine COM bridges.

A. Customer-facing — partner / browser ↔ KodeMed via Caddy on port 443

Open these in the partner firewall. The Public URL column shows illustrative production names (kodemed.ch subdomains); the live demo uses demo-* prefixes, and your on-prem install substitutes its own hostnames.

FromToPublic URLPortAuth
HISKodeMed Serverserver.kodemed.ch443OIDC JWT (partner IdP)
HISKodeMed UI (iframe)coding-ui.kodemed.ch443session cookie + CSP frame-ancestors
Browser (coder)KodeMed UIcoding-ui.kodemed.ch443OIDC code flow
BrowserKodeMed Server (REST + WSS)server.kodemed.ch443OIDC JWT (user)
BrowserDataServer (via Caddy)data.kodemed.ch443OIDC JWT (user)
BrowserGrouper (via Caddy)grouper.kodemed.ch443OIDC JWT (user)
CodingClient (tray)KodeMed Serverwss://server.kodemed.ch/ws/dll443OIDC JWT
BrowserOIDC IdP (auth code flow)your IdP host443cookies / TLS

B. Outbound from KodeMed — egress rules

Allow these in the KodeMed host's outbound firewall. Endpoints reach the public internet (or partner-side webhooks).

FromToProtocolPortAuth
KodeMed ServerHIS webhookHTTPS POSTpartner-definedHMAC-SHA256 over body
KodeMed ServerSMTP relay (feedback)SMTP / STARTTLS587 (or 465)SMTP basic auth
KodeMed ServerGitBucket API (feedback alt)HTTPS443bot token (optional transport)
Keycloak (KodeMed SSO)SMTP relaySMTP / STARTTLS587SMTP basic auth
All servicesOIDC IdP — JWKS fetchHTTPS443public, periodic re-fetch
Customer Docker hostHarbor registryHTTPS443robot account / read-only token

C. Internal — container ↔ container on the host network

These ports are NOT exposed externally. Docker compose wires them on the kodemed network; OpenShift wires them via Services. Open them in firewall ONLY if you split components across hosts.

FromToProtocolInternal portAuth
CaddyKodeMed UI (nginx)HTTP80—
CaddyKodeMed ServerHTTPS REST + WSS8080JWT pass-through
CaddyDataServerHTTPS REST8081JWT pass-through
CaddyGrouperHTTPS REST8082JWT pass-through
KodeMed ServerDataServerHTTPS REST8081service JWT (internal)
KodeMed ServerGrouperHTTPS REST8082service JWT (internal)
GrouperDataServerHTTPS REST8081service JWT (internal)
DataServerSearchServer (DE/FR/IT, optional)HTTP REST8083 / 8084 (native), 8080 (container)X-Internal-Token shared secret
Server / DataServer / KeycloakPostgreSQLTCP / TLS5432password (encrypted at rest)

D. Local-machine — COM DLL bridge (Windows host only)

In-process COM activation + a loopback WebSocket between the COM DLL and the CodingClient tray. Nothing leaves the workstation.

FromToProtocolPortAuth
HIS (in-process)KodeMed COM DLLCOM / DCOM—local user
KodeMed COM DLLCodingClient (tray)WebSocket loopback9876local token

Auth column conventions — OIDC JWT (user) is the browser-bearer access token (audience: kodemed-ui). service JWT (internal) is the audience-restricted token issued by KodeMed Server to call DataServer / Grouper. HMAC-SHA256 applies only to HIS webhooks. All HTTPS hops carry standard security headers (HSTS, CSP, X-Frame-Options) enforced by SecurityConfig. See Roles & Permissions for the role-name table.

Sequence — HIS REST integration

Happy-path flow from HIS-initiated session create to webhook delivery + result fetch.

sequenceDiagram
    autonumber
    participant HIS as HIS
    participant IDP as Partner OIDC IDP
    participant S as KodeMed Server
    participant UI as KodeMed UI
    participant WH as HIS webhook

    HIS->>IDP: client_credentials
    IDP-->>HIS: JWT
    HIS->>S: POST /api/v1/coding/session
{ data, format:"spiges", source:"API", instanceId } S->>S: validate JWT (JWK) S->>S: persist session S-->>HIS: 201 { sessionId, instanceId, redirectUrl, expiresAt } HIS->>UI: load redirectUrl in iframe Note over UI: coder edits, presses Apply UI-->>S: result S->>WH: HMAC-sign + POST (Authorization + X-KodeMed-Signature)
{ eventType, sessionId, instanceId, occurredAt,
format, data, codingResult } WH-->>S: 2xx opt HIS without webhook HIS->>S: GET /api/v1/coding/session/{id}/result/data S-->>HIS: 200 data (SPIGES) end

If the webhook gets a 5xx/timeout, KodeMed retries with the per-tenant webhookRetryDelaysSeconds (default 30s, 2m, 10m) and persists to the dead-letter queue if all attempts fail (replay via POST /api/v1/admin/webhook-failures/{id}/replay).

Sequence — DLL / CodingClient flow Legacy

In-process integration via the COM DLL for HIS that cannot make HTTPS calls but can load a Windows DLL. Same end-state (modified SPIGES returned to HIS) but the transport is COM ↔ loopback WebSocket ↔ KodeMed Server.

sequenceDiagram
    autonumber
    participant HIS as HIS process
    participant DLL as COM DLL
    participant CC as CodingClient (tray)
    participant S as KodeMed Server
    participant B as Browser

    HIS->>DLL: StartCoding(spiges)
    DLL->>CC: ws://127.0.0.1:9876
    CC->>S: wss /ws/dll
(Authorization: Bearer JWT) S-->>CC: dll.connected CC->>S: app.coding_session_launch
{ sessionId, data, format } S->>B: open in browser Note over B: coder edits, presses Apply S-->>CC: case.coded
{ sessionId, payload: { action:"APPLIED", data, codingResult } } CC-->>DLL: result DLL-->>HIS: result (SPIGES)

The DLL writes the modified SPIGES back to the HIS in-process. No outbound webhook is fired in this flow — the HIS already has the result by the time StartCoding returns.

Sequence — Webhook retry + dead-letter

Failure path. Server retries on 5xx / 408 / 429 / network errors with the per-tenant webhookRetryDelaysSeconds schedule; after the last attempt the payload is persisted to the WebhookFailure dead-letter table and an admin can replay it.

sequenceDiagram
    autonumber
    participant S as KodeMed Server
    participant WH as HIS webhook
    participant DLQ as WebhookFailure (DLQ)
    participant ADM as Admin

    S->>WH: attempt 1 (POST)
    WH-->>S: 503
    Note over S: wait 30s
    S->>WH: attempt 2 (POST)
    WH-->>S: timeout
    Note over S: wait 2m
    S->>WH: attempt 3 (POST)
    WH-->>S: 502
    Note over S: wait 10m
    S->>WH: attempt 4 (POST)
    WH-->>S: 502
    S->>DLQ: persist row
{ profileSlug, sessionId, instanceId,
eventType, payload, attempts:4,
lastError, lastHttpStatus } ADM->>DLQ: GET /api/v1/admin/webhook-failures DLQ-->>ADM: 200 [ {id,…}, … ] ADM->>S: POST .../{id}/replay S->>WH: replay POST WH-->>S: 200 S->>DLQ: mark row delivered

Retry schedule + max attempts are stored on the IntegrationProfile per partner. Default is 4 attempts at 30s / 2m / 10m (3 delays = retryMax − 1). Idempotency: the Idempotency-Key header is stable across retries so the HIS can dedupe.

Sequence — REST polling fallback

For HIS that cannot host a public webhook endpoint. Create the session, poll for completion. Same end-state as the webhook flow but driven by the HIS.

sequenceDiagram
    autonumber
    participant HIS as HIS
    participant S as KodeMed Server
    participant UI as KodeMed UI

    HIS->>S: POST /api/v1/coding/session
    S-->>HIS: 201 { sessionId, instanceId, redirectUrl, expiresAt }
    HIS->>UI: open redirectUrl in iframe
    Note over UI: coder edits, presses Apply
    UI-->>S: result
    loop poll every 5-30s
        HIS->>S: GET /api/v1/coding/sessions/{sessionId}/status
        S-->>HIS: 200 { status:"COMPLETED", action:"APPLIED" }
    end
    HIS->>S: GET /api/v1/coding/session/{id}/result/data
    S-->>HIS: 200 data (SPIGES)

Recommended poll interval: 5s while the iframe is visible, 30s after it closes. The expiresAt on the create response upper-bounds how long the session is pollable.

Sequence — CodingClient WebSocket /ws/dll

For native desktop integrations that already speak WebSocket. Skip the DLL transport; speak /ws/dll directly to the Server. Same message vocabulary as the DLL flow.

sequenceDiagram
    autonumber
    participant NC as Native client
    participant S as KodeMed Server
    participant B as Browser (UI)

    NC->>S: wss /ws/dll
(Authorization: Bearer JWT
or Sec-WebSocket-Protocol: kodemed.bearer.JWT) S-->>NC: dll.connected { instanceId } NC->>S: app.coding_session_launch
{ instanceId, data:SPIGES, format:"spiges" } S-->>NC: session.created { sessionId } S->>B: open in browser Note over B: coder Apply B-->>S: result S-->>NC: case.coded
{ sessionId, payload: { action:"APPLIED", data, codingResult } }

Token auth: prefer Authorization: Bearer <JWT> (server-to-server clients); browsers SHOULD use Sec-WebSocket-Protocol: kodemed.bearer.<JWT> instead. The legacy ?token=<JWT> query param is still accepted but logs a WARN — tokens land in access logs as plaintext. The token is validated at the WebSocket handshake (not per frame). After expiry or any disconnect, reconnect with a refreshed token; the server holds the session-id mapping in-memory for sessionExpiryMinutes.

Sequence — DataServer thesaurus lookup

For HIS that wants to autocomplete ICD-10 / CHOP / ATC codes in its own UI before opening a coding session. Direct REST call against the DataServer.

sequenceDiagram
    autonumber
    participant UI as HIS UI
    participant DS as DataServer
    participant PG as PostgreSQL

    UI->>DS: GET /api/thesaurus/search?q=J96.0
Authorization: Bearer JWT DS->>PG: SELECT … LIMIT 20 PG-->>DS: rows DS-->>UI: 200 [ { code, label, version, … } ] UI->>DS: GET /api/thesaurus/rules/code/ICD10/J96.0?version=2026 DS->>PG: SELECT rules WHERE code=$1 AND version=$2 PG-->>DS: rows DS-->>UI: 200 { rules:[ { kind:"INCLUSION", text:"…" }, … ] }

Cached at If-None-Match / ETag; year-versioned so a 2026 lookup can never silently fall back to 2025 data. Full schema in the DataServer Swagger UI.

Sequence — Grouper grouping

For HIS that wants to run a real-time SwissDRG / TARPSY / ST Reha grouping without involving a coder. Direct REST call against the Grouper.

sequenceDiagram
    autonumber
    participant HIS as HIS
    participant G as Grouper
    participant DS as DataServer

    HIS->>G: POST /api/v1/grouper/group
{ tariff:"SWISSDRG", version:"15.0",
patientCase:{ fallId, administratives, diagnosen:[…], behandlungen:[…] } } G->>G: load specs + catalogue (cached) G->>DS: POST /api/zusatzentgelt/match DS-->>G: supplements [] G-->>HIS: 200 { drg:"E77C", pccl:0,
effectiveCostWeight:0.638, supplements:[…], flags:[…] }

Three tariffs: SWISSDRG (acute), TARPSY (psychiatric), STREHA (rehab) — set tariff in the body. Entry points: POST /api/v1/grouper/group (one case), /group/batch (many cases, JSON), and /spiges/xml/{tariff} (raw SPIGES XML, parses every <Fall>). These are server-to-server calls — a backend HTTP client sends no browser Origin header, so CORS never applies; the CORS allow-list only governs browser-origin calls. GET /api/v1/grouper/versions lists the loaded tariff × version combinations. Response fields (per GroupingResponse): drg, mdc, pccl, baseCostWeight/effectiveCostWeight, supplements, flags, grouperStatus.

Sequence — OIDC login + iframe session cookie

How the clinician's JWT becomes a same-site cookie that survives the iframe load when the UI is embedded inside the HIS page.

sequenceDiagram
    autonumber
    participant B as Browser (HIS page)
    participant IDP as Partner OIDC IDP
    participant UI as KodeMed UI
    participant S as KodeMed Server

    B->>UI: iframe src=redirectUrl
    UI->>S: /actuator/info
    S-->>UI: 200
    UI->>UI: load runtime-config (issuer URL, etc.)
    B->>IDP: (silent) /authorize?prompt=none
    IDP-->>B: 302 with code
    B->>IDP: /token (PKCE)
    IDP-->>B: id_token + access_token + refresh
    B->>UI: set SameSite=None Secure cookie
    UI->>S: REST + WS calls
Authorization: JWT

The HIS site must be listed in IntegrationProfile.embedCspFrameAncestors for the browser to accept the iframe. The SameSite=None cookie attribute is mandatory for cross-origin iframe sessions and is the default for HIS REST integrations.

Schemas JSON Schema + XSD v1.0 canonical

Validate every wire payload your integration touches against the published schemas. All four wire transports (HIS REST request/response, HIS REST webhook, WebSocket /ws/dll, COM GetResults()) share the same field names + types — one parser per concept on the HIS side. The repo's scripts/test/test-schemas-uniformity.py enforces this on every CI run; partners SHOULD validate their code against the same schemas.

HIS REST (request → response → webhook)

WebSocket (CodingClient ↔ Server, full-duplex)

COM DLL (in-process integration, IKodeMed.GetResults())

Case data envelope (SPIGES XML carried inside data)

Shared building blocks (referenced via $ref from the flow schemas above)

Canonical samples (validator-checked on every CI run)

See schemas/README.md for the 15 uniformity rules (R1–R15) and the canonical-vs-legacy migration notes. C# / Java / Python validator examples live in the Test.UI partner sample below.

Real test data — the same SPIGES / BFS payloads we use ourselves (CodingUI public landing page + installer demo). Pipe these through your integration end-to-end without inventing your own fixtures. See the data/ README for the full curl walkthrough.

  • data/session-create-request.json — full REST request body (POST /api/v1/coding/session) with 3-case SPIGES embedded — validates against SessionCreate-1.0
  • data/sample-spiges.xml — minimal SPIGES 1.4 / 1.5 demo, 3 cases (acute / surgical / rehab)
  • data/test-data-full.xml — realistic Swiss medical cases, ICD-10-GM CH + CHOP 2026 codes (the "Load realistic test data" payload on the CodingUI public page)
  • data/test-data-anonymized.bfs.dat — BFS pipe-delimited format (MX/MB/MD lines). Set format=bfs on the REST call to exercise BfsTransformer.

Negative examples — one per flow, each intentionally invalid. Use them to validate that your receive-side validator correctly REJECTS malformed input (R10 in the drift detector enforces this end-to-end).

Reference Sample — KodeMed.Test.UI C# / WinForms Apache 2.0

A single-window C# WinForms app that exercises the COM DLL integration surface end-to-end (SendConfig + DoCoding + DoCodingWithFormat + GetResults). The DLL handles REST session create, iframe embed, and WebSocket signalling internally on the partner's behalf — partners reading this sample see only the COM call site. Partners integrating via the HIS REST contract directly (server-to-server, no DLL) MUST implement their own webhook receiver per KodeMed_HIS_REST_Integration_Spec §5.3 and validate against WebhookPayload-1.0.schema.json — the sample's README has the receive-side validator snippets.

It does not build straight out of the zip. Outside our monorepo the project file references lib\KodeMed.dll, which is deliberately not in the archive — it is a signed binary published separately, above. The bundled script fetches it for you:

# Option 1 — clone the GitBucket repo (always up to date)
git clone ssh://git@www.mieresit.com:7777/kodemed/KodeMed.Test.UI.git
cd KodeMed.Test.UI
run-sample.cmd                 # pulls, downloads KodeMed.dll, builds, runs

# Option 2 — offline zip
unzip KodeMed.Test.UI-sources.zip
cd KodeMed.Test.UI
run-sample.cmd

# Option 3 — by hand, if you would rather not run the script
curl -fLo lib\KodeMed.dll https://<this-portal>/windows/KodeMed.dll
curl -fLo lib\KodeMed.xml https://<this-portal>/windows/KodeMed.xml
dotnet restore
dotnet run

Needs the .NET 9 SDK on Windows x64. run-sample.cmd -SkipDll -SkipPull is the fast rebuild once the DLL is in place.

Troubleshooting

"authorization failed: no basic auth credentials" on docker compose up

Log in to Harbor before pulling — twice. oras keeps its own credential store, so a successful docker login does nothing for the oras pull that fetches the classification data; that is the classic second 401.

echo 'ROBOT_SECRET' | docker login harbor.mieresit.com -u 'robot$YOUR_ROBOT' --password-stdin echo 'ROBOT_SECRET' | oras login harbor.mieresit.com -u 'robot$YOUR_ROBOT' --password-stdin

Use the robot account (robot$…) KodeMed AG gives you — not a personal OIDC/SSO login, which returns a short-lived token and fails again later. Robot usernames contain $, so quote them with single quotes. Piping the secret to --password-stdin keeps it out of your shell history and out of ps. Both logins persist (~/.docker/config.json and ~/.config/oras/) and are done once per machine — as the same user that will run the pull.

DLL client opens wrong URL (404 on coding page)

KODEMED_PUBLIC_UI_URL is not set. The server returns its own URL instead of the CodingUI URL.

KODEMED_PUBLIC_UI_URL=https://coding-ui.hospital.ch

The server logs a warning at startup: CONFIG ⚠ KODEMED_PUBLIC_UI_URL is not set

WebSocket returns HTTP 200 instead of 101

Reverse proxy is not upgrading WebSocket connections. Enable mod_proxy_wstunnel (Apache) or add Upgrade headers (nginx).

# Apache
a2enmod proxy_wstunnel rewrite
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteRule ^/ws/(.*) ws://localhost:8080/ws/$1 [P,L]
WebSocket returns HTTP 400 — but only for users who come from Active Directory / an external IdP

A local Keycloak user connects; a federated one does not. The DLL reports The server returned status code '400' when status code '101' was expected and retries until it gives up. Nothing looks wrong with the credentials, because nothing is.

The cause is the size of the token. A user carrying a few hundred AD groups as realm roles produces a JWT of several kilobytes. The DLL sends it in the WebSocket URL, so it is the request line that exceeds Tomcat's 8 KB default — and Tomcat rejects the request before the application ever sees it.

How to recognise it with certainty: the server log shows nothing at all for those attempts. A 400 the application produced would be logged; these are not. If you also see reason=no_token entries with a perfectly readable instanceId, those are a different and harmless thing — the client connecting before it has finished logging in.

# .env — then recreate the containers (a restart keeps the old environment)
SERVER_MAX_HTTP_REQUEST_HEADER_SIZE=65536
docker compose up -d

Deployments generated from this wizard ship with 65536 already set. Older ones do not, and a version change alone will not add it — editing IMAGE_TAG moves the images and leaves the compose untouched.

The durable fix belongs in the identity provider: map only the groups KodeMed needs onto realm roles, instead of emitting all of them. The token travels on every request, not just this one.

Login fails after entering credentials (page reloads or shows error)

After entering the correct username and password, the page reloads back to the login form or shows a connection error. This is usually caused by a KC_HOSTNAME mismatch in the .env file.

Cause: Keycloak uses KC_HOSTNAME and KC_HOSTNAME_PORT to construct redirect URLs after authentication. If these default to localhost:9080, the browser is redirected to https://localhost:9080/... which does not exist.

Fix: In .env, set the correct values for your setup:

# IP-based access via Caddy
KC_HOSTNAME=192.168.1.100
KC_HOSTNAME_PORT=9443
# Hostname-based access via Caddy
KC_HOSTNAME=kodemed.hospital.ch
KC_HOSTNAME_PORT=9443

Then restart: docker compose down && docker compose up -d

Tip: Also verify OIDC_ISSUER_URI matches. It should be https://YOUR_HOST:9443/realms/kodemed.

"admin" account does not work on the login page

The admin account is the Keycloak administrator (master realm). It only works at the Keycloak admin console: https://HOST:9443/admin.

To log into the KodeMed application, use one of the demo accounts:

demo-admin / KodeMed2026! (administrator)
demo-coder / KodeMed2026! (medical coder)
demo-approver / KodeMed2026! (approver + coder)
demo-viewer / KodeMed2026! (read-only)
demo-auditor / KodeMed2026! (audit trail access)
demo-data-admin / KodeMed2026! (data import admin)

The Keycloak admin password is in your .env file: KEYCLOAK_ADMIN_PASSWORD.

Self-signed certificate: browser blocks API calls or login

When using Caddy with self-signed certificates, each port has a separate certificate. The browser must accept each one individually before the application works.

Open these URLs in order and accept the certificate warning on each one:

  1. https://HOST:9443 — Keycloak (required for login)
  2. https://HOST:8443 — Server API (required for data)
  3. https://HOST:8444 — DataServer API (required for classifications)
  4. https://HOST — CodingUI (the application)

If you skip a port, the browser silently blocks requests to that service and the app appears broken. For production, use a custom certificate (see the Deployment Wizard → TLS Certificate Mode → Custom certificate).

Login works but UI is empty / shows "CORS Failed" / WebSocket error 1015 in console

You log in successfully (the console shows Logged in as: demo-coder), but the UI stays empty or hangs on a spinner. The browser console contains one or more of:

  • CORS Failed on XHROPTIONS https://HOST:8443/api/v1/...
  • Code d'état : (null) / Status: (null) — the preflight never received a response
  • [WebSocket] Connection error followed by code: 1015
  • Firefox ne peut établir de connexion avec le serveur à l'adresse wss://HOST:8443/ws/dll...

Cause: WebSocket close-code 1015 is the standard signal for TLS handshake failure. The browser has accepted the self-signed cert for the UI port (:443) but NOT for the API port (:8443), so the XHR preflight and the WSS upgrade both die before they reach the server — that's why the XHR shows Status: (null) instead of a CORS-specific 403. The JWT itself is fine (you can decode it and confirm allowed-origins covers your host); the issue is purely TLS trust at the browser level.

Fix: open https://HOST:8443/actuator/health in a new tab, click Advanced → Proceed, confirm the response is {"status":"UP"}, then reload the UI tab. Do the same for :8444 and :8445 if their endpoints are called from the UI. Self-signed cert exceptions are tracked per origin (scheme + host + port), so the exception you clicked through on :443 does NOT cover :8443.

Verify it's TLS and not CORS: if the response status were a non-null number (e.g. 403) you'd be looking at a real CORS misconfiguration — check CORS_ALLOWED_ORIGINS in .env. The (null) status is the giveaway that the request never even completed the TLS handshake.

Custom certificate: Caddy refuses to start or shows TLS error

Check these common causes:

  • Wrong format: Certificates must be PEM-encoded (base64 text, not binary DER). Convert with: openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM
  • Incomplete chain: cert.pem must contain the full chain: server certificate + intermediate CA(s). Missing intermediates cause “certificate signed by unknown authority” on some clients.
  • Key mismatch: Verify cert and key match: openssl x509 -noout -modulus -in certs/cert.pem | md5sum must equal openssl rsa -noout -modulus -in certs/key.pem | md5sum
  • File permissions: Caddy runs as non-root. Ensure certs/cert.pem and certs/key.pem are readable: chmod 644 certs/cert.pem and chmod 600 certs/key.pem — the certificate is public, the private key must not be

Check Caddy logs: docker compose logs caddy

UI shows blank page (no content loads)

The browser opens the KodeMed URL but the page is empty or shows a white screen. This is usually caused by a misconfigured runtime-config.js.

What is runtime-config.js? This file tells the CodingUI where to find the backend services (Server, DataServer, Grouper, Keycloak). If the URLs are wrong or the file is missing, the UI cannot load.

Docker deployment: Check the mounted file:

docker exec kodemed-ui cat /usr/share/nginx/html/runtime-config.js

Native install: Check the generated file:

cat /opt/kodemed/ui/runtime-config.js

All URLs must be reachable from the user’s browser (not from the server). Common fixes:

  • Replace localhost with the server’s IP or hostname
  • Ensure the protocol matches your setup: https:// for Caddy/proxy, http:// only for localhost testing
  • After editing, restart the UI: docker compose restart kodemed-ui or sudo systemctl restart kodemed-ui
ERR_SSL_PROTOCOL_ERROR on port 3000

You are accessing https://HOST:3000 but port 3000 is the internal UI port that only speaks HTTP. This port should never be accessed directly with HTTPS.

Docker + Caddy: Access the UI via Caddy on port 443: https://HOST (no port needed).

Native install (localhost only): Use http://localhost:3000. For remote access, set up a reverse proxy (nginx, Apache) that terminates TLS and forwards to port 3000.

Important: HTTP is only supported for localhost testing. All production deployments must use HTTPS via a reverse proxy or Caddy.

Native install: services fail with “Connection refused” (PostgreSQL)

The server cannot connect to PostgreSQL. The native installer does not install PostgreSQL — you must set it up before running install-kodemed.sh.

# Install PostgreSQL
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
# Create database and user
sudo -u postgres psql -c "CREATE USER kodemed WITH PASSWORD 'YOUR_PASSWORD';"
sudo -u postgres psql -c "CREATE DATABASE kodemed OWNER kodemed;"
# Verify connection works
psql -h localhost -U kodemed -d kodemed -c "SELECT 1"

Then update /etc/kodemed/kodemed.env with the correct password and restart: sudo systemctl restart kodemed-server kodemed-dataserver kodemed-grouperserver

Login redirect loop: "Invalid scopes: openid profile email"

The browser keeps redirecting between the app and Keycloak with error=invalid_scope in the URL. This means the OIDC client scopes are not assigned in Keycloak.

Docker deployment (bundled Keycloak): The keycloak-init container fixes this automatically. Check its logs:

docker compose logs keycloak-init

If it shows "WARNING: kodemed-ui not found", re-run the realm initialiser:

docker compose up -d --force-recreate keycloak-init

If the realm itself needs rebuilding, remove only the Keycloak database volume — never every volume:

docker compose stop keycloak keycloak-init
docker volume rm <project>_keycloak-postgres-data
docker compose up -d

⚠ Never use docker compose down -v for this. The -v takes every volume — the KodeMed database, the thesaurus and all three Lucene indexes — when the problem lives only in the Keycloak realm. kodemed-install.sh and kodemed-update.sh both refuse that argument for the same reason.

Native / external Keycloak: In Keycloak Admin → Realm "kodemed" → Clients → kodemed-ui (and kodemed-dll) → Client scopes tab, verify these default scopes are assigned: openid, profile, email, web-origins, acr, basic, roles.

CORS error after login: "Access-Control-Allow-Origin missing"

The token exchange from the UI to Keycloak is blocked by CORS. In Keycloak Admin → Realm "kodemed" → Clients → kodemed-ui → Settings, add your UI origin to Web Origins:

http://localhost:3000
https://coding-ui.hospital.ch

The + wildcard should resolve origins from redirect URIs, but adding explicit origins is more reliable.

UI loads but shows blank page or login loop (network deployment)

The runtime-config.js uses localhost but you are accessing the UI from another machine. The browser tries to reach Keycloak/Server on localhost (the user's PC), which fails.

Fix: Replace localhost with the server's IP or hostname in runtime-config.js:

sed -i 's|localhost|192.168.1.100|g' runtime-config.js
docker compose restart kodemed-ui

Also update KC_HOSTNAME and CORS_ALLOWED_ORIGINS in .env to match the server IP/hostname, then restart Keycloak:

KC_HOSTNAME=192.168.1.100
CORS_ALLOWED_ORIGINS=http://192.168.1.100:3000
WEBSOCKET_ALLOWED_ORIGINS=http://192.168.1.100:3000
Portal returns 500 on login

Check oauth2-proxy logs: docker compose logs kodemed-downloads-auth

unauthorized_client → Client secret mismatch with OIDC provider
invalid_scope → Remove groups from OAUTH2_PROXY_SCOPE
invalid_redirect_uri → Check redirect URI matches OIDC client config

Server startup configuration warnings

Check logs after deployment:

docker compose logs kodemed-server | grep "CONFIG"

CONFIG ✓ All public URL configuration present — all good
CONFIG ⚠ KODEMED_PUBLIC_* — specific variable is missing

Required Environment Variables

VariableRequiredPurpose
KODEMED_PUBLIC_UI_URLYesCodingUI URL for browser redirect
KODEMED_PUBLIC_SERVER_URLRecommendedServer URL for DLL clients
KODEMED_PUBLIC_DATASERVER_URLRecommendedDataServer URL for classification data
KODEMED_PUBLIC_WEBSOCKET_URLRecommendedWebSocket URL for real-time communication
KODEMED_PUBLIC_GROUPER_URLRecommendedGrouperServer URL for DRG grouping
OIDC_ISSUER_URIYesOIDC issuer for token validation
CORS_ALLOWED_ORIGINSYesAllowed origins for API requests

Documentation

All technical documentation is in English. The product overview and the slide deck are available in four languages on the product page.

Architecture & integration

Deployment & operations

Sign-in & identity provider

Policies

Technical Documentation Confidential

Integration & API

  • GetResults() Return Format MD PDF

Classification Data

All classification data is pre-packaged and distributed via Harbor OCI artifacts. Pull with the ORAS CLI (one-line install for Linux/macOS/Windows):

# Classification data (ICD-10, CHOP, SwissDRG, TARPSY, ST Reha, medications, abbreviations, coding rules)
oras pull harbor.mieresit.com/kodemed/data:2026 -o import-data/
 
# Grouper specifications (.sgs files) — provided separately with customer contract
# Contact support@kodemed.ch for grouper spec files

On first deployment, files in import-data/ are imported automatically when the DataServer starts. For re-imports, use the DataServer admin API. See the DevOps Guide §14 for details.

Original data sources (packaged by KodeMed AG):

Deployment Architecture

  • Native Linux PNG
  • Docker Compose PNG
  • OpenShift / Kubernetes PNG

DevOps Guide Confidential

Server Administration & Deployment

  • DevOps Guide (EN) PDF

Markdown Documentation

  • DevOps & Server Admin Guide MD

Compose layers

KodeMed ships in three independently composable layers. The same container images run in every customer deployment; what differs is which layers you compose alongside and where TLS terminates.

+----------------------------------------------------------------+
| IDP layer (OIDC Identity Provider) |
| bundled Keycloak (turnkey, federates LDAP/AD) |
| or your existing Keycloak / Azure AD / Okta / Auth0 / ... |
+----------------------------------------------------------------+
^
| OIDC (JWT validation via JWK)
+----------------------------------------------------------------+
| TLS layer (terminate HTTPS at the edge — one of): |
| bundled Caddy (auto Let's Encrypt or custom cert) |
| your reverse proxy (nginx / Apache / HAProxy / Traefik) |
| OpenShift / Kubernetes Ingress |
+----------------------------------------------------------------+
^
| HTTP on the Docker bridge
+----------------------------------------------------------------+
| App layer (always present — kodemed-* services + Postgres) |
| postgres (state) |
| kodemed-server : 8080 (REST + WebSocket) |
| kodemed-dataserver : 8081 (thesaurus / classifications) |
| kodemed-grouper : 8082 (DRG / TARPSY / ST Reha) |
| kodemed-ui : 3000 (React frontend) |
| kodemed-downloads : 8090 (this portal, optional) |
+----------------------------------------------------------------+

Important callouts:

  • The app layer’s compose file does not include the TLS layer or the IDP layer — both are pluggable. The Deployment Wizard ships overlay compose files that opt-in to bundled Caddy and / or bundled Keycloak.
  • Run with overlays via docker compose -f docker-compose.yml -f docker-compose.caddy.yml -f docker-compose.keycloak.yml up -d. Drop any overlay you don’t need. This is the repository layout, not what you were given: a Deployment Wizard ZIP ships ONE already-merged docker-compose.yml with the TLS and IDP choices you made baked in, and contains no -f overlay files at all. In a wizard deployment just run docker compose up -d.
  • The overlays need Compose 2.24+: they use ports: !override [] to strip the host bindings. An older Compose does not understand the tag, the port lists merge instead of being replaced, and every service stays published on 0.0.0.0 — the stack comes up healthy and nothing in any log mentions it.
  • App-layer containers listen only on the Docker bridge by default — they never bind to public ports without an explicit TLS layer in front. That holds for the bundled Caddy overlay, your own reverse proxy, and the OpenShift / Kubernetes path.
  • The wizard preset localhost bundles everything for evaluation (Keycloak on :9080 — plain HTTP, no Caddy in this preset — and the services on localhost ports). The wizard preset saas-domain bundles Caddy with Let’s Encrypt and points at an external IDP. See the Deployment Wizard section.

Security & Compliance

Authentication

  • OAuth2 / OpenID Connect (SSO)
  • Keycloak, Azure AD, Okta compatible
  • JWT token validation on every request
  • Per-client credentials (DLL, UI, Portal)
  • Role-based access: kodemed-admin, -coder, -coder-controller, -approver, -auditor. The kodemed-coder-controller supervisor role may correct a case's admission/discharge values (opt-in per instance via kodemed.admin-edit.enabled), audited

Data Protection

  • GDPR Art. 32 & Swiss nFADP (nDSG) Art. 8 — AES-256-GCM encryption at rest (enabled by default by the installer)
  • TLS 1.2+ on all external / client-facing traffic; services run on an isolated private network
  • No persistent patient data on the client — browser session data is ephemeral & AES-256-GCM encrypted
  • Configurable data retention with automatic purge (default 90 days)

Audit & Logging

  • Full audit trail for all coding sessions (incl. admission/discharge corrections)
  • User, timestamp, action, IP address
  • Structured JSON logs (ELK / Splunk ready)
  • 90-day default retention (configurable)

Container Security

Hardening varies by deployment mode. The canonical demo compose is a development convenience and is not hardened. Production must use Helm or the wizard preset.

Setting Helm (OpenShift) Wizard preset Demo compose
runAsNonRootyes (1001 / 101)UI only (101)no
readOnlyRootFsyesUI, Keycloak, Postgresno
cap_drop ALLyesUI, Keycloak, Postgresno
no-new-privilegesyesyes (all services)no
Network policyyesno (flat network)no
seccomp profileyesnono

Wizard and Helm hardening for the Java services (Server, DataServer, Grouper) is being aligned; ask KodeMed AG for the current state if it matters to your review.

Licensing

  • RSA-4096 signed offline license file
  • Per-component validation (Server, DataServer, Grouper)
  • License types: DEMO, TRIAL, PRODUCTION
  • API status: GET /api/v1/license/status

Configuration Reference

Complete list of all environment variables, settings, and config keys across all KodeMed components. Use the filter to search.

KodeMed Server Port 8080

Main coding API — Spring Boot backend handling DLL sessions, WebSocket, persistence, webhooks.

Environment VariableDefaultDescription
SERVER_PORT8080HTTP server port
SPRING_PROFILES_ACTIVE(none)Active Spring profiles (use prod for production)
SPRING_DATASOURCE_URLjdbc:h2:mem:kodemedJDBC connection URL (PostgreSQL in prod)
SPRING_DATASOURCE_USERNAMEsaDatabase username
SPRING_DATASOURCE_PASSWORD(empty)Database password
SPRING_DATASOURCE_DRIVER_CLASS_NAMEorg.h2.DriverJDBC driver class (org.postgresql.Driver in prod)
SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE15HikariCP max connection pool size
SPRING_DATASOURCE_HIKARI_MINIMUM_IDLE5HikariCP minimum idle connections
SPRING_DATASOURCE_HIKARI_IDLE_TIMEOUT300000HikariCP idle timeout (ms)
SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT20000HikariCP connection timeout (ms)
SPRING_JPA_HIBERNATE_DDL_AUTOupdateHibernate DDL strategy (consider validate for prod)
Authentication is mandatory and not configurable. The legacy KODEMED_AUTH_ENABLED / GROUPER_AUTH_ENABLED flags have been removed — JWT validation runs on every endpoint that is not explicitly listed as public in SecurityConfig.
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI(Keycloak URL)OIDC issuer URI for JWT validation
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI(Keycloak certs URL)JWK Set URI for JWT validation
CORS_ALLOWED_ORIGINS(empty)Comma-separated CORS allowed origins
WEBSOCKET_ALLOWED_ORIGINS(empty)WebSocket allowed origins
KODEMED_ENCRYPTION_KEY(empty)AES-256 key (base64, 32 bytes). Generate: openssl rand -base64 32
KODEMED_LICENSE_FILE(auto-search)Path to kodemed.license file
KODEMED_PUBLIC_SERVER_URL(auto-detected)Public URL for the server API (returned to DLL)
KODEMED_PUBLIC_DATASERVER_URL(auto-detected)Public URL for DataServer (returned to DLL)
KODEMED_PUBLIC_GROUPER_URL(auto-detected)Public URL for Grouper (returned to DLL). Required in multi-host deployments where Grouper is not on the same hostname as Server.
KODEMED_PUBLIC_WEBSOCKET_URL(auto-detected)Public WebSocket URL wss:// (returned to DLL)
KODEMED_PUBLIC_UI_URL(auto-detected)Public URL for CodingUI (returned to DLL)
KODEMED_OAUTH2_REALMkodemedOAuth2 realm name (returned to DLL)
KODEMED_OAUTH2_CLIENT_IDkodemed-dllOAuth2 client ID for DLL authentication
KODEMED_PERSISTENCE_ENABLEDtrueEnable persistent storage of coding sessions
KODEMED_AUDIT_ENABLEDtrueEnable audit trail (requires persistence)
KODEMED_RETENTION_DAYS90Retention for completed sessions (days, 0=forever)
KODEMED_STORE_ORIGINALtrueStore original case data alongside changes
KODEMED_SESSION_IDLE_TIMEOUT_MINUTES60A coding session expires after this many minutes without coder activity (autosave, edits, the UI activity ping); an idle open tab still expires. A licence session cap stays absolute. Replaces KODEMED_PERSISTENCE_SESSION_EXPIRY_MINUTES (still honoured when the new key is unset). KODEMED_PERSISTENCE_OFFLINE_TIMEOUT_MINUTES is no longer used.
DLL Webhook — published by Server via GET /api/v1/config and fired by the DLL (KodeMed.CodingClient/WebhookService.cs). Applies only when the customer uses the Windows tray client. For HIS REST integrations, see the Server Webhook card below.
KODEMED_HOOK_ENABLEDfalseEnable the DLL post-coding webhook
KODEMED_HOOK_URL(empty)Webhook target URL the DLL will POST to
KODEMED_HOOK_AUTH_TYPEnoneDLL webhook auth: none, bearer, header
KODEMED_HOOK_TIMEOUT_SECONDS30DLL webhook HTTP timeout (seconds)
KODEMED_HOOK_RETRY_COUNT3DLL webhook retry count on failure
KODEMED_HOOK_INCLUDE_RESULT_DATAfalseInclude result data in DLL webhook payload
KODEMED_HOOK_INCLUDE_ORIGINAL_DATAfalseInclude original data in DLL webhook payload
KODEMED_HOOK_INCLUDE_GROUPER_RESULTSfalseInclude DRG grouper results in DLL webhook payload
KODEMED_HOOK_EVENTSappliedComma-separated event types to fire the DLL webhook on
KODEMED_LOG_INGEST_TOKEN(generated)Shared token every component presents when shipping its log events to the Server, so admin Settings can show all of them. Empty disables the feature in both directions: no appender starts and the ingest endpoint refuses everything
KODEMED_LOG_SHIP_URLhttp://kodemed-server:8080/api/v1/internal/logsWhere the components send them. Change only if the Server is not reachable at its compose service name
KODEMED_LOG_STORE_ENABLEDtrueKeep the shipped events on the Server
KODEMED_LOG_STORE_DIR/app/log-storeWhere they are kept (a named volume, so they survive container recreation)
KODEMED_LOG_STORE_RETENTION_DAYS30How long shipped logs are kept
KODEMED_LOG_STORE_MAX_BYTES2147483648Total size cap (2 GB). Bounded by age and size, whichever bites first — the cap is what stops one chatty component filling the disk the database is also on
KODEMED_LOG_FORWARD_URL(empty)Optional Logstash/ELK HTTP input to relay to. Additive: the local store is kept either way, so an ELK that is down cannot take the only log viewer with it
KODEMED_LOG_FORWARD_AUTH_HEADER(empty)Header name for the ELK input, e.g. Authorization
KODEMED_LOG_FORWARD_AUTH_VALUE(empty)Header value for the ELK input
KODEMED_RATE_LIMIT_ENABLEDtrueEnable rate limiting (DoS protection)
KODEMED_RATE_LIMIT_CAPACITY100Default token bucket capacity per IP (general endpoints)
KODEMED_RATE_LIMIT_REFILL100Tokens refilled per period (general endpoints)
KODEMED_RATE_LIMIT_PERIOD1Refill period in minutes (default 100 req/min)
KODEMED_RATE_LIMIT_AUTH_CAPACITY20Bucket capacity for endpoints reachable without credentials (OIDC config, session create, feedback, WebSocket handshake). Sized against brute force — raise with care
KODEMED_RATE_LIMIT_AUTH_REFILL10Tokens refilled per period for those endpoints
KODEMED_RATE_LIMIT_AUTH_PERIOD1Refill period in minutes for those endpoints (10 req/min)
KODEMED_RATE_LIMIT_ADMIN_CAPACITY300Bucket capacity for the authenticated admin API (/api/v1/admin/*). Separate from the auth bucket: one Settings page-load fires eight requests, and these routes are already behind hasRole(ADMIN)
KODEMED_RATE_LIMIT_ADMIN_REFILL300Tokens refilled per period for the admin API
KODEMED_RATE_LIMIT_ADMIN_PERIOD1Refill period in minutes for the admin API
KODEMED_UNDO_MAX_PER_CASE50Max undo entries kept per coding case
KODEMED_UNDO_RETENTION_DAYS7Undo entry retention (days)
KODEMED_GROUPER_PORT8082Grouper port (used to derive internal URL published to DLL)
KODEMED_UI_PORT3000UI port (used to derive internal URL)
CONTEXT_PATH(empty)Servlet context path (e.g. /api when behind a path-prefixing reverse proxy)
SERVER_CONNECTION_TIMEOUT20000Tomcat connection timeout (ms)
SERVER_MAX_CONNECTIONS8192Tomcat max simultaneous connections
ASYNC_REQUEST_TIMEOUT60000Async request timeout (ms)
H2_CONSOLE_ENABLEDfalseEnable embedded H2 console (development only — never enable in production)
INSTANCE_DISCONNECT_TIMEOUT60Minutes before disconnected DLL instance is terminated
HEARTBEAT_INTERVAL30WebSocket heartbeat interval (seconds)
RECONNECT_INTERVAL60DLL WebSocket reconnect interval (seconds)
KODEMED_DATASERVER_PORT8081DataServer port (used to derive internal URL)
KODEMED_UI_URL(empty)CodingUI URL for redirects (e.g. https://coding.hospital.ch)
SWAGGER_SERVER_URL(empty)Swagger UI base URL (useful behind reverse proxy)
SWAGGER_OAUTH2_CLIENT_IDkodemed-serverOAuth2 client ID for Swagger UI
TABLE_PREFIXkm_app_Database table prefix
LOGGING_LEVEL_ROOTINFORoot log level (WARN in prod profile)
MANAGEMENT_ENDPOINTShealth,info,metrics,prometheusExposed Spring Actuator endpoints

Server — Feedback / GitBucket integration

The Server forwards user feedback to GitBucket (issues) and SMTP (email). All keys can be left empty in environments where neither is desired. Disable the entire subsystem with KODEMED_FEEDBACK_ENABLED=false.

Environment VariableDefaultDescription
KODEMED_FEEDBACK_ENABLEDtrueMaster toggle for the feedback subsystem
KODEMED_FEEDBACK_LABELtester-feedbackGitBucket label applied to created issues
KODEMED_FEEDBACK_GITBUCKET_URLhttps://www.mieresit.com/gitbucketGitBucket base URL
KODEMED_FEEDBACK_REPOmieres-it/kode-medGitBucket repo (org/repo)
KODEMED_FEEDBACK_MILESTONE(empty)Milestone title to attach to created issues
KODEMED_FEEDBACK_USER(empty)GitBucket user (basic auth)
KODEMED_FEEDBACK_PASSWORD(empty)GitBucket password / API token
KODEMED_FEEDBACK_SMTP_HOST(empty)SMTP server (empty disables email path)
KODEMED_FEEDBACK_SMTP_PORT587SMTP port
KODEMED_FEEDBACK_SMTP_USER(empty)SMTP username
KODEMED_FEEDBACK_SMTP_PASSWORD(empty)SMTP password
KODEMED_FEEDBACK_SMTP_AUTHtrueSMTP authentication enabled
KODEMED_FEEDBACK_SMTP_STARTTLStrueSMTP STARTTLS enabled
KODEMED_FEEDBACK_SMTP_TOsupport@kodemed.chDestination address for feedback emails
KODEMED_FEEDBACK_SMTP_FROM(empty)Sender address (defaults to SMTP user)
KODEMED_FEEDBACK_SMTP_SUBJECT_PREFIX[KodeMed Feedback]Subject prefix for feedback emails
KODEMED_FEEDBACK_QUEUE_DIR./feedback-queueOn-disk queue for retry when GitBucket/SMTP are unreachable

Server Webhook (HIS REST integration) Per-IntegrationProfile · available

For HIS REST integrations the Server fires the webhook itself. Configuration is per-IntegrationProfile in the database (set via the admin API). There are no env vars for this flow — the values below are profile fields, not .env entries. Field names are camelCase (Spring Boot Jackson default), matching the IntegrationProfileDtos.WebhookRequest DTO.

POST /api/v1/admin/integration-profiles
Authorization: Bearer <admin JWT>
Content-Type: application/json
 
{
  "slug": "<your-his-slug>-test",
  "webhook": {
    "url": "https://your-his-host/your/webhook",
    "signing": "HMAC_SHA256",
    "secret": "<plaintext in; encrypted at rest; never returned>",
    "retryMax": 4,
    "retryDelaysSeconds": [30, 120, 600],
    "eventTypes": ["case.coded", "case.discarded"]
  }
}

For the full payload shape, retry semantics, DLQ replay path and HMAC signature verification snippets, see Integration → Webhook Contract.

KodeMed DataServer Port 8081

Data import/export service — ICD-10, CHOP, thesaurus, hot-folder import.

Environment VariableDefaultDescription
DATASERVER_PORT / SERVER_PORT8081HTTP port (DATASERVER_PORT takes priority)
SPRING_DATASOURCE_URLjdbc:h2:mem:kodemedJDBC connection URL
SPRING_DATASOURCE_USERNAMEsaDatabase username
SPRING_DATASOURCE_PASSWORD(empty)Database password
Authentication is mandatory. The legacy KODEMED_AUTH_ENABLED flag has been removed.
KODEMED_ADMIN_API_KEY(empty)API key for admin endpoints (import/reimport scripts). Generate: openssl rand -hex 32
KODEMED_LICENSE_FILE(auto-search)Path to kodemed.license file
KODEMED_IMPORT_ENABLEDtrueEnable hot-folder import system
KODEMED_IMPORT_AUTO_SCANfalseAuto-scan inbox on timer (disabled by default for safety)
KODEMED_IMPORT_BASEDIR./importBase directory for import files
KODEMED_IMPORT_INBOX_DIR./import/inboxInbox — place files here for processing
KODEMED_IMPORT_SUCCESS_DIR./import/successSuccess — processed files moved here
KODEMED_IMPORT_ERROR_DIR./import/errorError — failed files moved here
KODEMED_IMPORT_ARCHIVE_KEEP3Copies kept per file in success/warnings/error (newest first, each with its log; older ones are deleted after each import and at start-up). 0 keeps every copy
KODEMED_IMPORT_SCAN_INTERVAL_MS30000Scan interval in ms (only when auto-scan=true)
KODEMED_IMPORT_BATCH_SIZE1000Batch size for data imports
KODEMED_THESAURUS_BATCH_SIZE2000Batch size for thesaurus index inserts
KODEMED_THESAURUS_AUTO_BUILDtrueAuto-build thesaurus on startup
KODEMED_THESAURUS_SIMILARITY_THRESHOLD0.85Cosine-similarity floor for fuzzy thesaurus matches (0.0–1.0)
KODEMED_IMPORT_WAIT_TIMEOUT300Max seconds to wait for import to finish before timing out
KODEMED_IMPORT_NO_STATUS_THRESHOLD10Seconds without status update before an import is considered stalled
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE100MBMaximum multipart upload size (single file)
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE100MBMaximum multipart upload size (whole request)
TABLE_PREFIXkm_data_Database table prefix (different from Server!)
CORS_ALLOWED_ORIGINS(empty)CORS allowed origins
SWAGGER_SERVER_URL(empty)Swagger UI base URL
SWAGGER_OAUTH2_CLIENT_IDkodemed-serverOAuth2 client ID for Swagger UI
LOGGING_LEVEL_ROOTINFORoot log level
MANAGEMENT_ENDPOINTShealth,info,metrics,prometheus,cachesActuator endpoints (includes cache management)

KodeMed GrouperServer Port 8082

DRG grouper — stateless service for SwissDRG, TARPSY, ST Reha grouping. No database required.

Environment VariableDefaultDescription
SERVER_PORT8082HTTP port
Authentication is mandatory. The legacy GROUPER_AUTH_ENABLED flag has been removed.
GROUPER_SPECS_PATH./specsPath to grouper specification files (.sgs)
GROUPER_CATALOGUE_PATH./cataloguesPath to catalogue files (.csv) for cost weights
GROUPER_DEFAULT_SWISSDRG15.0Default SwissDRG version
GROUPER_DEFAULT_TARPSY6.3Default TARPSY version
GROUPER_DEFAULT_STREHA3.4Default ST Reha version
GROUPER_DATASERVER_URLhttp://kodemed-dataserver:8081Required. URL to DataServer for high-cost-medication (Zusatzentgelt) lookups. Without it, supplements silently return empty.
SERVER_CONNECTION_TIMEOUT20000Tomcat connection timeout (ms)
SERVER_MAX_CONNECTIONS8192Tomcat max simultaneous connections
ASYNC_REQUEST_TIMEOUT60000Async request timeout (ms)
KODEMED_LICENSE_FILE(auto-search)Path to kodemed.license file
CORS_ALLOWED_ORIGINS(empty)CORS allowed origins
SWAGGER_SERVER_URL(empty)Swagger UI base URL
SWAGGER_OAUTH2_CLIENT_IDkodemed-serverOAuth2 client ID for Swagger UI
LOGGING_LEVEL_ROOTINFORoot log level
LOGGING_LEVEL_COM_MIERESIT_GROUPERINFOGrouper application log level
LOGGING_LEVEL_SWISSDRGINFOSwissDRG library log level
MANAGEMENT_ENDPOINTShealth,info,metrics,prometheusActuator endpoints

KodeMed CodingUI Port 3000

React frontend served via nginx. Configured at runtime via runtime-config.js.

Config KeySourceDescription
window.__KODEMED_CONFIG__ (runtime-config.js — injected at deploy time)
apiUrlruntime-config.jsMain server API URL (e.g. https://server.hospital.ch/api/v1)
dataServerUrlruntime-config.jsDataServer URL (e.g. https://data.hospital.ch)
grouperServerUrlruntime-config.jsGrouperServer URL (reserved, UI proxies via Server)
wsUrlruntime-config.jsWebSocket URL (e.g. wss://server.hospital.ch/ws/dll)
oauth2Urlruntime-config.jsOAuth2/Keycloak base URL (e.g. https://sso.hospital.ch)
oauth2Realmruntime-config.jsOAuth2 realm name (e.g. kodemed)
oauth2ClientIdruntime-config.jsOAuth2 client ID (e.g. kodemed-ui)
URL Query Parameters (injected by DLL when opening embedded browser)
apiUrlURL paramOverride API URL
wsUrlURL paramOverride WebSocket URL
dataServerUrlURL paramOverride DataServer URL
oauth2UrlURL paramOverride OAuth2 base URL
oauth2RealmURL paramOverride OAuth2 realm
oauth2ClientIdURL paramOverride OAuth2 client ID
langURL paramUI language (de, fr, it)
classificationVersionURL paramClassification year (e.g. 2026)
Config Resolution Priority: URL params > runtime-config.js > VITE_* build vars > /api/v1/config > defaults

Caddy / reverse proxy Compose overlay

These variables shape Caddyfile.* and the per-service routing in docker-compose.caddy.yml. They have no effect on KodeMed services themselves — only on the TLS / hostname layer.

Environment VariableDefaultDescription
DOMAINkodemed.chApex domain for the deployment
SSO_DOMAINsso.kodemed.chFQDN of the Keycloak instance Caddy fronts
UI_SUBDOMAINdemo-coding-uiSubdomain for CodingUI on apex
SERVER_SUBDOMAINdemo-serverSubdomain for the Server REST API + WebSocket
DATASERVER_SUBDOMAINdemo-dataSubdomain for DataServer
GROUPER_SUBDOMAINdemo-grouperSubdomain for GrouperServer
PORTAL_SUBDOMAINdemo-portalSubdomain for the demo (admin) portal
PROD_PORTAL_SUBDOMAINportalSubdomain for the customer-facing portal
SSO_SUBDOMAINdemo-ssoSubdomain for the Keycloak instance fronted by Caddy. Override per environment.
DOCS_SUBDOMAINdemo-docsSubdomain for documentation site
SUPPORT_SUBDOMAINsupportSubdomain for support portal redirect
WWW_SUBDOMAINwwwSubdomain for marketing website
COOKIE_DOMAIN(empty)Cookie domain shared across subdomains (e.g. .kodemed.ch)
RUNTIME_CONFIG_FILEruntime-config.demo.jsWhich runtime-config.*.js the UI mounts. Use runtime-config.ch.js on CH.
IMAGE_TAGlatestTag pulled from harbor.mieresit.com/kodemed/* images
DOWNLOADS_VERSION(release version)Version baked into the downloads image; surfaced on the portal Overview
SUPPORT_URLhttps://support.kodemed.chPublic support URL (link target only)
WWW_URLhttps://www.kodemed.chPublic marketing URL (link target only)
DEPLOY_HOST(per env)SSH target used by scripts/ci/deploy-docker-compose.sh to push the stack

Portal — OAuth2-Proxy Demo + prod

The portal pages (downloads demo + prod) are protected by oauth2-proxy sidecars in docker-compose.demo.yml. Two distinct OIDC clients allow demo-portal and prod-portal to live behind the same Keycloak realm without sharing cookies.

Environment VariableDefaultDescription
OAUTH2_DOWNLOADS_CLIENT_IDkodemed-downloadsOIDC client id for the demo portal proxy
OAUTH2_DOWNLOADS_CLIENT_SECRET(secret)OIDC client secret for the demo portal proxy
OAUTH2_DOWNLOADS_COOKIE_SECRET(32-byte secret)Cookie signing secret. Generate: openssl rand -base64 32 | tr -d '=' | head -c 32
OAUTH2_DEMO_REDIRECT_URLhttps://demo-portal.kodemed.ch/oauth2/callbackRedirect URL for demo portal — must match the realm client config
OAUTH2_PROD_CLIENT_IDkodemed-downloadsOIDC client id for the prod portal proxy
OAUTH2_PROD_CLIENT_SECRET(secret)OIDC client secret for the prod portal proxy
OAUTH2_PROD_COOKIE_SECRET(32-byte secret)Cookie signing secret for the prod portal proxy
OAUTH2_PROD_REDIRECT_URLhttps://portal.kodemed.ch/oauth2/callbackRedirect URL for prod portal
OIDC_PROD_ISSUER_URIhttps://sso.kodemed.ch/realms/kodemedOIDC issuer used by the prod portal oauth2-proxy
OIDC_PROD_TOKEN_URLhttps://sso.kodemed.ch/realms/kodemed/protocol/openid-connect/tokenToken endpoint for the prod portal proxy
OIDC_PROD_JWKS_URLhttps://sso.kodemed.ch/realms/kodemed/protocol/openid-connect/certsJWKS endpoint for the prod portal proxy
DEMO_PORTAL_URLhttps://demo-portal.kodemed.chPublic URL of the demo portal (used for cross-links)
PROD_PORTAL_URLhttps://portal.kodemed.chPublic URL of the prod portal

Keycloak (bundled mode) SSO

Used by the wizard’s localhost preset and by the optional docker-compose.keycloak.yml overlay. Customers using their own Keycloak (HIS REST integrations) do not need any of these — only the issuer URL matters then.

Environment VariableDefaultDescription
KC_HOSTNAMElocalhostPublic hostname Keycloak advertises in tokens and redirect URIs
KC_HOSTNAME_PORT9080Public port (9080 plain, 9443 TLS, omit when behind Caddy on 443)
KC_HOSTNAME_STRICTfalseReject mismatching Host: headers. Keep false behind a reverse proxy.
KC_HTTP_ENABLEDtrueAllow plain HTTP on the internal port. TLS is terminated by Caddy.
KC_PROXY_HEADERSxforwardedTrust X-Forwarded-* headers from Caddy for scheme/host detection
KC_HEALTH_ENABLEDtrueExpose /health for compose healthchecks
KC_DBpostgresDatabase backend (only postgres supported in our compose)
KC_DB_URLjdbc:postgresql://kodemed-keycloak-db:5432/keycloakJDBC URL for the bundled Keycloak DB
KC_DB_NAMEkeycloakKeycloak database name
KC_DB_USERNAMEkeycloakKeycloak database user
KC_DB_PASSWORD(secret)Keycloak database password (auto-generated by the wizard)
KEYCLOAK_ADMINadminKeycloak master realm admin user
KEYCLOAK_ADMIN_PASSWORD(secret)Keycloak master realm admin password
KC_IMPORT_REALM/opt/keycloak/data/import/kodemed-realm.jsonRealm JSON imported on first start
KC_LOG_LEVELinfoKeycloak log level
KC_METRICS_ENABLEDtrueExpose Prometheus metrics on /metrics
KC_IMAGEquay.io/keycloak/keycloak:26.0Override the Keycloak image (pin per release)

KodeMed DLL/Client C# Desktop

Windows desktop DLL integration. Config file: kodemed-client-config.json (next to DLL or %APPDATA%\KodeMed\).

JSON KeyDefaultDescription
serverUrl(required)KodeMed Server URL (e.g. https://kodemed.hospital.ch)
codingUIUrl(from server)CodingUI URL (fetched from /api/v1/config if not set)
grouperServerUrl(from server)GrouperServer URL for DRG grouping (fetched from /api/v1/config if not set)
trustSelfSignedCertificatesfalseTrust self-signed TLS certs (Caddy IP mode only, NOT for production)
oauth2Url(from server)OAuth2/Keycloak base URL
oauth2RealmkodemedOAuth2 realm name
oauth2ClientIdkodemed-dllOAuth2 client ID for desktop auth
language(system)UI language override (de, fr, it)
webSocketAutoReconnecttrueAuto-reconnect WebSocket on disconnect
webSocketReconnectIntervalSeconds60Reconnect interval (seconds, min 5)
webSocketHeartbeatIntervalSeconds30Heartbeat interval (seconds)
hook sub-object (post-coding webhook — auth credentials stored locally, NOT on server)
hook.enabled(from server)Override: enable/disable webhook
hook.url(from server)Override: webhook target URL
hook.authType(from server)Auth type: none, bearer, header
hook.authToken(local only)Bearer token for webhook auth (SENSITIVE)
hook.authHeaderName(local only)Custom header name (SENSITIVE)
hook.authHeaderValue(local only)Custom header value (SENSITIVE)

Important Notes

  • Authentication: mandatory and not configurable. The legacy KODEMED_AUTH_ENABLED and GROUPER_AUTH_ENABLED flags were removed; every endpoint that is not explicitly listed as public in SecurityConfig requires a valid JWT against the configured OIDC issuer.
  • TABLE_PREFIX: Server uses km_app_, DataServer uses km_data_. Both share the same PostgreSQL database.
  • LOGGING_LEVEL_ROOT is the correct Spring Boot env var (not LOG_LEVEL).
  • KODEMED_LICENSE_FILE: search order (first match wins): $KODEMED_LICENSE_FILE if set, then ./kodemed.license (resolved relative to each service’s WorkingDirectory — on native installs that is the per-service directory, not the install root), then /etc/kodemed/kodemed.license, then %APPDATA%/KodeMed/kodemed.license (Windows DLL only). See the Linux Server section for the per-service copy requirement on native installs.
  • KODEMED_ENCRYPTION_KEY: Only used by Server. DataServer and GrouperServer do not encrypt data at rest.
  • Webhook auth credentials (token, header) are stored in the DLL config only, never on the server. The server only stores non-sensitive webhook settings.

Health Check & Smoke Test Endpoints

Use these public endpoints (no authentication required) to verify your deployment:

ServiceEndpointExpected Response
ServerGET /actuator/health{"status":"UP"}
ServerGET /api/v1/configJSON with serverUrl, dataServerUrl, version info
DataServerGET /actuator/health{"status":"UP"}
DataServerGET /api/v1/healthJSON with "UP" status, service name, version
GrouperServerGET /actuator/health{"status":"UP"}
GrouperServerGET /api/v1/grouper/versionsJSON with loaded grouper versions (SwissDRG, TARPSY, ST Reha)
All servicesGET /api/v1/license/statusLicense status (VALID / EXPIRED / INVALID)

HTTP 402 on any endpoint means the license is invalid or missing. Check KODEMED_LICENSE_FILE path and verify with: java -jar kodemed-license-cli.jar verify --public-key kodemed-public.pem --license kodemed.license

License Generator Admin

Generate License File

Fill in the customer details below. The tool generates a signed kodemed.license file ready to send to the customer.

CLI Command

Run this on a machine with the private key and the kodemed-license-cli.jar:


        
        Copied!
      

License CLI Tool

Download the CLI JAR to generate and verify license files.

  • kodemed-license-cli.jar Download JAR
  • Quick Reference java -jar kodemed-license-cli.jar generate --private-key key.pem --type DEMO --org "Hospital" --days 90 --output kodemed.license