Project overview
[!IMPORTANT] Ilavrita serves FHIR R4 today, and is not ready for patient data.
The by-key interactions, search, authorization, audit and payload storage are implemented and tested across 126 resource types. What is missing is listed below and in docs/known-limitations.md; the CapabilityStatement advertises only what the routes actually serve, because a server must never claim behaviour it has not implemented and tested.
Do not put patient data in this build yet. Not because the boundaries are absent — they are enforced and tested — but because:
- Nothing checks a resource against a profile. The R4 base definitions and value sets are embedded and seeded, and every write is checked against the ones for its type — undeclared elements, missing required ones, cardinality, choice types, primitive syntax and required bindings are all refused. What is not checked is a profile that constrains the base, nor a FHIRPath invariant, nor whether a reference resolves.
- There is no migration story beyond the schema this build applies to its own database at startup. Backup and restore exist; upgrading between releases is what does not.
- Nothing here has been through an external security review or an official FHIR conformance suite. The tests are ours.
Picking this up cold? Start with HANDOFF.md — what works, what does not, and the traps that are invisible from the code.
Contents
- Why Ilavrita
- What works today
- Quick start
- The API
- Architecture
- Repository layout
- Development
- Project setup
- Security and compliance
- Licensing
Why Ilavrita
Healthcare teams rebuild the same primitives every time: clinical resources, version history, search, validation, access control, audit, file handling and a safe way to deploy. Every team that builds them again spends months not building their actual product, and arrives at slightly different interoperability behaviour. Ilavrita exists so that work is done once, in the open.
Many of those teams also need something far smaller than a distributed application stack. A clinic, a pilot, a device-adjacent service or a local development environment should not require an external database to run a standards-compliant FHIR server.
Ilavrita targets both ends: a single binary with SQLite for small deployments, and an architecture that does not have to be rewritten to reach a clustered one.
What works today
| Capability | State |
|---|---|
GET /healthz, GET /version | Working |
GET /fhir/R4/metadata | Working — advertises exactly what the routes serve |
| FHIR create, read, update, delete, history, vread | Working, 126 resource types |
| FHIR search | Working — GET and POST /_search, over a declared parameter set |
| Project isolation and authorization | Enforced — compartments, element filters, field restriction |
| Audit trail | Working — every interaction and login, in the transaction that did it |
| Authentication | Working — password, sessions, TOTP second factor with an administrator recovery path, per-install throttle |
| Bringing an install into use | Working — one claim token, written to the data directory, spent once through POST /auth/claim |
| Binary payloads | Working — bytes stored outside the database |
| Subscriptions | Working — rest-hook, and websocket within one process; queues are claimed, so replicas do not notify twice |
Resource validation, $validate | Against the R4 base definitions, required bindings and every FHIRPath invariant R4 states |
| Declared profiles | meta.profile is resolved and its resource-level rules applied; one this server does not hold is reported, never silently passed |
| Reference integrity | $validate reports a relative reference that leads nowhere; writes do not refuse one, because R4 permits it |
| FHIR R4 representation | Checked against the HL7 validator by scripts/conformance.sh, not only against our own tests |
| Search parameters | Built-in set, plus any a Project defines with a SearchParameter; new ones backfill in the background |
| Search modifiers | :missing, :exact, :contains, :not — every other one is refused by name |
| LOINC and SNOMED | Not resolved — a code is stored and read back, never looked up or checked |
| Bundle transaction | Working — all-or-nothing, with urn:uuid: references resolved across entries |
| Bundle batch | Working — every entry settles on its own, inside a savepoint |
| Conditional create, update and delete | Working — a search names the target, run under a grant for searching that type |
| Conditional read | Working — If-None-Match and If-Modified-Since answer 304 |
Prefer: return= | Working — minimal, representation and OperationOutcome |
| History filtering | _since narrows; _at and _list are refused, never ignored |
| Type-level and system-level history | Not implemented |
| Patch | Not implemented |
| XML | Not implemented — JSON only |
| Backup and restore | Working — ilavrita backup, verify-backup, restore |
| Whole-system history | Not implemented |
| PostgreSQL, SMART, Bulk Data, HL7v2, DICOM | Out of scope for v0.1 |
Search is deliberately narrow: every parameter is a projection a write maintains
and a predicate a read compiles, so packages/search/registry.go lists what is
actually answered rather than what R4 defines. A parameter outside it is refused
with 400, never ignored — a search that silently drops a criterion returns more
than it was asked for, and the caller cannot tell.
docs/known-limitations.md is the authoritative list, and ROADMAP.md is the order things arrive in.
Quick start
Requires Go 1.27+, and Node 22+ with pnpm 10+ for the workspace tooling.
git clone https://github.com/Ilavrita/Ilavrita.git
cd Ilavrita
make bootstrap
make run
make bootstrap fetches the pinned embedded-runtime fork. A plain clone without
submodules will not compile — go.mod replaces embedded-runtime with
third_party/embedded-runtime, and that directory has to exist.
Container
docker compose up --build
Images are published per release for linux/amd64 and linux/arm64 with build
provenance attached:
docker run --rm -p 8090:8090 -v ilavrita-data:/data ghcr.io/ilavrita/ilavrita:latest
The API
$ curl -s http://127.0.0.1:8090/healthz
{"status":"ok"}
$ curl -s http://127.0.0.1:8090/version
{"version":"dev","revision":"unknown","fhirVersion":"4.0.1"}
GET /version reports the commit the binary was built from, which is how a
running instance is matched to a source tag and a release artefact.
$ curl -s -H 'Accept: application/fhir+json' http://127.0.0.1:8090/fhir/R4/metadata
{
"resourceType": "CapabilityStatement",
"status": "draft",
"kind": "instance",
"fhirVersion": "4.0.1",
"format": ["application/fhir+json"],
"software": {"name": "Ilavrita", "version": "dev"},
"rest": [{
"mode": "server",
"resource": [
{
"type": "Patient",
"interaction": [
{"code": "create"}, {"code": "read"}, {"code": "update"},
{"code": "delete"}, {"code": "history-instance"}, {"code": "vread"},
{"code": "search-type"}
],
"searchParam": [
{"name": "_id", "type": "token"}, {"name": "_lastUpdated", "type": "date"},
{"name": "identifier", "type": "token"}, {"name": "family", "type": "string"},
{"name": "given", "type": "string"}, {"name": "gender", "type": "token"},
{"name": "active", "type": "token"}, {"name": "birthdate", "type": "date"}
],
"operation": [{"name": "validate", "definition": "..."}],
"versioning": "versioned",
"updateCreate": true
}
]
}]
}
rest[].resource carries 126 entries, one per declared type. A resource appears
there only once the conformance suite covers every status and header rule for it,
and the statement is generated from the routes that were actually registered — so
it cannot name something this build does not serve.
Every FHIR route requires an authenticated principal:
$ curl -s -i http://127.0.0.1:8090/fhir/R4/Patient/123 | head -2
HTTP/1.1 401 Unauthorized
Content-Type: application/fhir+json
{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"diagnostics": "This request carries no authenticated principal."
}]
}
A type the statement does not declare answers 404 on every route, and an
interaction this build does not implement answers 501 — always as an
OperationOutcome, never a framework error:
$ curl -s -i http://127.0.0.1:8090/fhir/R4/Appointment/123 | head -1
HTTP/1.1 404 Not Found
$ curl -s -i http://127.0.0.1:8090/fhir/R4/Patient/_history | head -1
HTTP/1.1 501 Not Implemented
The HTTP surface is described in api/openapi.yaml and CI
checks that description against a live server on every change. See
docs/api.md for why both OpenAPI and CapabilityStatement exist and
which to trust.
Architecture
embedded-runtime is the runtime foundation. It is not the product, and it is not the public contract.
Everything Ilavrita publishes — routes, error shapes, search semantics, versioning, tenancy and authorization — belongs to Ilavrita and sits behind its own interfaces. That indirection is why a PostgreSQL backend can be added later without rewriting FHIR behaviour, and why a client written against Ilavrita today keeps working when storage changes underneath it.
Four rules hold the layering together, and the first is enforced by a depguard
rule in .golangci.yml rather than by convention:
- Only
packages/storage/embedded-runtimeimports the embedded-runtime runtime. - No SQL above the storage backend. Higher layers reason in FHIR terms.
- No embedded-runtime concept reaches
/fhir/R4— not a collection name, not an admin route, not an error shape. embedded-runtime's own API (/api) and admin console (/_) are disabled unlessILAVRITA_EXPOSE_embedded-runtime=true. - Never advertise what is not implemented. If the CapabilityStatement says a resource is supported, a test proves it.
docs/architecture.md covers the search pipeline, transaction boundaries and the tenancy model. The reasoning behind the big decisions is in docs/adr.
Repository layout
apps/ilavrita the server binary
apps/console operator console (not implemented)
apps/docs documentation site (not implemented)
packages/fhir FHIR R4 types published at the boundary
packages/search query parsing and planning
packages/storage the persistence interfaces services depend on
packages/storage/embedded-runtime SQLite backend — the only package that may import embedded-runtime
packages/project Project isolation boundary
packages/authz authorization decisions
packages/audit security event records
packages/files Binary and DocumentReference payload storage
packages/config runtime configuration
packages/observability logging, correlation, health
packages/sdk-typescript TypeScript client
api/openapi.yaml HTTP surface description
third_party/embedded-runtime the pinned Ilavrita embedded-runtime fork
Development
make build # compile the server
make test # Go tests
make lint # golangci-lint
make fmt # format
pnpm build # everything, through Turborepo
API description
make api-lint # valid OpenAPI 3.1
make api-verify # description matches a running server
make api-docs # render docs/api.html
Running CI locally
act runs the workflows on your machine:
brew install act
make ci-local
make ci-image builds the runner image act uses. The stock act image loses
node from PATH once actions/setup-go runs, which breaks every JavaScript
action after it; build/docker/act-runner.Dockerfile
fixes that so local runs match CI.
Changelog
CHANGELOG.md is generated from commit history by git-cliff. Never hand-edit it:
make changelog
This is why commits are single-line Conventional Commits. A vague subject line becomes a vague changelog.
Project setup
| Area | Where |
|---|---|
| Contributing | CONTRIBUTING.md |
| Code of conduct | CODE_OF_CONDUCT.md |
| Governance | GOVERNANCE.md |
| Getting help | SUPPORT.md |
| Branch protection | docs/branch-protection.md |
| Release process | docs/releasing.md |
Branches are feature → dev → main → tag. Releases are cut from main only, and
the release workflow refuses a tag whose commit is not an ancestor of
origin/main. Protection rules are versioned as JSON in
.github/rulesets so a change to who can push where is
reviewed like any other change.
Pull requests are labelled automatically by area and size.
Security and compliance
Report vulnerabilities through SECURITY.md, never a public issue.
docs/security.md describes the controls and says for each one whether it is enforced today. Project isolation, authorization, privilege separation, authentication, audit and the rules around deleted data are enforced and tested. Request correlation is not implemented, and TLS is a deployment concern.
Nothing here has had an external security review. The tests are ours, and that is the reason to keep patient data out of this build — not an absent boundary.
Releases are signed and attested — container images with Cosign plus build provenance and an SBOM bound to the digest, npm packages with npm provenance. docs/supply-chain.md has the verification commands.
Dependencies are scanned by CodeQL, GitHub dependency review and FOSSA. Findings are reviewed and recorded in docs/license-compliance.md rather than silently ignored.
The FOSSA licence check is red, and is an open work queue — not something the
green engineering checks cover. Two questions are genuinely open: whether
modernc.org/libc's glibc-derived headers affect the planned commercial licence,
which needs legal review rather than a scanner, and CVE-2023-36308 in a transitive
dependency with no fixed version. The rest are believed to be scanner artefacts.
That page carries the live count and the date it was read; this one deliberately
does not, because a number here would be stale the week after it was written.
Ilavrita holds no certification, and running it does not make an organisation compliant with HIPAA, GDPR, EHDS or any other regime. It provides technical controls. Compliance is a property of your deployment, your policies and your organisation.
Licensing
AGPL-3.0-only — see LICENSE. A commercial option is planned but its terms are not final; see COMMERCIAL-LICENSE.md.
Ilavrita builds on embedded-runtime (MIT), whose notice is preserved at LICENSES/embedded-runtime-MIT.txt. Per-dependency attribution is in THIRD_PARTY_NOTICES.md.
The Ilavrita name and marks are not covered by the AGPL; see assets/brand/README.md.
Source revision 9462213 · dev
