Skip to content

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.

refhir.toml.example
# 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 | postgres
sqlite_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 = false
sdc_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-Id
store = true # also keep recent requests in the database: GET /_requests
retention_hours = 24
refused_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 access
include_query = true # keep search parameters (they can name a patient by identifier)
# retention_days = 2190 # unset keeps the audit trail forever
unattributed_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 = false
requests_per_second = 50 # sustained rate, in cost units a second
burst = 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-up
max_sync_connections = 10 # /sync connections one caller may hold
max_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 = 1
search = 2
write = 2
bundle = 5
operation = 5
export = 20
sync = 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