Skip to content
Browse documentation

OPRP 0.1.0-dev.11 · Draft documentation

Subscriptions and encrypted notifications

OPRP subscriptions let a credentialed requester learn that authorized record changes are available without continuously polling. The Host sends a small signed, post-quantum encrypted hint to a verified callback. The requester retrieves clinical data through its existing scoped snapshot/change-feed endpoints. Subscriptions are optional: polling remains sufficient for synchronization.

This experimental profile is implemented in 0.1.0-dev.5 for Patient demographics and laboratory Observation changes. It reuses the existing OPRP envelope and Host-issued grants. It requires neither a patient portal session nor a separate patient approval for every notification. The Host's policy and standing or conditional permissions still decide access, as described in authorization.

Registration and activation

The requester first obtains a current read grant from the Host, chooses a stable subscription ID, and durably records that ID and its requested expiry at its callback receiver. It then sends an encrypted subscribe request to the Host. Subscription permission comes from the existing read scopes; registration cannot add access.

{"operation":"subscribe","patient":"synthetic-patient-001","access_token":"<protected JWT>","subscription_id":"hospital-subscription-001","callback":"https://requester.example/oprp/exchange","topics":["Patient"],"expires_at":1790874000}

expires_at is an absolute UTC Unix timestamp in seconds. It must be in the future and no later than the grant's approved end. The example timestamp is illustrative, not a current test value. The initial topics are Patient under patient/Patient.r and Observation under patient/Observation.rs?category=laboratory. Version 0.1.0-dev.9 also admits Condition, AllergyIntolerance, MedicationStatement, Encounter, Procedure and Immunization under their type read scopes, up to eight selected topics. See FHIR coverage. Observation notifications cover only the reference Host's laboratory profile; general Observation access is not implied. Empty, repeated, unknown or unauthorized topics fail.

The Host returns pending in the subscription's state. Its delivery worker sends a protected challenge containing the subscription ID, a random 32-byte challenge, the exact normalized callback URL and a challenge expiry no more than 300 seconds after registration. The callback must authenticate the pinned Host and find the subscription among its own expected registrations. It returns a signed, encrypted subscription_challenge_ack with the exact subscription ID, challenge and callback, bound to the incoming envelope's message ID. An HTTP success code alone does not establish callback control.

Only a matching reply from the requesting application's current signing and encryption keys activates the subscription. The Host then schedules an initial catch-up hint, covering changes accepted before or during callback validation. Challenge failures retry until the short challenge deadline; an expired challenge suspends the registration. Register a new ID to try again.

sequenceDiagram
    participant App as Requester
    participant Host as Patient Host
    participant Callback as Requester callback
    App->>Callback: Persist expected ID and expiry
    App->>Host: subscribe with current scoped JWT
    Host-->>App: Pending subscription
    Host->>Callback: Signed encrypted challenge
    Callback-->>Host: Signed encrypted challenge acknowledgment
    Host->>Host: Persist activation and catch-up work
    Host->>Callback: Encrypted changes-available hint
    Callback->>Callback: Commit receipt and durable pull task
    Callback-->>Host: Signed encrypted delivery acknowledgment
    App->>Host: Authorized snapshot or scoped change polling

Registration retries reuse the same ID and exact patient, grant, callback, topics, keys and expiry. Changed input under the same ID fails with subscription_id_conflict. A renewed credential for the same keys can replace the stored credential on an identical registration retry, without extending permissions or expiry. There is no automatic callback edit or grant rebinding. Cancel and register a new ID for those changes.

Callback destination policy

Control of a callback and permission to contact its network destination are separate checks. The default reference worker permits HTTPS callbacks on port 443 with no URL credentials, query string or fragment. Each attempt resolves DNS again, requires every returned address to pass a conservative public-address policy, and pins the connection to one checked address while TLS validates the original hostname. Redirects are never followed. Local, private, link-local, multicast, mapped IPv4 and selected reserved/transition addresses fail closed. Some special public ranges are conservatively excluded.

