---
{
  "title": "FHIR Package Management: Pinning, Tree-Shaking, and Why Runtime Resolution Had to Go",
  "description": "How Aidbox moved canonical resolution from runtime to configuration time — with pinning, tree-shaking, and a deterministic candidate selection algorithm.",
  "date": "2026-04-03",
  "author": "Evgeny Mukha",
  "reading-time": "10 min read",
  "tags": ["FHIR Standard", "Aidbox", "Infrastructure"],
  "tldr": "Aidbox now resolves all canonical versions at configuration time — not runtime. Pinning locks every reference to an exact version, tree-shaking strips unused canonicals from dependencies, and the result is a flat, fully resolved set where every lookup is just URL+version."
}
---

## The Problem with Runtime Resolution

Before the 2511 release, Aidbox resolved all canonical references at runtime. When a StructureDefinition references `http://hl7.org/fhir/us/core/StructureDefinition/us-core-race` without a version, Aidbox had to determine which version to use. And even when the result was cached, the cache was fragile. Installing a new package could introduce new canonicals, reset caches, and cause the same reference to resolve to a different version.

This is how most FHIR packages are published. Canonicals contain outgoing references to other canonicals — ValueSets pointing to CodeSystems, profiles pointing to base StructureDefinitions — and the vast majority of these references carry no version. The assumption is that the consumer will figure it out from context.

That assumption pushes the responsibility of version resolution onto the consumer.

The real issue wasn't performance — it was the lack of context. At runtime, all you have is a flat set of installed packages and their canonicals. But which packages were explicitly installed by the user? Which ones are transitive dependencies? Which ones depend on what? That information was effectively lost after installation. Every package was just a bag of canonicals dumped into the same pool. Because the resolution result depended on whatever happened to be in that pool, installing or removing a package could silently change which version a reference resolved to. Pieces of the dependency graph could appear or disappear without the resolver being able to detect it.

This made complex dependency scenarios difficult to handle correctly. Take `hl7.fhir.us.davinci-hrex` — it pulls in **three different versions of `hl7.fhir.us.core`**. When a profile from HRex references `http://hl7.org/fhir/us/core/StructureDefinition/us-core-provenance` without a version, which of the three US Core versions should Aidbox pick? This cannot be reliably deduced at runtime — all three are present simultaneously, and there's no information left to disambiguate. This decision must be made before the system starts, not during a validation call.

We needed a way to resolve all these ambiguities once, upfront, when the full dependency graph and user intention are still available — not at runtime when they're gone.

