Skip to content
Browse documentation

OPRP 0.1.0-dev.11 · Draft documentation

Coordinated migration between patient record Hosts

The experimental 0.1.0-dev.4 profile moves a reference Patient/laboratory record between two participating Hosts while preserving clinical IDs, versions, retained evidence and operation receipts. The source accepts writes during initial copying, fences writes while capturing a final consistent archive, and permanently retires its authority before the destination activates. Signed discovery changes only after a verified activation receipt. A delayed queue obtains destination authorization and recovers old write outcomes without duplicates.

This is a coordinated live transfer, distinct from copying an offline backup. The executable profile handles one record/address and an empty destination. It does not claim production completion, general FHIR/attachments, incremental streaming, rollback-resistant custody, or external vendor interoperability.

Who authorizes a move

Export and transfer are different rights. oprp/record.export permits downloading the supported record. oprp/record.transfer is the reference management right for changing Host custody. A representative with export/read rights cannot initiate a move without scoped transfer-management authority. The source and destination independently authenticate the patient or their representative and validate that authority.

Trusted source integration calls source.migration.begin(...); trusted destination integration calls destination.migration.admit(...). These are local integration methods, not public unauthenticated endpoints. A Host can obtain the owner's decision through its own app, hosting agreement or other authenticated policy process. The protocol does not mandate a patient portal or login at each phase. Once admitted, automatic workers perform the encrypted handoff under that decision. Real owner authentication, enrollment and legal-evidence verification remain integration work.

The source verifies the current signed address binding and its own keys before preparing a plan. The destination verifies the signed plan, its own pinned keys and independently authenticated owner admission. Merely holding a valid Host credential does not permit importing someone else's record or activating a destination.

State and ordering

sequenceDiagram
    participant Owner
    participant Source
    participant Destination
    participant AddressProvider as Address provider
    participant App as Requester queue
    Owner->>Source: Authorize selected destination and address
    Source->>Destination: Signed plan
    Owner->>Destination: Admit the same record and plan
    Source->>Destination: Protected baseline
    Note over Source: Copying permits writes
    Source->>Source: Fence writes and capture final archive atomically
    Source->>Destination: Protected final archive
    Destination-->>Source: Signed ready receipt for exact archive
    Source->>Source: Persist release and retire source authority
    Source->>Destination: Signed release
    Destination->>Destination: Import and activate atomically
    Destination-->>Source: Saved signed activation receipt
    Source->>AddressProvider: Verified complete handoff
    AddressProvider->>AddressProvider: Compare generation and publish new binding
    App->>AddressProvider: Rediscover with remembered generation
    App->>Destination: Obtain new authorization and snapshot
    App->>Destination: Retry unchanged queued operation IDs
Phase Source custody Destination custody Write behavior
Initial copying Active Staged Source writes continue; destination serves no application record access.
Final capture Frozen Staged Source denies writes, including receipt recovery; destination remains inaccessible.
Released Moved Staged or ready Neither accepts writes until activation completes.
Activated Moved Active Only destination accepts writes under its own authorization.
Discovery published Moved Active New clients resolve destination; stale source calls fail.
Cancelled before release Active Aborted after signed cleanup Source can continue; no release exists to activate destination.

There may be a period with zero writable Hosts. Availability is sacrificed during a failed handoff rather than reopening the source after release. The guarantee applies to participating reference Hosts using intact durable state; it cannot stop a malicious Host, direct database changes or restoration of stale custody storage.

Signed control documents

Each control proof contains exactly payload, signature, credential, and status_evidence. It uses compact encoded JSON and ML-DSA-65 over ASCII OPRP-MIGRATION-0.1. plus the encoded payload. Credential/status evidence is evaluated at the signed creation time under independent trust policies, while known revocations and disabled issuers still apply. This is an OPRP experiment, not an inherited standard or independent timestamp.