Requests and responses have size limits and a 10-second network timeout. Callback responses must use application/oprp+json. The worker rechecks authority after destination preparation and immediately before constructing and sending the envelope. No bearer token travels in the callback URL or notification body.

A Host can supply a privileged transport factory for a separately approved, tightly allowlisted private integration. The synthetic demo uses one exact ephemeral loopback endpoint and its explicit development CA. There is no requester-supplied “allow private network” switch. Real operators also need egress controls, resolver time limits and destination-policy maintenance. The prototype's address classifier is an initial conservative implementation, not a maintained registry of every special-use assignment.

Notification and acknowledgment contract

The decrypted notification contains only:

{"operation":"change_notification","subscription_id":"hospital-subscription-001","delivery_id":"<stable opaque ID>"}

Its acknowledgment contains:

{"status":"ok","operation":"notification_ack","subscription_id":"hospital-subscription-001","delivery_id":"<same stable opaque ID>"}

The message is signed by the Host and encrypted for the registered requester; the acknowledgment is signed by the requester and encrypted for the Host. Both use the existing experimental post-quantum wire profile. Each retry gets a fresh message envelope and reply binding while retaining its logical delivery ID. Neither clinical values, resource identifiers, patient IDs, JWTs nor feed positions appear inside the notification. A subscription ID is still linkable metadata to its authenticated receiver; network observers can see outer identities, sizes, destination and timing.

The hint means “check your authorized feed.” It does not establish an ordered clinical event stream or guarantee one notification per write. Multiple visible changes coalesce into one pending delivery. Writes for other patients or unrequested resource topics do not schedule hints. Feed polling determines the ordered changes and current visibility.

Durable delivery and receiver recovery

The change-feed append path applies the current grant's visibility projection and creates or advances notification work in the same transaction that accepts a visible event. The prior unconditional SQLite trigger is removed when the Host initializes. Grant-policy or visible label transitions also enqueue a minimal wake-up so clients can rebuild invalidated mirrors. A failed or duplicate write therefore creates no extra wake-up. The outbox keeps one row per subscription, with a stable delivery ID, internal revision, attempt count, next attempt time and short worker lease. Internal revisions and global feed positions are never sent to the callback.

The explicit host.subscriptions.deliver(clock, transportFactory?, limit?) worker claims due jobs with a 30-second database lease. Multiple Host instances sharing the intact database cannot claim the same unexpired lease. A process crash leaves work retryable after lease expiry. On network failure, bad acknowledgment or temporary trust freshness failure, the next attempt waits 2 seconds, then exponentially longer delays up to 300 seconds. Retries stop when authority or subscription lifetime ends. This first scheduler has no jitter or per-destination circuit breaker.

Delivery is at least once. The reference receiver commits the delivery receipt and a durable pull task together before returning acknowledgment. A repeated delivery produces a fresh acknowledgment without duplicating the pull task. If another change arrives while the first delivery is in flight, the Host preserves the work and assigns a new delivery ID after acknowledgment. If another notification arrives while the application is processing a pull task, its inbox revision increases; completion of the old task cannot erase the newer task. Completed inbox rows retain their monotonic revision counter, so repeated old completion calls cannot erase later work after the inbox was briefly empty.

After receiving a hint, applications resume their last durably stored scoped cursor and apply feed pages idempotently. Commit the applied data and next cursor before marking the matching inbox revision complete. Drain has_more pages. If the cursor expired because of time or retention, obtain and durably apply a fresh scoped snapshot. The callback inbox survives restart even after the Host has accepted acknowledgment. Polling independently on reconnect remains useful; a webhook never replaces feed recovery.

Permission and lifecycle rules

Every challenge, notification and retry checks the stored credential against fresh authority status, current requester key pins, the live grant, the approved period, requested topics, representative authority and the record's current Host custody. Subscription delivery uses the continuing grant, not a cached JWT that must be refreshed every five minutes. Short JWT expiry alone therefore does not interrupt an otherwise valid subscription.

