Communication Service — Sync Contract
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 16 offline-first
1. Per-aggregate conflict policy
| Aggregate | Policy | Rationale |
|---|---|---|
MessageThread | server_authoritative | Thread participant list and escalation are centrally governed; clients never mutate metadata offline |
Message | append_only | Messages are immutable once sent; offline composition queues with clientMutationId then server assigns messageId + sentAt |
ReadReceipt | lww (last-write-wins) on (messageId, userId) | Reading the same message from two devices converges to latest read time |
NotificationIntent | server_authoritative | Intents are created server-side in response to events; clients never author |
DispatchRecord | append_only | DLR callbacks arrive in any order; each attempt becomes a new row |
VirtualSession | server_authoritative | State machine centrally controlled; clients suggest transitions (end, cancel) but server decides |
VirtualParticipant.admitState | server_authoritative | Only providers can admit; racing admits resolve server-side |
2. Offline client behavior
| Scenario | Client behavior |
|---|---|
| Compose message offline | Queue with clientMutationId; send when online; on reconnect server returns messageId |
| Mark read offline | Queue POST /read with readAt; server resolves by LWW |
| View cached thread | Stale-while-revalidate; show "offline - latest messages at {ts}" banner |
| Create virtual session offline | Not supported (server is authoritative and realtime provider is cloud-dependent) |
| Receive push while app offline | OS-level delivery; deep link opens thread once reconnected |
3. Conflict resolution examples
Two devices mark the same messages read
- Client A:
{ messageIds: [m1, m2], readAt: 10:00 } - Client B:
{ messageIds: [m2, m3], readAt: 10:05 } - Server: stores per
(messageId, userId);m2LWW keeps 10:05;m1andm3inserted.
Offline compose arrives after thread archived
- Client sends message with
clientMutationId; server responds 409THREAD_ARCHIVED; client shows error and offers to reopen thread.
4. Clocks & ordering
- All timestamps server-assigned on write; client-supplied
clientSentAtis informational only. - Messages ordered by
(sent_at, message_id)to disambiguate same-millisecond inserts.