The source-signed plan contains v, kind: plan, transfer_id, patient, address, source, destination, destination_endpoint, both Hosts' signing/encryption key IDs, from_generation, to_generation, created_at, and expires_at. Address is the canonical acct: URI. Destination endpoint requires HTTPS. Generations are safe positive integers and to_generation must equal from_generation + 1. The reference decision lives at most one day. The plan pins each Host's key pair; key rotation during a transfer needs separate recovery rather than silent substitution.

Other receipt payloads share v, kind, transfer_id, plan_digest, and created_at. Digests use base64url SHA-512 of the entire compact JSON encoded proof/archive object, including signature/evidence. Preserve the exact accepted objects rather than regenerating randomized signatures.

Receipt Signer Additional bindings
ready Destination archive_digest for the exact verified final archive.
release Source Same archive digest and ready_digest.
activation Destination Same archive digest and release_digest.
abort Source Plan digest; proves cancellation before any release.

Verification checks proof kind, source/destination signer and both key IDs, transfer ID, plan digest, receipt digest chain and time ordering. A complete handoff is {plan, ready, release, activation}. The release must have been created before plan expiry. A valid committed release does not cease to exist when that planning deadline later passes; activation and publication recovery can continue under current local authority and fresh transport credentials.

Baseline and final catch up

After both owner integrations accept the plan, source-to-destination migration_stage sends the signed full baseline inside the normal protected exchange. The destination verifies source identity, archive signature, record binding, profile, resources, receipts and historical evidence, then stores it as inaccessible staging data. It does not insert a writable Patient or return its content through ordinary application operations.

The source continues normal authorized writes during copying. freeze(transferId, now) acquires the same SQLite write transaction used by clinical mutations, records frozen custody and captures the final full archive in that transaction. A write that commits first is included; a write that acquires the lock afterward sees the fence. The mutation path checks custody after acquiring its lock, before deduplication or mutation. A second process sharing the source database sees the same durable fence. Failure to generate the final archive rolls back both the capture and the fence.

migration_stage_final transmits that saved archive. Final catch up currently resends a complete bounded archive rather than streaming deltas. The destination requires every baseline resource to remain present without version regression or same-version content change, and every baseline receipt to remain unchanged. Deletions are outside this resource profile. New writes and receipts accepted during copying must be present in the final source snapshot. Retained event/audit coverage follows the archive's explicit incomplete-history declaration.

Once accepted, the final archive and ready receipt are immutable. Identical final retries recover the saved ready receipt. A different final package fails. Staging archives still use the 512 KiB manifest and protected-body bounds; exceeding a bound fails explicitly. No content is truncated.

Release and destination activation

Source release(transferId, ready, now) verifies the destination receipt against the exact saved final archive and current source management authority. One transaction stores the signed release, marks source custody moved, revokes all source grants for that record, and closes its pending approval tasks. No timeout, failed HTTP call or process restart can automatically reopen that source. The source keeps its record copy; deletion is a separate decision.

migration_activate carries the release to the pre-admitted destination. The destination checks its saved final package and ready receipt, verifies the release, and rechecks local management authority. One transaction imports the archive, preserves operation receipts, starts destination feed events, records active custody and saves the signed activation receipt. A collision or other error rolls back the entire activation. Retrying an identical release recovers the receipt and creates no duplicate resources or events, even after restart.

The source can accept the activation receipt through complete(...) and assemble the public handoff evidence. Its source fence remains permanent regardless of whether that acknowledgment or discovery publication succeeds. There is no automatic failback after release. A later move back would need a newly authorized transfer; return into a retained occupied record is not implemented.

Discovery publication

ReferenceDirectory demonstrates an independently operated address provider with a persistent binding store. Its trusted enrollment integration admits a first record/Host binding; it refuses to overwrite an occupied address through enrollment. It publishes normal signed WebFinger metadata through ordinary DNS-domain HTTPS resolution. No Convey registry or account is required.

publish(handoff, now, optionalCurrentDestinationCredential) verifies all four proofs and performs a transaction comparing the existing patient, source Host and old generation before replacing the endpoint/credential/Host and incrementing the generation. Publication requires activation, so it cannot route new clients to an unactivated destination. Identical retries are idempotent across provider restart; conflicting or stale cutovers fail. A renewed credential with the same pinned destination keys may replace an expired transport credential without changing the handoff or generation.

