Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

Wundklar · sync protocol

The mutation envelope, the ledger with durable results and the per-kind resolution rules that keep offline work truthful.

Markdown

Bedside work is created offline. The engine on the device batches mutations with their dependencies, uploads their encrypted objects, pushes the batch and then pulls new rows through a cursor that never skips a late-committing transaction. The frozen protocol lives in packages/api/src/features/sync; the decision is ADR-0020.

Envelope

Every mutation on the wire is

{ id: UUIDv7, kind, entityId, patientId?, baseRev?,
  dependsOn: id[], occurredAt, deviceSeq, payload,
  objects?: [{ objectKey, variant, byteSize, sha256Cipher, wrappedDek, keyRef }] }

The handler for kind runs the mutation and writes the ledger row in one transaction. Each result is one of applied {rev, merged?}, duplicate, conflict {theirs}, rejected {code} (permanent, recorded in the ledger) or deferred {code} (transient: SYNC_DEPENDENCY_PENDING, OBJECT_PENDING, SYNC_KIND_UNAVAILABLE, timeouts).

A dependsOn id must be applied or duplicate in the ledger or earlier in the batch; a ledger rejection rejects the dependant permanently in a later batch too (SYNC_DEPENDENCY_REJECTED).

Per-kind behaviour

BehaviourExample kindsServer rule
insert-onlypatient.create, wound.create, photo.capture, assessment.record, visit.recordreplay via ledger; same id with a different payload hash → permanent SYNC_ID_CONFLICT
field mergepatient.update, patient.contactdisjoint fields merge; the same field changed concurrently → server order wins and the previous value is kept in the trail
conflictingplan.propose, reminder.update (due date)stale baseRev / baseVersion → conflict { theirs } and nothing written
terminal-monotonicwound.close, reminder.update (status)terminal state wins; later non-terminal changes are ignored

Clinical records never update in place (ADR-0028): a correction is a new row with supersedes_id and a reason.

Staged objects

Encrypted bytes are uploaded first (objects.stage returns a presigned PUT with the SHA-256 of the ciphertext). The referencing mutation's handler calls attachStagedObjects inside its transaction and checks the same org and device, the size and the checksum; missing → OBJECT_PENDING, mismatch → OBJECT_INVALID (ADR-0025). photo.capture carries only the metadata and applies at once; photo.attach follows when the upload is done.

Pull and snapshot

sync.pull paginates through (txid xid8, id) strictly below pg_snapshot_xmin(pg_current_snapshot()) filtered by the patient scope rule and the device's device_scope; a new patient on the scope triggers scopeAdded so history older than the cursor arrives on the next snapshot, and a leaving patient produces scopeRemoved.

sync.snapshot runs one REPEATABLE READ transaction and returns patients, risks, contacts, wounds, the last 9 measurements, the last 2 assessments, the active plan, prescriptions, stock, open reminders, the last photo metadata, the day's schedule, the policy and the current access state.

Authorization

Every push, pull and snapshot runs through the clinical boundary (ADR-0031): session assurance, AVV, access state, device, role permission and patient scope. Lists redact identifying ciphertext (name_ct, address_ct, contact name and phone) for patients outside scope — hiding in the UI is not enforcement, because every enrolled device holds the organization data key.

Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.