The FHIR community had already been moving in this direction. The [FHIR IG Guidance on Pinning](https://build.fhir.org/ig/FHIR/ig-guidance/pinning.html) laid the groundwork — the spec describes the intended resolution algorithm:

1. Find the package that contains the resource with the reference
2. Compile the full list of that package's dependencies (transitive)
3. Find all resources matching the canonical URL within those packages
4. Select the most recent version

This sounds straightforward, but the complications pile up fast. "Most recent" is ambiguous when you have semver, date-based, and arbitrary version strings. Multiple resources can have the same URL and version. And in a RESTful API context, canonicals arrive without package metadata — the server may not know which package "owns" a given canonical at all.

The spec is deliberately light on these implementation details — it leaves a lot up to implementers. So we reverse-engineered what we could from existing tools that attempt pinning — the [IG Publisher](https://github.com/HL7/fhir-ig-publisher) and [UploadFIG](https://github.com/brianpos/UploadFIG) — and finalized the resolution strategy at **[FHIR Camp 2025](https://www.health-samurai.io/events/fhir-camp-2025)** in collaboration with [Grahame Grieve](https://www.linkedin.com/in/grahame-grieve-952637), [Gino Canessa](https://www.linkedin.com/in/gino-canessa/), and [Lloyd McKenzie](https://www.linkedin.com/in/lloyd-mckenzie-6b6681/).

In Aidbox, this results in a two-phase approach to package handling: all the hard work happens before actual server runtime, and runtime gets a flat, fully resolved canonical set.

## Two-Phase Approach: Configuration and Runtime

```mermaid
flowchart LR
    A(FHIR Packages):::blue2 --> B(Configuration Phase<br/>pinning + tree-shaking):::green2 --> C(Runtime Phase<br/>flat canonical lookup):::violet2
```

**Configuration phase.** When you install a package — via the API, the UI, or the [`BOX_BOOTSTRAP_FHIR_PACKAGES`](https://docs.aidbox.app/reference/all-settings#bootstrap-fhir-package-list) config — Aidbox downloads the package and all its dependencies, then walks the entire dependency graph. Every unversioned canonical reference gets rewritten with an explicit `|version` suffix:

```
before: http://hl7.org/fhir/ValueSet/observation-status
after:  http://hl7.org/fhir/ValueSet/observation-status|4.0.1
```

At the same time, canonicals from dependency packages that nobody actually references are dropped — this is tree-shaking.

**Runtime phase.** All canonicals are already resolved. Every outgoing reference carries an explicit version. For Aidbox, resolving a canonical is now a simple database query by `<url>|<version>` — no graph traversal, no context-dependent resolution, no ambiguity. Packages as a concept no longer exist at runtime; only fully resolved canonicals remain.

## How Pinning Works in Aidbox

The pinning process walks every canonical in the installed packages and rewrites its outgoing references with exact versions.

### Collecting Outgoing References

For each canonical resource, Aidbox collects all outgoing canonical references — URLs that point to other canonicals. It does this for all five canonical resource types that can contain outgoing references:

- **StructureDefinition** (excluding `snapshot` — only `differential` matters)
- **CodeSystem**
- **ValueSet**
- **SearchParameter**
- **CapabilityStatement**

For example, a StructureDefinition might reference a ValueSet for a binding, a base StructureDefinition it derives from, and extension definitions it uses. Each of these references needs a pinned version.

### Building the Candidate List

For each outgoing reference, Aidbox builds a candidate list: all canonicals with the matching URL available across the package's dependency tree (including transitive dependencies). If a package depends on `hl7.fhir.us.core@5.0.0`, and US Core depends on `hl7.terminology.r4`, then terminology canonicals are valid candidates.

### The Candidate Selection Algorithm

When multiple candidates exist — different versions, different packages, same canonical URL — Aidbox selects the best one using a deterministic, multi-stage comparison chain:

| Priority | Criterion | Rule |
|----------|-----------|------|
| 1 | **Status** | `active` > `draft` > `retired` > `unknown` |
| 2 | **Terminology over Core** | Canonicals from `hl7.terminology` packages take precedence over identically-named canonicals from core packages |
| 3 | **Version** | Compared using the detected algorithm: semver, integer, date, or alphabetic |
| 4 | **lastUpdated** | Final tiebreaker using `meta.lastUpdated` |

Before comparison, candidates are filtered. Aidbox excludes canonicals from packages matching known noise patterns — expansions, examples, search-related, elements, corexml — and CodeSystem resources whose `content` is not `"complete"`.

**Version algorithm detection** deserves a note. Aidbox first checks `versionAlgorithmString` or `versionAlgorithmCoding` on the resource (an R5+ feature). If neither is present — which is most of the time with R4 content — Aidbox infers the scheme by inspecting the version string: does it look like semver (`1.2.3`)? An integer (`4`)? A date (`2024-01-15`)? Fallback is alphabetic comparison. This matches the practical approach recommended in the FHIR IG Guidance.

### Recursive Pinning

Pinning is recursive. Once a reference is pinned to a specific canonical, that canonical's own outgoing references are pinned too — and so on, down the entire dependency chain. Each `<url>|<version>` pair is processed only once (memoized), so the process terminates even with circular references.

This recursive walk is also what drives tree-shaking.

## Tree-Shaking: Only What You Need

A typical FHIR package carries hundreds or thousands of canonicals. But when you install a package, you don't need all the canonicals from its dependencies — you only need the ones that are actually referenced.

Consider installing `hl7.fhir.us.core@5.0.0`. It depends on `hl7.terminology.r4` (4,158 canonicals) and `us.nlm.vsac` (8,918 canonicals). But US Core only references a small subset of each. Why load the rest?

Tree-shaking is the answer. During the recursive pinning walk, Aidbox tracks which dependency canonicals are actually reached by following outgoing references from the target package. Only these reachable canonicals — and their own recursive dependencies — make it into the final canonical set.

The result:

- **Target package**: all canonicals are included (this is what the user asked for)
- **Dependency packages**: only referenced canonicals are included (tree-shaken)

The reduction is dramatic. Compare the `installedCanonicals` counts from a real installation:

```http
POST /fhir/$fhir-package-install
Content-Type: application/json

{
  "resourceType": "Parameters",
  "parameter": [
    { "name": "package", "valueString": "hl7.fhir.us.core@5.0.0" }
  ]
}
```

The response shows how dramatically tree-shaking reduces the canonical count. Out of 4,158 canonicals in `hl7.terminology.r4`, only 7 survive. Out of 8,918 in `us.nlm.vsac`, only 20:

| Package | Installed Canonicals | Intention |
|---------|---------------------|-----------|
| `hl7.fhir.us.core@5.0.0` | 194 | direct |
| `us.nlm.vsac@0.3.0` | 20 | transitive |
| `hl7.fhir.uv.sdc@3.0.0` | 11 | transitive |
| `hl7.terminology.r4@3.1.0` | 7 | transitive |

Every canonical in the system is unique by `<url>|<version>`. No duplicates, no conflicts, no ambiguity.

## Package Sources

Aidbox supports three ways to obtain packages:

```mermaid
flowchart RL
    A(NPM Registry<br/>fs.get-ig.org/pkgs, Simplifier, Verdaccio):::violet2
    B(Local Filesystem<br/>/srv/aidbox-fhir-packages):::green2
    C(Direct URLs<br/>https:// or file://):::yellow1

    E(Aidbox Artifact Registry):::blue2

    A -->|npm| E
    B -->|file| E
    C -->|url| E
```

**NPM registries.** The default registry is `https://fs.get-ig.org/pkgs`, which mirrors `packages2.fhir.org` with daily syncs. You can point Aidbox to any NPM-compatible registry — [Simplifier](https://packages.simplifier.net), a private [Verdaccio](https://verdaccio.org/) instance, or any custom mirror.

**Local filesystem.** Mount a directory with `.tgz` packages to `/srv/aidbox-fhir-packages` in the Aidbox container. Packages must follow the naming convention `{name}#{version}.tgz` — for example, `hl7.fhir.r4.core#4.0.1.tgz`. Aidbox checks local packages first before hitting the remote registry. This is essential for air-gapped environments and also speeds up startup significantly.

**Direct URLs.** You can pass an `https://` URL pointing to a `.tgz` tarball or a `file://` path to a local file. Useful for installing packages that aren't published to any registry — like custom IGs built locally with [SUSHI](https://fshschool.org/docs/sushi/).

## Dependency Overrides

Remember the `hl7.fhir.us.davinci-hrex` problem from earlier — three versions of US Core, impossible to disambiguate at runtime? With overrides, the user makes this choice explicitly at configuration time.

HRex pulls in `hl7.fhir.us.core@7.0.0`, `hl7.fhir.us.core.v610`, and `hl7.fhir.us.core.v311`. If you only need the 7.0.0 version, skip the other two:

```http
POST /fhir/$fhir-package-install
Content-Type: application/json

{
  "resourceType": "Parameters",
  "parameter": [
    { "name": "package", "valueString": "hl7.fhir.us.davinci-hrex@1.1.0" },
    {
      "name": "override",
      "part": [
        { "name": "from", "valueString": "hl7.fhir.us.core.v610" },
        { "name": "to", "valueBoolean": false }
      ]
    },
    {
      "name": "override",
      "part": [
        { "name": "from", "valueString": "hl7.fhir.us.core.v311" },
        { "name": "to", "valueBoolean": false }
      ]
    }
  ]
}
```

Three types of overrides:

| Override | Syntax | Use Case |
|----------|--------|----------|
| **Skip dependency** | `"to": false` (valueBoolean) | Exclude a dependency — like dropping unwanted US Core versions |
| **Pin version** | `"to": "0.17.0"` | Force a specific version of a dependency |
| **Replace package** | `"to": "npm:alt.package@1.0.0"` | Swap one package for another entirely |

The `from` field supports both name-only matching (`hl7.fhir.us.core` — matches any version) and version-qualified matching (`hl7.fhir.us.core@6.1.0` — matches only that exact version). Version-qualified overrides take priority.

Overrides also solve registry gaps. `hl7.fhir.us.core@8.0.0` depends on `us.nlm.vsac@0.23.0`, but Simplifier stopped hosting VSAC versions after `0.17.0`. Pin it to `0.17.0`, skip it entirely, or replace it with an alternative — without modifying the source packages.

## What About Runtime Installation?

Does this mean you lose the ability to install packages and canonicals at runtime? No — but with a few clarifications.

**Packages** installed at runtime are still pinned automatically. The difference is that pinning happens in an isolated context: only the package's own dependency tree is involved. Already-installed content is unreachable during this pinning pass, so the new package gets a self-consistent canonical set without interfering with what's already loaded.

**Individual canonicals** that you upload directly — outside of any package — are your responsibility to pin. If you provide a canonical with unversioned outgoing references, Aidbox won't attempt to pin them for you. It does, however, retain a fallback mode for unpinned references: it will use the same candidate selection algorithm described above — status, terminology priority, version comparison — to resolve to the best available match. So nothing breaks — you just don't get the determinism guarantees that package-level pinning provides.

## What This Means in Practice

Here's what this gives you in Aidbox:

**Predictability.** The same package installation input always produces the same canonical set. There's no runtime ambiguity, no order-dependent behavior, no hidden state affecting resolution.

**Performance.** Canonical lookups are simple database queries by `<url>|<version>`. No graph traversal, no dependency walking, no caching heuristics.

**Debuggability.** Every canonical in the system has an explicit version. When validation fails, you see exactly which version of which StructureDefinition was used — not "whatever happened to resolve at that moment."

**Smaller footprint.** Tree-shaking means transitive dependencies contribute only the canonicals that are actually needed. Installing US Core pulls in 7 canonicals from `hl7.terminology.r4` instead of 4,158.

The tradeoff is that installation takes a bit longer — Aidbox needs to walk the full dependency graph, build candidate lists, and recursively pin references. But for the initial package set, this happens only once, on first start.

## References

**Spec:**
- [FHIR IG Guidance: Managing Canonical Versions (Pinning)](https://build.fhir.org/ig/FHIR/ig-guidance/pinning.html)
- [FHIR NPM Package Specification](https://build.fhir.org/packages.html)

**Aidbox docs:**
- [Artifact Registry Overview](https://docs.aidbox.app/modules-1/profiling-and-validation/fhir-schema-validator)
- [FAR Package Management API](https://docs.aidbox.app/reference/package-registry-api)

**Tools:**
- [IG Publisher](https://github.com/HL7/fhir-ig-publisher) — HL7's official Implementation Guide publisher
- [UploadFIG](https://github.com/brianpos/UploadFIG) — Brian Postlethwaite's canonical upload tool
- [SUSHI](https://fshschool.org/docs/sushi/) — FHIR Shorthand compiler for building custom IGs
- [Verdaccio](https://verdaccio.org/) — lightweight NPM proxy/registry for private hosting

**Events:**
- [FHIR Camp 2025](https://www.health-samurai.io/events/fhir-camp-2025) — where the resolution strategy was finalized
