September 2026 Feature Updates
September opened with dependency upgrades and a round of Elasticsearch indexing work (strict mappings, byte-sized bulk batches, per-tenant concurrency), then spent its middle weeks on two structural changes: a PostgreSQL search backend that runs alongside Elasticsearch, and a single shared artifacts/ directory with a patching pipeline for upstream HL7 files. The last week filled in the search surface on top of that backend (_include, chained parameters, POST _search, Patient/$everything), added a core TestScript for every resource type, and closed with a rewritten admin console built around one command bar.
Backend Changes
PostgreSQL search backend
- A new
pg_searchmodule infhir-searchimplements theSearchEnginetrait on PostgreSQL, selected withHASTE_SEARCH.backend=postgresand indexing into its own database and connection pool. It covers the number, date, string, token, quantity, reference, and URI parameter types, with its own schema migration and indexing path. - Where a parameter is stored depends on whether it can repeat. A singular parameter becomes a scalar column on its resource type's table (
r4_patient_idx), which is what lets an index answer ordered comparisons, prefix matches, and sorts. A repeating one becomes a row per value in a shared per-type table (r4_param_token_idx), keyed by a hashed parameter identity. Every table points back to an anchor row through aBIGINTres_key, so only the anchor carries tenant, project, type, and id. - Cardinality is decided at build time: a new
generate search-param-cardinalitycommand resolves each SearchParameter's FHIRPath expression against the StructureDefinition snapshots, per resource type, and emits a compiled table of single-valued parameters. Anything it cannot prove singular goes to the shared tables, since a scalar column would silently drop extra values. - PRs: #974, #986, #987, #988, #989.
Chained search parameters
- The PostgreSQL backend now supports chains such as
Observation?subject:Patient.name=Smith. Each reference followed is a correlatedEXISTSover the referenced resource's anchor row, nested one level per link, so the planner picks the join order from statistics and no capped id list can silently drop matches. - A reference with several target types needs a
:Typemodifier, the modifier must be one of the declared targets, and chain depth is capped. Chained search is PostgreSQL-only for now, with its own TestScript suite. - PRs: #1014.
_include, _revinclude, and POST _search
- A new
search_includesmiddleware adds the resources a page of matches points at. It runs above the search backend, so it works on both Elasticsearch and PostgreSQL: references are deduplicated to distinct(type, id)pairs and read by primary key, which bounds the cost by the page instead of the corpus.:iteratefollows up to two hops, and a per-search cap on included resources truncates instead of failing. - Included entries now go through the same access-policy check a direct read would face, so an include cannot return a resource the caller could not read, and
_elementsfiltering applies to match entries only. POST [base]/_searchandPOST [base]/{type}/_searchaccept form-encoded parameters, combined with any in the query string. The TypeScript client gained a matching option to send searches as POST, which keeps identifiers out of URLs.- PRs: #997, #999, #1012, #1013.
Patient $everything
Patient/{id}/$everythingreturns the patient and every resource in their compartment as one searchset Bundle. The resource types and linking parameters come from the R4 PatientCompartmentDefinition, so nothing hard-codes a list of clinical types.- One search per (type, parameter) pair runs through the normal client, so access policies, scopes, and auditing apply as they would to the equivalent hand-written searches.
_count,_type,_since,start, andendare supported, results are deduplicated, and a hard entry ceiling keeps one request from becoming a bulk export. - PRs: #995.
Versionless FHIR routes
- The FHIR endpoint dropped its version segment:
/w/{tenant}/{project}/api/v1/fhir/r4is now/w/{tenant}/{project}/api/v1/fhir, with the FHIR version treated as a property of the project. The same string is the tokenaud, so tokens issued against the old path are no longer valid. The TypeScript client, OpenAPI generator, and docs were updated to match. - The root and type-level route handlers were merged into one.
- PRs: #985, #984.
Access policy assignments and federated login
- The token route now resolves a user's policies through
AccessPolicyV2Assignmentresources. Policies that still name the user in the deprecatedAccessPolicyV2.targetkeep applying until they are migrated. - A new
Project.identityProviderSettinglets a project list default access policies per identity provider. The first time a user signs in through that provider, one assignment per policy is created against their new Membership. - The federated callback now verifies
state, surfaces the provider'serrorresponse when an authorization request is rejected, and fills the new user's name and email from the id token'sprofileandemailclaims when present. - PRs: #980, #982, #983.
Subscription tiers in one place
- A new
subscriptioncrate is the single source for what each tier allows: the request budget the rate limiter enforces, per-type resource caps, whether a tenant may set its own branding, and the price and support terms. Requests are weighted by what they cost the server (a write costs 25 points, a search 6, a read 1). haste-health subscription exportwrites the table to JSON for the website's pricing page, with a check mode that fails when the published file is out of date, so prices cannot drift from enforced limits.- PRs: #1000.
HL7v2 over MLLP
- The
hl7v2CLI group gained a hardenedlistencommand (connection limit, idle and partial-message timeouts, graceful shutdown) and asendcommand that waits for the acknowledgement. - The listener now answers with a real
ACKmessage that echoes the sender'sMSH-10inMSA-2, usingAA,AE, orARto tell the sender whether to retry, instead of a bare control byte that interface engines treat as a failure. - The CLI's cached access token is now refreshed a minute before it expires, which long-running processes like the listener depend on.
- PRs: #998, #1004.
Product and UX
Admin console rewrite
- The admin app's resource views were rebuilt as a console with a single command bar. Everything it runs is a verb plus a target, so
GET Patient?name=SmithandDELETE Patient?name=Smithare the same target with different consequences, and conditional writes need no separate shape. The workspace picks its view (results, resource, history, mutation editor) from the target level and the response. - Follow-ups added resource-type and panel headers, a form view built from the resource's StructureDefinition, and an inline project-creation form on the system landing page so a first sandbox needs no trip through the raw editor.
- PRs: #1006, #1007, #1009.
FHIR search input with autocomplete
- A new
FHIRSearchInputcomponent in the components package completes resource types at the root, search parameters after the?, modifiers and reference target types after a:, and prefixes or codes after the=, including the parameters reachable through a chain. It ships with syntax highlighting, a results table, and auseSearchhook, and is the editor behind the console's command bar. - PRs: #1005.
Access policy assignment UI
- The resource editor gained an assignments view for attaching access policies to memberships, client applications, and operations through
AccessPolicyV2Assignment. - PRs: #981.
Platform and Runtime
Search indexing: project-scoped cursors and sequence fixes
- The tenant-wide indexing cursor was replaced by a
search_index_lockstable with one row per (tenant, project, backend). Projects now index independently, and separate search backends keep separate progress markers. Existing projects inherit their tenant's Elasticsearch position. - The sequence functions behind the indexing watermark were rewritten to avoid catalog scans and to fix two skipped-row cases: a first-ever write that took no lock, and a lock-key filter that stopped matching once the sequence passed 2^32.
- PRs: #1003, #971.
Elasticsearch indexing throughput
_bulkrequests are now batched by byte size (targeting 10MB) instead of document count, with each document serialized once and the same bytes sent on the wire.- The index mapping became strict, replica count is preserved across a rebuild, and the number of tenants indexed concurrently in one poll is configurable.
- PRs: #967, #968, #969.
Search modifier fixes
- Modifier handling was pulled into a shared query module in
fhir-searchand corrected across the Elasticsearch and PostgreSQL clause builders for every parameter type. - PRs: #992.
Core TestScripts and conformance coverage
backend/testscripts/corenow holds a TestScript for every R4 resource type plus the Haste Health types, 155 in all, run in CI by a dedicated workflow. The website's conformance section gained a test-coverage page built from them.- The end-to-end workflow runs as a matrix over both search backends, and a separate workflow exercises HL7v2 ingest and replay.
- The TestScript runner now honors
requestHeaderentries, with variables evaluated in header values. - PRs: #992, #993, #994, #991, #976.
Client and runtime internals
- The Rust HTTP FHIR client supports HTTP Basic authentication alongside bearer tokens.
- Each
ResourceTypecan deserialize its resource directly from raw bytes, transaction processing collects references by walking the entry in place, and the FHIRPath evaluator's allocation context moved to a synchronous mutex. - The server's listen port moved from a CLI flag into configuration (
haste.tomlandHASTE_*variables). - PRs: #975, #970, #973, #972, #990.
Artifacts, Tooling, and Maintenance
Centralized artifacts
- FHIR artifacts (profiles, search parameters, terminology, operations, test data) moved to a single top-level
artifacts/directory read by both the Rust and TypeScript stacks, removing the second copy and roughly 1.7 million lines of duplicated JSON. Each package is both an npm package in the frontend workspace and a source of embedded resources for the backend. - PRs: #977, #978.
Artifact patching
- A new
artifact-patchercrate changes upstream HL7 files without editing them. A package opts in with apatches/manifest.toml; RFC 6902 JSON Patch files target a resource by type and id or url, Rust rules run over every resource, and the result is written as.min.json. haste-health artifacts buildproduces the outputs andartifacts difflists every change from upstream, since each edit is recorded as it is applied.- PRs: #979.
Dependency upgrades
- Backend crates, frontend packages, Storybook, and the website's Docusaurus stack were all brought up to date, followed by a batch of Dependabot frontend updates at month end.
- PRs: #963, #964, #965, #966, #1015.
Docs and website
- The generated FHIR reference pages were rebuilt on a set of
FhirModelcomponents (element tree, search parameters, model index), and the configuration, search, and Docker Compose docs were revised for the new search backend options. - PRs: #994, #1001, #1002, #1008.
Standalone Commits (No PR Link)
- Docs: rewritten platform architecture and access control pages, an admin app guide and README update, and a README for the
artifacts/directory. - Website: a new social card, header and margin adjustments, and the test-coverage page wired into the deploy.
- CI: the core TestScripts workflow added on main, and Haste Health artifacts included in the search-parameter cardinality analysis.
- Build/deploy: Docker Compose file updates, base image and audit workflow bumps, and a Wrangler bump.
- Small backend follow-ups: a fix for system-level invocation in the HTTP client, a CORS change to mirror the request, and removal of an outdated test.
- Version bumps:
d87db8dc2.
Contributors
Thanks to Luni-4 for continuing the clippy remediation into the remaining crates: the first pass over the server crate (#960), and fhir-generated-ops (#932), which fixed the lints at the source by changing the operation codegen.
