Helios FHIR Server (HFS)
A high-performance FHIR server built in Rust.
An open test server is available at https://hfs.heliossoftware.com/ for experimentation and evaluation.
Features
- Full FHIR RESTful API support
- Multiple FHIR version support
- Pluggable storage backends (SQLite, PostgreSQL, MongoDB)
- Content negotiation (JSON)
- Conditional operations with ETag support
- Multi-tenant support via X-Tenant-ID header
- CORS support
Installation
From Source
# Clone the repository
# Build with default features (R4 + SQLite)
# Build with all FHIR versions
Usage
Running the Server
# Run with default settings (R4, SQLite, port 8080)
# Specify a different port
# Use an in-memory database
# Enable debug logging
Command Line Options
Usage: hfs [OPTIONS]
Options:
--port <PORT> Server port [env: HFS_SERVER_PORT=] [default: 8080]
--host <HOST> Host to bind [env: HFS_SERVER_HOST=] [default: 127.0.0.1]
--log-level <LOG_LEVEL> Log level (error, warn, info, debug, trace)
[env: HFS_LOG_LEVEL=] [default: info]
--database-url <URL> Database connection URL [env: DATABASE_URL=]
--data-dir <PATH> Path to FHIR data directory containing search parameter
definitions [env: HFS_DATA_DIR=] [default: ./data]
--max-body-size <BYTES> Maximum request body size [env: HFS_MAX_BODY_SIZE=]
[default: 10485760]
--request-timeout <SECS> Request timeout in seconds [env: HFS_REQUEST_TIMEOUT=]
[default: 30]
--enable-cors Enable CORS [env: HFS_ENABLE_CORS=] [default: true]
--cors-origins <ORIGINS> Allowed CORS origins [env: HFS_CORS_ORIGINS=] [default: *]
-h, --help Print help
-V, --version Print version
hfs --version prints the crate version and, when the binary was built from a
git checkout, the commit it was cut from — hfs 0.2.1 (git 1a2b3c4d5). The
same identity is advertised to FHIR clients as CapabilityStatement.software
on GET /metadata (the commit as an extension on software) and reported by
GET /health. When building from a source archive with no .git, set
HFS_GIT_SHA=<sha> in the build environment to record the commit.
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
HFS_SERVER_PORT |
8080 | Server port |
HFS_SERVER_HOST |
127.0.0.1 | Host to bind |
HFS_BASE_URL |
http://localhost:8080 | Public HTTP(S) base for response links and Location headers |
HFS_LOG_LEVEL |
info | Log level (error, warn, info, debug, trace) |
HFS_UI_ENABLED |
true | Serve the web UI at /ui; false for headless deployments (/ui then returns 404 + OperationOutcome) |
DATABASE_URL |
fhir.db | Database connection string |
HFS_DATA_DIR |
./data | Path to FHIR data directory (search parameters) |
HFS_SEARCH_PARAM_CACHE_TTL |
3600 | Seconds between refreshes of the in-memory SearchParameter registry from storage; a param POSTed to one cluster node becomes visible to others within this interval. 0 disables the refresh. |
HFS_MAX_BODY_SIZE |
10485760 | Max request body size (bytes; applies to the decompressed body for compressed requests) |
HFS_REQUEST_TIMEOUT |
30 | Request timeout (seconds) |
HFS_DASHBOARD_RECONCILE_SECS |
30 | Seconds between Home dashboard reconcile passes against storage (CLI --dashboard-reconcile-interval-secs). Whole seconds, must be > 0; 0 or a non-numeric value is a startup error. A tenant's full recount is additionally spaced to at most every max(interval, 10 × its last recount duration); also how soon a failed background seed is retried. |
HFS_DASHBOARD_REFRESH_SECS |
5 | Seconds between refreshes of a Home dashboard whose figures are moving (approximate, or an import running) (CLI --dashboard-refresh-secs). Re-reads in-memory counters only, never storage. Whole seconds, must be > 0 and <= HFS_DASHBOARD_IDLE_REFRESH_SECS; otherwise a startup error. |
HFS_DASHBOARD_IDLE_REFRESH_SECS |
10 | Seconds between watch ticks of a Home dashboard whose figures are settled (exact, no import running), so an open tab notices an import starting (CLI --dashboard-idle-refresh-secs). Whole seconds, must be > 0 and >= HFS_DASHBOARD_REFRESH_SECS; otherwise a startup error. |
HFS_ENABLE_CORS |
true | Enable CORS |
HFS_CORS_ORIGINS |
* | Allowed CORS origins |
HFS_CORS_METHODS |
GET,POST,PUT,DELETE,OPTIONS | Allowed HTTP methods |
HFS_CORS_HEADERS |
Content-Type,Authorization,X-Requested-With | Allowed headers |
HFS_DEFAULT_TENANT |
default | Default tenant ID |
HFS_TERMINOLOGY_SERVER |
(none) | Terminology server URL for :in/:not-in modifiers and FHIRPath memberOf()/subsumes() |
HFS_COMPOSITE_SYNC_MODE |
asynchronous |
Composite-store write sync mode for ES-backed backends (sqlite-elasticsearch, postgres-elasticsearch, mongodb-elasticsearch, s3-elasticsearch). One of asynchronous, synchronous, hybrid. With asynchronous (default) the write returns as soon as the primary commits and the search backend is updated on a background worker — lowest latency, but a follow-up search can race the indexing. Use synchronous when callers need read-your-write semantics (e.g. integration tests, bulk-load flows that immediately search). Ignored when the storage backend has no search secondary. |
HFS_ELASTICSEARCH_REFRESH_INTERVAL |
1s |
Elasticsearch index refresh_interval for ES-backed backends. Controls how quickly indexed documents become searchable when no per-write refresh is requested. -1 disables periodic refresh entirely. Applied when an index is created; indices that already exist keep their current setting. |
HFS_ELASTICSEARCH_WRITE_REFRESH |
false |
The refresh parameter applied to Elasticsearch index/delete operations. One of false (no per-write refresh), wait_for (block each write until the affected shards refresh), or true (force a refresh per write; expensive, low-volume deployments only). |
HFS_ELASTICSEARCH_NESTED_OBJECTS_LIMIT |
50000 |
Elasticsearch index.mapping.nested_objects.limit: the maximum nested objects a single document may contain, summed across every nested search-parameter field. Elasticsearch's own default of 10000 rejects larger documents, so a resource with very many indexed values (for example a Synthea Provenance with 13,554 target references) is stored but never searchable. Applied to new indices through the index template, and raised at startup on existing indices that are below it; the setting is dynamic, so no reindex is needed for resources that already indexed. |
HFS_ELASTICSEARCH_REQUEST_TIMEOUT_MS |
30000 |
Timeout of every Elasticsearch HTTP request, in milliseconds, including _bulk. A _bulk request that times out is split in half and resent, down to a single document, so raise this only when one large document still times out on its own. |
HFS_ELASTICSEARCH_BULK_MAX_BYTES |
10485760 (10 MiB) |
Byte cap on one _bulk request body, applied together with the 500-operation cap, for bulk creates and every rebuild. Without it, 500 Synthea Provenance resources (~108 KB each) made a ~54 MB request that exceeded the request timeout. A single document larger than the cap is sent alone. |
HFS_ELASTICSEARCH_BULK_CONCURRENCY |
1 |
How many _bulk requests of one page are in flight at once. 1 sends them one at a time, as before #1125. Raising it shortens a rebuild when the cluster is not the bottleneck; splitting after a 413 or a timeout, and 429 back-off, stay sequential within the request that caused them. |
HFS_REINDEX_BATCH_BYTES |
0 |
Byte cap on one page of the automatic rebuild, on top of HFS_REINDEX_BATCH_SIZE. 0 means count only. With a cap set, a page of ~108 KB Provenance resources ends at the first one that crosses it instead of holding ~108 MB in memory. Honoured by the SQLite source; other sources page by count only. |
HFS_ELASTICSEARCH_REINDEX_REFRESH |
(unset) | The refresh parameter for the _bulk writes of $reindex and of the deferred post-import rebuild: false, wait_for, or true. Unset follows HFS_ELASTICSEARCH_WRITE_REFRESH. Set false to let a rebuild skip the per-request refresh wait while ordinary writes keep wait_for. |
HFS_REINDEX_BATCH_SIZE |
1000 |
Page size of the automatic deferred rebuild that follows a bulk import, on every backend. POST $reindex keeps its own batchSize parameter (default 100). |
Read-after-write search on an ES-backed composite needs both
HFS_COMPOSITE_SYNC_MODE=synchronous and
HFS_ELASTICSEARCH_WRITE_REFRESH=wait_for. See
Search visibility on Elasticsearch-backed composites
in the persistence crate for why either setting alone still leaves a window.
Elasticsearch _bulk failures are handled per shape. A request that times out,
or is answered 413, 408 or 504, is split in half and each half resent,
down to a single document; once a single document times out, the rest of the
page fails as transient instead of being split further. A 429, for the whole
request or for single items, is retried with bounded exponential back-off for
the rejected items only. A per-document 4xx is permanent and not retried. A
connection error, a whole-request 5xx, or a 429 that outlasts its retries
fails every document of that request as transient. Every failed document is
recorded by Type/id, with Elasticsearch's error.type and reason when it
gave one, in GET /$reindex-status/{job_id}. See
Bulk writes and rebuilds on Elasticsearch-backed composites.
On sqlite-elasticsearch, the recommended pair for a bulk import is
HFS_ELASTICSEARCH_WRITE_REFRESH=wait_for with
HFS_ELASTICSEARCH_REINDEX_REFRESH=false: ordinary writes stay
read-your-write, while the rebuild skips the per-request refresh wait.
Measured on a 228,580-resource cut, that took the deferred rebuild from 806 s
to 145 s (1,576 resources/s) with the same result.
A manifest ingested with indexing deferred records that it still owes a
rebuild, on its own row. If the server stops mid-rebuild, the next start
re-fires it and logs resuming search-index rebuilds left outstanding by an earlier run; the marker is cleared once the rebuild has run.
Set HFS_BASE_URL to the public address clients can reach. The value may
contain a path prefix. It must be an absolute http or https URL without
credentials, a query string, or a fragment. HFS rejects invalid values and
does not infer the public address from request forwarding headers.
FHIR Version Support
Build with specific FHIR versions using feature flags:
# R4 only (default)
# R5 only
# Multiple versions
Database Backends
SQLite (Default)
# Run with file-based database
# Run with in-memory database
PostgreSQL
MongoDB
API Endpoints
| Interaction | Method | URL |
|---|---|---|
| capabilities | GET | /metadata |
| read | GET | /[type]/[id] |
| vread | GET | /[type]/[id]/_history/[vid] |
| update | PUT | /[type]/[id] |
| patch | PATCH | /[type]/[id] |
| delete | DELETE | /[type]/[id] |
| create | POST | /[type] |
| search | GET/POST | /[type]?params or /[type]/_search |
| history (instance) | GET | /[type]/[id]/_history |
| history (type) | GET | /[type]/_history |
| history (system) | GET | /_history |
| batch/transaction | POST | / |
| health | GET | /health |
Examples
Create a Patient
Read a Patient
Search for Patients
Get CapabilityStatement
Batch and Transaction Support
HFS supports both batch and transaction bundles for processing multiple operations in a single request.
Note: Transaction bundles require ACID transaction support. The default SQLite backend fully supports transactions. If using other backends, check the capability matrix in the persistence crate documentation.
Transaction Bundle (Atomic)
All operations succeed or all fail. Entries are processed in FHIR-specified order: DELETE → POST → PUT → GET.
The urn:uuid:patient-1 reference is automatically resolved to the actual Patient ID after creation.
Batch Bundle (Independent)
Each operation is processed independently; failures don't affect other entries.
Current Limitations
The following FHIR bundle features are not yet implemented:
- Conditional reference resolution (
Patient?identifier=12345) - PATCH method in bundles
- Prefer header handling (
return=minimal, etc.)
Search Parameter Configuration
HFS loads FHIR SearchParameter definitions from JSON bundle files to enable comprehensive search functionality. By default, these files are expected in a data/ directory relative to the working directory or executable.
Data Directory Structure
data/
├── search-parameters-r4.json # FHIR R4 SearchParameters (HL7 spec)
├── search-parameters-r4b.json # FHIR R4B SearchParameters (HL7 spec)
├── search-parameters-r5.json # FHIR R5 SearchParameters (HL7 spec)
├── search-parameters-r6.json # FHIR R6 SearchParameters (auto-downloaded at build time)
├── sql-on-fhir-search-parameters.json # SQL-on-FHIR IG ViewDefinition search params (custom file)
└── *.json # Custom SearchParameter files (see below)
Search Parameter Loading
On startup, HFS loads SearchParameters in this order:
- Minimal fallback - Built-in
_id,_lastUpdated,_tag,_profile,_security(always available) - Spec file - Loads from the appropriate
search-parameters-*.jsonbased on configured FHIR version - Custom files - Loads any additional
.jsonfiles in the data directory (not matchingsearch-parameters-*.json) - Stored parameters - Loads any custom SearchParameters POSTed to the server
Custom SearchParameter Files
You can add custom SearchParameters by placing JSON files in the data directory. Each file can contain:
- A single SearchParameter resource
- An array of SearchParameter resources
- A FHIR Bundle containing SearchParameter resources
Example custom SearchParameter file (data/custom-search-params.json):
Or as a Bundle:
SQL-on-FHIR ViewDefinition Search Parameters
data/sql-on-fhir-search-parameters.json ships the 17 ViewDefinition SearchParameters from the SQL-on-FHIR IG as one of these custom Bundle files. Do not edit it by hand — regenerate it from the official IG package with:
The script strips generated narratives and rewrites the composite parameters' legacy component.definition canonicals to the core-namespace canonicals the parameters actually declare (an upstream IG inconsistency, reported as HL7/sql-on-fhir#403; the rewrite becomes a no-op once fixed upstream). Bump DEFAULT_PACKAGE_URL in the script when a new IG ballot/release ships.
Custom Data Directory
Specify a custom location for the data files:
# Via command line
# Via environment variable
HFS_DATA_DIR=/opt/hfs/data
If the spec file is missing, HFS logs a warning and continues with minimal fallback parameters. This ensures the server can start even without the full spec files, though search functionality will be limited.
R6 Automatic Download
When building with the R6 feature enabled, the search-parameters-r6.json file is automatically downloaded from the HL7 build server during compilation. The download is skipped if:
- The file already exists and is less than 24 hours old
- The
DOCS_RSenvironment variable is set (for docs.rs builds)
Multi-Tenant Support
Use the X-Tenant-ID header to isolate data between tenants: