Plugin SPI
What the SPI is
The subscription-service engine ships with a small, stable Service Provider Interface (SPI) that lets anyone extend the pipeline without forking the core repo. A plugin is a JAR drop-in or a YAML profile bundle — there is no JavaScript / Lua / WASM runtime to learn; the JVM ServiceLoader mechanism does the discovery, and the configuration is plain HOCON / YAML.
The full canonical reference for each interface lives in the engine repo under plugins-spi/README.md. This page summarizes the eight extension surfaces and points you at the right starting place.
The eight extension surfaces
Each surface is a single Java/Kotlin interface (or, for vendor profiles, a YAML schema). Implementations are discovered at boot, validated against the SPI version the engine ships, and bound into the pipeline at the documented hook points.
1. HL7VendorProfile
The most common kind of plugin. A profile bundles:
- message-type → StructureMap mappings (e.g.,
ADT^A04→maps/hl7v2-ADT-A04-Epic.fml) - vendor-quirk tolerations (Epic’s MSH-3 shortcode format, Meditech’s segment ordering, Athena’s RTF-prefixed attachments)
- default channel security defaults
- positive / negative sample messages used as a regression-test suite
- optional Java code for resolvers and field extractors
A profile can be 100% declarative — drop a .tgz into /plugins/profiles/ and restart. See Vendor profiles for the manifest shape and authoring workflow.
2. MessageSink
By default, transformed FHIR Bundles are persisted to HAPI’s JPA storage. A MessageSink routes them ELSEWHERE — in addition to, or instead of, HAPI:
- Kafka topics keyed by patient or facility
- S3 / GCS for cold-storage replay
- An outbound HL7 v2 MLLP socket
- A custom REST endpoint at a downstream service
Multiple sinks can be registered and chained.
3. SubscriptionFilter
FHIR Subscription.criteria are an MDB matcher by default. A programmatic filter is a small piece of Kotlin/Java that returns true/false given a resource. Use cases:
- ML-based prioritization (only fire for resources flagged “interesting”)
- Time-of-day or facility-of-day routing
- Cross-resource rules (“fire this
Observationsubscription only if the relatedEncounteris admitted”)
4. ObservabilityEnricher
Add labels to Prometheus metrics or fields to JSON logs at runtime. Use cases: tying the engine’s metrics to a customer’s billing system, stamping a tenant’s compliance fields, adding correlation IDs from upstream callers.
5. AuditEventEnricher
A close cousin of ObservabilityEnricher, but scoped to FHIR AuditEvent resources emitted by the auth/audit interceptor. Use cases: stamping the originating practitioner (PV1-7 → AuditEvent.agent.who), copying facility codes (MSH-4 → AuditEvent.agent.location), and any customer-specific compliance attributes.
6. AuthorizationDecider
Replace or supplement the OIDC role check on the operator UI and admin API. Plugs the engine into a customer’s existing IAM:
- LDAP / AD group membership
- Azure AD app-role mapping
- Okta SSO Service-Principal-Names
- Custom attribute-based access control (ABAC)
7. StorageBackend
The FHIR storage layer. The default is HAPI’s JPA. A StorageBackend plugin can swap in:
- Elasticsearch for search-heavy workloads
- A sharded backend for very-large multi-tenant deployments
- A read-replica fan-out for global geo-redundancy
8. UiExtension
A registered UI fragment in the Next.js operator console. Five fragment kinds are supported:
NavLink— adds an item to the left navPageRoute— adds a top-level routePanelWidget— embeds in a dashboard slotRowAction— adds an action to a list-row context menuDetailTab— adds a tab to a detail page
Contract: TypeScript types in @bzonfhir/ui-extensions on npm. The code is baked into the published image, not loaded from a directory at runtime — the registry gates each extension by entitlement, so commercial UI features simply start working when the license is in place. Community UI extensions are deferred until the SPI matures (~v1.6).
The contract version
Every SPI interface lives in the plugins-spi Maven artifact. The artifact’s semver is the contract: a plugin authored against plugins-spi:1.2.0 runs on any engine that publishes the same major (1.x). Breaking changes bump the major, and a deprecation window is documented on the engine release notes.
plugins-spi is Apache 2.0 and depends only on Java standard library + Kotlin stdlib + Jackson — nothing engine-internal leaks across the boundary.
Authoring a plugin
The fastest path is to clone the subscription-service-plugins-community template repo, which ships:
- A skeleton
MessageSink+SubscriptionFilter+UiExtension - A
ServiceLoaderregistration file - Maven publishing config to GitHub Packages or Maven Central
- An integration test harness that boots the engine in-process and exercises your plugin against synthetic FHIR Bundles
Where the canonical reference lives
This page is a summary. For the full interface signatures, lifecycle diagrams, and reference implementations, see the engine repo:
plugins-spi/README.md— full SPI referenceplugins-builtin/— the built-in plugins (use as canonical examples)- Vendor profiles — for the most common case, an HL7 vendor profile
Related
- Architecture — where each surface plugs in
- Vendor profiles — declarative authoring path
- Security — trust boundaries for third-party JARs