Snapshots, ordered changes, and synchronization
The reference change feed supports Patient name updates, laboratory Observation creation and six versioned clinical types with deletion. It is a durable polling contract carried inside signed, encrypted exchange. Stable paged baselines and durable mirrors are implemented; optional notifications wake authorized polling. Field/security-label transitions invalidate grant-specific generations and force a fresh baseline. Attachments and portable cross-Host cursors remain outside this slice.
Begin with a consistent snapshot
Request sync_snapshot with patient, access_token, optional limit and page_cursor. The Host verifies current authority and returns status: ok, resources: [{resource, version}], a stable feed cursor, snapshot_id, total, has_more and continuation page_cursor. Only record types covered by the JWT's read scopes are included. A write-only token cannot obtain a snapshot or changes. A demographics-only grant receives no lab resources or lab counts.
The Host reads the resources and captures the event watermark in a single database transaction. A mutation accepted later has a sequence beyond that watermark and appears in later change polling. There is no interval between the snapshot and cursor that silently loses a committed mutation. The development seed method is bootstrap-only and is not a record protocol operation.
Stage all pages of the captured baseline, then persist the complete snapshot and feed cursor together in the requester's own transaction before presenting the snapshot as synchronized. Protect local copies according to the authority to cache them and the application's storage policy. A snapshot authorizes no additional disclosure beyond its read grant.
Poll and commit a page
Request changes with patient, access_token, cursor, and integer limit from 1 to 100. The response contains:
status: "ok"
events: [{event_id, kind: "created" | "updated" | "deleted", resource, version}]
visibility_epoch: opaque current grant visibility generation
cursor: opaque next cursor
has_more: whether another authorized event remains in this page window
Each mutation commits its resource, stable event ID, immutable event resource/version, write receipt and success audit together. An identical write retry returns the existing receipt without appending another event. A rejected or conflicted mutation creates no accepted-change event. The events preserve Host commit order; timestamps are not used to invent a global clinical order.
Within a page, only permitted patient/type events are selected. has_more refers to another authorized event, not any hidden event. Restricted labs and unrelated patients do not appear through resource bodies, event IDs, counts or pagination flags. When no further authorized event remains in the captured window, the Host advances the encrypted cursor past the window so hidden entries do not create an endless polling loop.
Clients must tolerate repeated pages. Retrying an earlier cursor can return the same stable event IDs; delivery is not an exactly-once processing guarantee. Apply each page and its next cursor atomically. If that local transaction fails, retain the prior cursor and retry. Continue while has_more is true, then poll again on the application's chosen schedule. No recurring automation or outbound worker is created by this API.
Cursor privacy and binding
The cursor contains internal Host position and authorization context, encrypted and authenticated with AES-256-GCM under a persistent Host-internal random key. It is not a JWT, a public sequence number, or a client-signed request. The protected exchange separately supplies post-quantum identity authentication and payload confidentiality.
Its authenticated context binds protocol version, Host, persistent feed epoch, application subject/signing key, patient, grant, visible resource types, internal position, and expiry. The context is wrapped and padded to 2,048 plaintext bytes before encryption, so even growth in a hidden counter's digit count does not change cursor length. Clients must store it opaquely. They cannot modify the position or decode a global event counter. Cursor values are randomized; repeated identical pages need not produce the same cursor string.
Cursor lifetime is at most one hour and no later than the authorization end in the reference Host. Refreshing a JWT inside the same unchanged grant preserves cursor usability. A different grant, altered visibility, wrong patient, wrong Host/epoch, or wrong caller key requires a new snapshot or an explicit future migration mechanism. Revocation prevents both snapshot and feed access even if an existing cursor or JWT has not expired.
The internal cursor key is persisted in the reference database, so ordinary restart preserves cursors. It requires the same at-rest and recovery care as other Host service secrets. Key rotation, distributed services, feed migration and corrupt-state recovery remain operational work.
Retention and resynchronization
retainAfter(sequence) is a trusted local Host administration method, not a client operation. It prunes event bodies and records which patient/type histories lost entries. A client's cursor expires due to a retention gap only when its own visible history was removed. Pruning hidden lab or other-patient events does not invalidate an otherwise complete demographics feed or reveal those changes through a new gap error.
If an authorized visible event is no longer retained, or the cursor's time expires, return cursor_expired. The client obtains a fresh scoped snapshot and atomically replaces the corresponding local view and cursor. A context mismatch returns cursor_context_changed. Clients must not treat either condition as an empty change page or blindly advance their state.
Search/history/baseline results now use immutable bounded sessions. Continue their opaque page_cursor with the same limit; only the baseline feed cursor is used for subsequent changes. Sessions expire after ten minutes or grant end. See pagination and mirrors for byte budgets, quotas, current-authority checks and recovery. Streaming larger archives and attachments remain work.
Executable example
After the Host grants the needed read scopes, use the bundled mirror for staging and atomic cursor application:
const mirror = new DurableMirror('mirror.sqlite', client, patient);
try {
await mirror.sync(50);
const visible = mirror.resources();
} finally { mirror.close(); }
The mirror guide shows imports, Python/.NET equivalents, restart, stale access and at-rest limits. The outbound durable queue remains separate. Receiving an event does not complete queued clinical dependencies or a human workflow.
Verification and remaining scope
Change-feed tests verify snapshot watermarks, immutable event versions, pagination, repeated pages, absence of duplicate events after repeated writes, patient/category filtering, opaque cursors, hidden-event retention, tampering, grant changes, gaps and fresh snapshots, revocation, restart, and token refresh.
The design supplies the event/receipt basis for backups, two-Host transfer and future webhooks. Outstanding work includes label policy/obligation profiles, general resource/profile enforcement, provenance, dependency graphs, portable cursors, receipt retention, multi-process fault injection and complete audit coverage. FHIR R4 history is a standards reference for future history mapping; this experimental change feed is not a claim to implement that interaction.
Expanded clinical changes
Version 0.1.0-dev.9 includes the six admitted clinical types in scoped snapshots/events. Their accepted deletions append kind: deleted, version and {resourceType,id} only. Consumers remove that current resource from their mirror and persist the cursor atomically; no clinical fields accompany the marker. Histories remain separately accessible under the declared type read permission. Stable paged snapshots and durable SDK mirrors are implemented. Field/security-label changes invalidate old feed/page cursors; SDK mirrors clear and rebuild under the current epoch. See visibility.