Configuration file
This is refhir.toml.example, shown whole and generated from the file when the site is built. It lists
every setting, with a comment on each. Treat the values as examples, not as defaults: where a
default differs, the comment says so, and a setting left out of your file takes the server’s own
default.
Copy it to refhir.toml, or mount it at /etc/refhir/refhir.toml. The server looks for
./refhir.toml, then /etc/refhir/refhir.toml, or the path in REFHIR_CONFIG. Command-line flags
override the file; see Command line and
Configuration.
# Copy to refhir.toml (or mount at /etc/refhir/refhir.toml).# CLI flags override these values. Production template: deploy/refhir.production.toml
[fhir]# Release defaults installed at first startup (only r4 in this build). Pinned per database:# you cannot point an existing database at a different version (e.g. r4 → r5).version = "r4"
[server]bind = "0.0.0.0:8080"# Required for SMART when bind is not loopback (production TLS URL).# public_url = "https://refhir.example.com/fhir"# How many reverse proxies (load balancer, ingress, CDN) stand between clients and this server.# Per-address limits and the audit trail need the client's address, which behind a proxy is only# in X-Forwarded-For. 0 ignores that header and uses the connection's address, which is right# when clients connect directly and wrong behind a proxy, where every client then looks like the# proxy. With N, the entry N from the right is the client (each proxy appends the address it# saw, so anything to the left of that is whatever the client claimed).# trusted_proxy_hops = 1# The networks those proxies connect from. X-Forwarded-For is then believed only on a connection# from one of them; without this a client that reaches the server directly (a node port, a pod# address) can write the header itself and choose, or frame, an address.# trusted_proxy_peers = ["10.0.0.0/8", "fd00::/8"]
[database]kind = "sqlite" # sqlite | memory | postgressqlite_path = "/data/refhir.sqlite"# postgres_url = "postgresql://refhir:refhir@postgres:5432/refhir"# Or set REFHIR_POSTGRES_URL instead of postgres_url.# TLS: no sslmode means no TLS. In production use# ?sslmode=verify-full&sslrootcert=/etc/refhir-pg-tls/ca.crt# (also sslcert/sslkey for client certificates; see docs/operations.md, "Postgres and TLS").
[cluster]# Several processes on one Postgres database (see docs/clustering.md).# role = "primary" # primary | standby | replica; REFHIR_ROLE# primary_url = "http://primary:8080" # replicas send writes here; REFHIR_PRIMARY_URL# db_pool = 8 # most Postgres connections (SQLite: read connections, default 4); REFHIR_DB_POOL
[conformance]reference_policy = "strict" # strict | fhir# strict: a reference must point at a resource this server holds, of a type the element allows,# however deep it sits (`Encounter.participant.individual` as much as `Observation.subject`).# fhir: a reference may point at something this server does not hold; the type is still checked.# The server assigns the id of a resource it creates, and PUT to an id that does not exist is a# 405. Set true to let that PUT create the resource under the id the client names (FHIR# "update as create"), for data that has ids of its own.# update_as_create = false# Where packages are downloaded from: a registry or mirror URL, or s3://bucket/prefix# (AWS S3 or any S3-compatible store; keys in REFHIR_S3_ACCESS_KEY_ID / REFHIR_S3_SECRET_ACCESS_KEY,# endpoint in REFHIR_S3_ENDPOINT; see deploy/production.md).# Build your own bundle with scripts/package-bundle.py.# package_registry = "https://packages.fhir.org"# package_cache = "/data/package-cache"
# [fhir].version installs that release's defaults (r4 today). Optional IGs:# packages = ["hl7.fhir.us.core@6.1.0", "/ig/my-implementation-guide.tgz"]
[smart]enabled = false# patient = "example-patient"# encounter = "example-encounter"# fhir_user = "Practitioner/example" # demo login only# REFHIR_SMART_DCR_TOKEN / REFHIR_SMART_CONFIDENTIAL_SECRET in production## SMART needs a login. Use an OpenID Connect provider for real deployments:# [smart.oidc]# issuer = "https://idp.example.com/realms/clinic"# client_id = "refhir"# client_secret via REFHIR_OIDC_CLIENT_SECRET# scopes = ["openid", "profile"]# fhir_user_claim = "fhirUser"# Optional, all off by default (see deploy/production.md):# allowed_claims = { organization_id = ["org_abc123"] } # only these users may sign in# allowed_email_domains = ["example.com"] # ... or only these email domains# patient_from_fhir_user = true # a Patient-bound user skips the patient picker# [smart.oidc.fhir_user_lookup] # bind a person by search when the token has no fhirUser claim# resource_type = "Practitioner"# claim = "sub"# system = "https://idp.example.com/sub"# Or, for testing only (accepts any username, verifies nothing):# demo_login = true
[export]# Local NDJSON output for $export jobs.dir = "/data/export"
[terminology]# Optional HTTP pull into the internal terminology index (not per-request TX).urls = []
[features]trace_engine = falsesdc_archiver = false
# --- Rate limits, quotas, metering, request logs and the audit trail -----------------------# Metering, the request log and the audit trail are ON by default. Nothing is limited until you# size a limit for your traffic: a [rate_limit] or [quota] section that is present is ON unless# it says `enabled = false`. See docs/governance.md.
[metering]enabled = true # per client, per UTC day, per class of request: GET /_usage
[request_log]enabled = true # one structured log line per request, with an X-Request-Idstore = true # also keep recent requests in the database: GET /_requestsretention_hours = 24refused_per_second = 20 # rows kept per second for throttled or unattributed failures; # past that they are counted, not stored (0 keeps every one)
[audit]enabled = true # who did what to which resource: GET /_audit (FHIR AuditEvent)reads = true # false records only changes, refusals and admin accessinclude_query = true # keep search parameters (they can name a patient by identifier)# retention_days = 2190 # unset keeps the audit trail foreverunattributed_per_second = 20 # detailed rows per second for refusals with no caller known; # past that a `sampled` summary row says how many (0 keeps all)# The hash chain over the audit trail is keyed by the environment variable REFHIR_AUDIT_KEY# (at least 16 bytes, e.g. `openssl rand -hex 32`, the same on every server, kept outside the# database). Without it the chain only catches mistakes. GET /_audit/verify checks it.
# A token bucket per caller (client + user; an address for callers with no token).# Switched off here; set enabled = true to turn it on.[rate_limit]enabled = falserequests_per_second = 50 # sustained rate, in cost units a secondburst = 100 # the most a caller can spend at once (must be >= the dearest cost)auth_failures_per_minute = 20 # per address, before more unknown tokens are refused unlooked-upmax_sync_connections = 10 # /sync connections one caller may holdmax_live_queries = 100 # live queries one /sync connection may hold# How many servers share these limits, each taking an equal part. Unset, servers count one# another through the database (a heartbeat every ten seconds) and follow a fleet as it grows.# Set 1 where a balancer sends each caller to a single server.# servers = 3# For callers with no token. Unset: a tenth of the above with --smart, the same without.# anonymous_requests_per_second = 5# anonymous_burst = 20
# What a request costs. An export is one request that is a great deal of work.[rate_limit.cost]read = 1search = 2write = 2bundle = 5operation = 5export = 20sync = 5
# A client's own rate, by client_id. 0 exempts the client.# [rate_limit.clients.batch-loader]# requests_per_second = 200# burst = 400# [rate_limit.clients.internal-monitor]# requests_per_second = 0
# Totals a client may not pass in a UTC day or month. Switched off here.[quota]enabled = false# requests_per_day = 100000# writes_per_day = 10000# bytes_per_day = 1073741824# requests_per_month = 2000000# writes_per_month = 200000# bytes_per_month = 21474836480# refresh_seconds = 30 # how often totals are reread, to see other servers' counts
# A client's own totals override field by field; 0 lifts a limit the default sets.# [quota.clients.acme]# requests_per_day = 1000000# bytes_per_day = 0