Change Reference behavior
Grant revoked or approved period ends Suspend and remove pending delivery work before callback contact.
Subscription expires Suspend and remove work; no automatic renewal.
Parent/representative authority withdrawn Suspend; a new current grant and registration are required.
Requester rotates signing/encryption keys Suspend old-key subscriptions; register a callback using the new keys.
Credential or issuer explicitly revoked/disabled Suspend; do not send to the old credential.
Status unavailable/stale or same-key credential expired Pause with retry; refresh status or renew the stored credential without extending authority.
Source Host releases a migrated record Suspend source notifications; no subscriptions are imported into the destination.
Destination activation Request fresh destination access, use a new scoped snapshot/cursor and register anew.
Unsubscribe Cancel, discard pending delivery work and keep the ID terminal.

Revocation is checked at the send decision. It cannot retract a notification already sent or bytes buffered in transit. The receiver's later data pull independently checks current access, so an earlier wake-up conveys no new permission. The worker also sweeps quiet subscriptions for permanent withdrawal when it runs. A Host that does not run its worker emits no notifications; polling still works.

subscription_status and unsubscribe requests contain operation, patient, access_token and subscription_id. They require the same application keys and current original grant. Returned views include status: "ok", subscription_id, state, expires_at and topics. States are pending, active, suspended and cancelled. A suspended or cancelled ID stays terminal; an identical registration retry reports its state and cannot reactivate it.

Integration example

The reference receiver lives in notification-receiver.ts; the worker and lifecycle live in subscriptions.ts. The TypeScript SDK exposes subscribe, subscriptionStatus and unsubscribe. Construct a receiver with a dedicated durable database, the application identity/credential, a pinned Host credential, caller-owned trust store and the exact callback URL. The database pins both identities' keys and the endpoint across restarts; changing Host or application keys requires a separately admitted receiver context.

const id = 'hospital-subscription-001';
const until = client.clock() + 900;
receiver.expect(id, until); // Persist before sending registration.
await client.subscribe(patient, callback, ['Patient'], until, id);
// Host operator runs its worker; callback HTTP handler calls receiver.handle(envelope, now).
// Application resumes its stored cursor for each receiver.pending() task.
// Complete only after the mirror and cursor commit:
receiver.complete(task.subscription_id, task.revision);

The receiver does not perform clinical pulls or external side effects inside its acknowledgment transaction. Its durable inbox is the integration boundary; a production mirror/worker still needs to own atomic data/cursor updates. Close receiver databases and caller-owned trust stores independently.

Verification and remaining work

Run npm run subscriptions for two verified synthetic HTTPS endpoints. The demo exercises pending activation, signed encrypted challenge, loss of a processed delivery acknowledgment, receiver restart, duplicate-safe acknowledgment, authorized feed retrieval, retention-gap snapshot recovery and revocation before transport. The conformance guide records fourteen focused cases, including actual source release during migration and two Host instances sharing delivery leases.

This step supplies a working reference slice, not a production notification service. Remaining work includes general FHIR subscription topics and deletion/visibility markers, scalable sweeps, jitter/backpressure/circuit breaking, resolver time limits, receipt and audit retention, independent receiver conformance, Host-key rollover, secure storage adapters and a client mirror. The callback composition inherits the experimental envelope's lack of forward secrecy and pending independent security review. Documentation is saved locally for the website section; it has not been published.

Next: tighten profile/serialization conformance, expand handling-obligation policy and strengthen production callback operations. The first expanded clinical slice and locally packaged SDKs are now documented in FHIR coverage and SDKs.

Visibility profile update

The Host advertises visibility_profile: "oprp-visibility-1". Visibility and cache invalidation defines trusted grant policy, exact security labels, SUBSETTED field projections, write/export protection and required baseline/change visibility_epoch. Changed visibility invalidates prior cursors and clears SDK mirrors before a new baseline; disconnected clients cannot learn unknown permission changes. Negative conformance records authenticated rejection tests. Host policy and approval delivery remain local integration choices.

View source

Documentation follows the experimental reference implementation.