helios-hfs 0.2.2

Helios FHIR Server - A high-performance FHIR R4/R4B/R5/R6 server
helios-hfs-0.2.2 is not a library.

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
git clone https://github.com/HeliosSoftware/hfs.git
cd hfs

# Build with default features (R4 + SQLite)
cargo build --release -p helios-hfs

# Build with all FHIR versions
cargo build --release -p helios-hfs --features R4,R4B,R5,R6,sqlite

Usage

Running the Server

# Run with default settings (R4, SQLite, port 8080)
./target/release/hfs

# Specify a different port
./target/release/hfs --port 3000

# Use an in-memory database
./target/release/hfs --database-url :memory:

# Enable debug logging
./target/release/hfs --log-level debug

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)
cargo build -p helios-hfs --features R4,sqlite

# R5 only
cargo build -p helios-hfs --no-default-features --features R5,sqlite

# Multiple versions
cargo build -p helios-hfs --features R4,R4B,R5,R6,sqlite

Database Backends

SQLite (Default)

cargo build -p helios-hfs --features sqlite

# Run with file-based database
./target/release/hfs --database-url ./data/fhir.db

# Run with in-memory database
./target/release/hfs --database-url :memory:

PostgreSQL

cargo build -p helios-hfs --no-default-features --features R4,postgres

./target/release/hfs --database-url "postgresql://user:pass@localhost/fhir"

MongoDB

cargo build -p helios-hfs --no-default-features --features R4,mongodb

./target/release/hfs --database-url "mongodb://localhost:27017/fhir"

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

curl -X POST http://localhost:8080/Patient \
  -H "Content-Type: application/fhir+json" \
  -d '{
    "resourceType": "Patient",
    "name": [{"family": "Smith", "given": ["John"]}],
    "birthDate": "1970-01-01"
  }'

Read a Patient

curl http://localhost:8080/Patient/123

Search for Patients

curl "http://localhost:8080/Patient?family=Smith"

Get CapabilityStatement

curl http://localhost:8080/metadata

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.

curl -X POST http://localhost:8080/ \
  -H "Content-Type: application/fhir+json" \
  -d '{
    "resourceType": "Bundle",
    "type": "transaction",
    "entry": [
      {
        "fullUrl": "urn:uuid:patient-1",
        "resource": {
          "resourceType": "Patient",
          "name": [{"family": "Smith"}]
        },
        "request": {
          "method": "POST",
          "url": "Patient"
        }
      },
      {
        "resource": {
          "resourceType": "Observation",
          "subject": {"reference": "urn:uuid:patient-1"},
          "code": {"text": "Blood Pressure"}
        },
        "request": {
          "method": "POST",
          "url": "Observation"
        }
      }
    ]
  }'

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.

curl -X POST http://localhost:8080/ \
  -H "Content-Type: application/fhir+json" \
  -d '{
    "resourceType": "Bundle",
    "type": "batch",
    "entry": [
      {
        "request": {
          "method": "GET",
          "url": "Patient/123"
        }
      },
      {
        "request": {
          "method": "DELETE",
          "url": "Patient/456"
        }
      }
    ]
  }'

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:

  1. Minimal fallback - Built-in _id, _lastUpdated, _tag, _profile, _security (always available)
  2. Spec file - Loads from the appropriate search-parameters-*.json based on configured FHIR version
  3. Custom files - Loads any additional .json files in the data directory (not matching search-parameters-*.json)
  4. 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):

{
  "resourceType": "SearchParameter",
  "id": "patient-mrn",
  "url": "http://example.org/fhir/SearchParameter/patient-mrn",
  "name": "mrn",
  "status": "active",
  "code": "mrn",
  "base": ["Patient"],
  "type": "token",
  "expression": "Patient.identifier.where(type.coding.code='MR')"
}

Or as a Bundle:

{
  "resourceType": "Bundle",
  "type": "collection",
  "entry": [
    {
      "resource": {
        "resourceType": "SearchParameter",
        "url": "http://example.org/fhir/SearchParameter/patient-mrn",
        ...
      }
    }
  ]
}

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:

scripts/sync-sof-search-params.py            # pinned IG release
scripts/sync-sof-search-params.py --ci-build # preview the IG CI build

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
./target/release/hfs --data-dir /opt/hfs/data

# Via environment variable
HFS_DATA_DIR=/opt/hfs/data ./target/release/hfs

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_RS environment variable is set (for docs.rs builds)

Multi-Tenant Support

Use the X-Tenant-ID header to isolate data between tenants:

curl -H "X-Tenant-ID: clinic-a" http://localhost:8080/Patient
curl -H "X-Tenant-ID: clinic-b" http://localhost:8080/Patient