Changelog
All notable changes to ReFHIR are recorded here. The format follows Keep a Changelog, and versions follow Semantic Versioning.
While the version is 0.x, a release can change configuration, the HTTP API or the /sync
protocol. When it does, the entry says so under Changed. Database upgrades are forward-only
(Schema migrations): back up before you upgrade.
- Subscriptions send
channel.header. Each entry (R4 saysName: value, for exampleAuthorization: Bearer …) is added to therest-hookrequest, so a receiver that needs a token can be served. A name can be given once. An entry that is not a valid header, that repeats a name, or that sets a header the server controls (Host,Content-Length,Transfer-Encoding,Connection,Keep-Alive,Upgrade,TE,Trailer,Expect,Cookie,Proxy-*) fails the notification and puts the Subscription inerrorwith the reason. AContent-Typeentry replaces the default, whatever its case. - A Subscription’s
statusfollows its deliveries. When arest-hooknotification still fails after its eight attempts (about ten seconds), the server setsstatustoerroranderrorto the time and the reason. The next notification that gets through setsstatusback toactiveand clearserror. Delivery continues while the status iserror; onlyoffstops it, and the server never changes anoffSubscription. Prefer: handling=lenienton a searchGET. A search whose parameter value cannot be read (see Changed) can ask for the parameter to be left out instead: the search runs on the rest, the Bundle carries anOperationOutcomeentry (search.modeoutcome) with a warning for each parameter set aside, and itsselflink no longer has them. The response saysPreference-Applied: handling=lenient, and search responses now carryVary: Prefer.handling=strictis the default. Conditional create, update and delete,batchandtransactionentries, GraphQL and live queries are never lenient. See FHIR API.
Changed
Section titled “Changed”- Naming more than one database on the command line is now an error.
--sqlite,--memoryand--postgresare alternatives, but ReFHIR used to start without complaint and use one of them:refhir --memory --sqlite x.dbran in memory and never createdx.db. It now exits withpass only one of --sqlite, --memory, or --postgres, as the CLI reference has always said, and that includes the same flag twice.--sqliteor--postgresfollowed directly by another flag is refused too, where it used to take the flag as its value (--sqlite --memoryopened a file called--memory).refhir migratedoes the same for--sqliteand--postgres(exit status 2). A single flag still overrides the database the config file or the environment names. If a script passes two, remove the one you did not mean. - A
rest-hookendpoint that redirects has not received the notification. Redirects are no longer followed, and a3xxis a failed delivery. A301,302or303used to turn thePOSTinto aGET, and the notification counted as delivered. - Retries are spaced. A failed notification waits at least a second and a half between
attempts. The retry queue also runs after every commit, so on a busy server all eight attempts
could be used up in a moment. Before each retry the Subscription is read again: if it has been
deleted, is
offor sends somewhere else the notification is dropped, and a changed header is the one sent. - A
rest-hookchannel with no endpoint ends inerror. It was skipped without a word. - A notification for a Subscription leaves out its
channel.header. A Subscription whosecriteriaareSubscriptionis told about every one written, and would otherwise have been sent their credentials. - The server now writes to Subscriptions. The
statusanderrorchange above is an ordinary update, so a Subscription can gain a version you did not write (visible in its_history, not in the audit trail: there is no caller to name). AnIf-Matchthat names an older version fails with409, as for any concurrent edit. - Subscription behavior is documented in full (Subscriptions):
only creates and updates are notified, as R4 defines it (a delete, or a resource that stops
matching, sends nothing);
channel.payloadandendare ignored; nothing limits where an endpoint may point; retries are in memory and a restart drops them; the WebSocket channel has no handshake and keeps one socket per id.docs/design.mdno longer lists Subscription delivery with the durable jobs: only$exportis durable. - A comma in a search value now separates alternatives wherever the value comes from: a query
string,
If-None-Exist, a conditional update or delete, a Bundle entry. A client that sent a value holding a comma without escaping it (identifier=a,bfor the identifiera,b) now matches the resources withaor withbinstead, so a conditional create can be blocked by, and a conditional update or delete can act on, a different resource than before. Write\,for a comma that is part of a value. - A search parameter given more than once is AND, as FHIR defines it, in a query string, in the
URL of a Bundle entry and in Subscription criteria (only the last one counted in the last two).
The values were joined with a comma, which now means OR, so
date=ge2026-01-01&date=lt2026-02-01would have become a match on either bound. _revinclude=*is a400naming the problem. It used to return no reverse includes and say nothing._includeand_revincludeno longer add another patient’s resources for a patient-scoped token, as a search does not return them. A match that another match refers to is no longer repeated as an include. The CapabilityStatement’srest.documentationstates how search treats commas, repeated parameters,:iterateand the limits above.- A bare id given to a reference parameter that can point at any resource (
Task?subject=123) or that lists notargetis now a400that asks forType/id; it used to find nothing. - A search whose parameter value cannot be read is now a
400, where it used to return an empty result.GET /Patient?birthdate=notadateandGET /Observation?value-quantity=abcanswered200with an empty Bundle, which looks the same as a search that matches nothing; they now answer400with anOperationOutcomethat names the parameter, the value and what was expected. It covers a date, number or quantity that does not parse, a composite with the wrong number of parts, anearthat is notlatitude|longitude,:missingwithouttrueorfalse, and:of-typewithout exactlysystem|code|value, also behind a chain or_hasand inside a_filter(where a parameter that does not exist is now refused too). Strings, tokens, uris and references are free text and are not affected. The same check now refuses a conditional create, update or delete that used such a value, which used to match nothing (and so create), a live query over/sync, which answersQueryFailedat once instead of staying empty, and a query in a Questionnaire that$populateruns, which now fails with the message instead of populating from nothing. Each alternative of a comma list, and each occurrence of a parameter given twice, is read on its own. A client that relied on the empty result can sendPrefer: handling=lenient.
- A comma between the values of one search parameter is OR.
Slot?status=free,busyandHealthcareService?_id=a,bmatched nothing, because the whole value was compared as one. This holds for ids and every parameter type, with\,for a literal comma, and a value lists at most 1000 alternatives.:notand:not-inwith a list leave out every value in it. _include:iterateand_revinclude:iterateare accepted instead of refused as an unknown parameter. They apply a rule to the matches and to the resources already added, until none is added, and a search that would add more than 1000 resources that way is a400.- A reference search given just an id found nothing.
Observation?patient=123now returns whatObservation?patient=Patient/123returns. A bare id is looked up as a reference to each type the search parameter can point at, sosubject=123finds aPatient/123or aGroup/123. Live queries over/syncread ids the same way, and are woken only by writes that reference that id.
0.0.1 - 2026-10-15
Section titled “0.0.1 - 2026-10-15”The first release. Everything below is new.
- A FHIR R4 (4.0.1) server. Every resource type in the installed packages gets read, vread,
create, update, JSON Patch, delete, history, search, conditional create, update and delete, and
batchand atomictransactionBundles. Reads can be returned as XML. Resources are validated against the installed StructureDefinitions, and references are checked on write (strictby default,fhiroptionally). The server assigns resource ids;--update-as-createletsPUTcreate under a client-chosen id. - Search over the full R4 search parameter set, compiled from the installed packages and
backfilled in the background when a package or a
SearchParameteradds one. Chaining,_has,_include,_revincludeand compartment search. - Operations:
$everything,$export(system, type and instance, as durable jobs),$validate,$meta,$meta-add,$meta-delete,$document,$installfor packages, terminology ($expand,$lookup,$validate-code,$subsumes,$translate,$expandability), and Structured Data Capture 4.0.0 ($populate,$extract,$assemble,$next-question,$behavior, with an optional form-archiver mode). - GraphQL at
/graphql, and Subscriptions withrest-hookandwebsocketchannels. - Reactive queries over a
/syncWebSocket: subscribe to the result of a FHIR query and receive each committed change to it, with a transaction Bundle arriving as one step. Go and TypeScript clients (the latter with a React hook,useFHIRQuery). REST reads that follow a write can wait for a replica to catch up, throughX-Refhir-Commit-TsandX-Refhir-Min-Ts. - SMART App Launch 2.2 as authorization server and resource server: authorization code with
PKCE, EHR launch, refresh tokens, backend services (
private_key_jwt), dynamic client registration, token introspection and revocation, v1 and v2 scopes, and patient-compartment enforcement on REST and/sync. Users sign in through OpenID Connect, with an allowlist, binding to aPractitionerorPatient, and token introspection for a gateway. - Governance: per-client metering (
/_usage), a request log (/_requests) and an audit trail ofAuditEvents (/_audit) that is hash-chained and verifiable (/_audit/verify), all on by default; rate limits and quotas that you configure./syncis governed like HTTP. - Storage in SQLite (a file, or in memory) or Postgres, with Postgres TLS (
sslmode,sslrootcert,sslcert,sslkey) and forward-only schema migrations.refhir migrate --statusand--dry-runreport what a start would do. - Several processes on one database: a writer lease with fencing, standbys that take over,
and read replicas that forward writes.
/livez,/readyzand/healthzprobes. - Packages installed from packages.fhir.org, a mirror, an S3 bucket, a
.tgzor a folder, at start-up or through$install. Value sets are expanded with their own package’s terminology, and cross-version copies do not shadow R4’s. - Deployment: a container image, a Helm chart (single process or a Postgres cluster), a Docker Compose cluster behind HAProxy, and an Inferno profile for SMART conformance runs.
refhir --version.