A directory outage leaves the destination active and the source fenced. A cached old binding may still reach the source, but its record requests fail with record_moved. Clients remember the highest known generation and reject lower signed bindings. New clients without that history still have the previously documented discovery rollback limits. The prototype provides a local publication integration, not a public address-control registration/update API or domain-enrollment service. Multiple aliases/providers and cross-directory atomic publication remain work.

Queue and authorization continuity

client.followDiscovery(document, address, directoryTrust, transport, expectedPatient, minimumGeneration) verifies the current binding and record identity, switches transport, preserves the highest known generation and clears source tokens/refresh state when the Host/key/record context changes. It can follow a valid new binding even if the old Host credential has expired. It awaits the application-owned persistence hook before reporting success. Ordinary HTTP redirects or URLs in error messages never authorize this switch.

queue.followMigration(...) additionally updates the selected patient's durable route after SDK state has saved. Every queued attempt checks its patient route before transport. Moving one patient cannot silently send another patient's queued data to the new Host. A storage failure or mismatched route blocks sending. This adapter still uses one worker/client at a time; a multi-patient queue may pause when its next patient needs another Host route.

Source JWTs, refresh credentials, grants, policies and cursor secrets are not copied. The requester obtains a new bounded grant from destination policy, automatically where standing permission suffices or through that Host's conditional approval where needed. A destination snapshot supplies a new cursor; old source cursors cannot be replayed there.

The queue then reuses its original logical operation IDs and unchanged request bodies in insertion order. Imported receipts recover uncertain source outcomes before later writes run. Current destination permission is checked first. A version/content conflict still stops the queue; migration is not permission to overwrite another writer's updates. The adapter does not automatically revive already rejected authority or bypass unresolved entries. Legacy queues acquire a route pin from the explicitly supplied initial client; protect that association during upgrades.

Cancellation and recovery boundaries

Before release, trusted source abort(...) signs and persists cancellation, restores active custody and drops its final staging snapshot. migration_discard delivers that signed abort to the correct destination, which discards clinical staging bytes and retains the control decision. An expired plan does not automatically discard destination staging: the source may already have issued a valid committed release. Neither side guesses that outcome from a timeout.

Current representative authority is checked at source freeze/release and destination staging/activation. If withdrawn, the step fails. Trusted reauthorize(transferId, authenticatedApprover, side, now) can replace the local approving principal after a fresh authenticated decision for the same immutable plan. It cannot change destination, expiry, final content or committed custody. Source continuation still must meet the original planning deadline. The owner can cancel an uncommitted expired/blocked source plan; a committed source remains fenced.

Ordinary restart recovery uses the same databases and public/private key identities. Do not restore an old pre-release source database and treat it as current custody. Anti-rollback storage, distributed leases/fencing, disaster-recovery adjudication, public operator tooling and independent security review are production gates. The prototype demonstrates durable normal restart/retry, not safety against arbitrary stale snapshots, lost service keys or dishonest operators.

Runnable evidence

Run npm run migration for the two-Host verified HTTPS demo. It copies a baseline, loses a committed write response, performs final capture/release, loses an activation response, switches WebFinger generation, obtains new authorization and replays the queue without duplicate clinical data. It re-exports signed custody/history evidence and deletes temporary synthetic databases/certificates.

The migration tests cover those steps plus Host/provider restart, same-database multi-process fencing, atomic activation failure, missing resources/receipts, wrong final digests, forged/wrong-signer proofs, missing owner admission, cancellation, stale discovery, expired plans, failed archive capture, route persistence and representative withdrawal/continuation. Control schema. See migration conformance and recovery for exact failure expectations and remaining release gates.

Durable destination validation

An optional durable admission worker schedules every retained destination representation before ready proof issuance and rechecks admission during activation. Pending/rejected validation commits no partial record. After restart or expired approval, run the same destination worker and retry the control step; source release does not bypass validation or remove its custody fence.

View source

Documentation follows the experimental reference implementation.