Sync Contract
:::info Source
Sourced from services/assignment-service/SYNC_CONTRACT.md in the documentation repo.
:::
Companion: 03 sync-service · 01 Enterprise Architecture (Offline-first)
1. Offline Relevance
Assignment-service participates in offline-first flows in two distinct directions:
| Direction | Who caches? | Purpose |
|---|---|---|
| Learner-facing | Learner device | Show "upcoming / due / overdue" list without network |
| Admin-facing (S5) | Admin device | View compliance report snapshot offline; author assignments offline, sync on reconnect |
We produce read-only projections for learners. Admin authoring is full bidirectional (CRDT-less; last-writer-wins on draft-only assignments).
2. Projected Entities
2.1 LearnerWindowProjection
Per-user denormalized window record; the learner's slice.
interface LearnerWindowProjection {
windowId: string;
assignmentId: string;
assignmentTitle: I18nString;
courseId: string;
courseVersionId: string;
occurrenceStart: ISODate;
dueAt: ISODate;
graceUntil: ISODate;
state: 'open'|'in_progress'|'completed'|'overdue'|'closed_missed';
lastKnownAt: ISODate; // server snapshot time
version: number;
}
2.2 AssignmentDraftProjection (admin, S5)
Mirror of Assignment aggregate in draft state only. Locked via lease for offline edit.
3. Sync API (delta)
Implemented by sync-service as a thin façade in front of assignment-service's read projections. Protocol per 03 sync-service:
3.1 Pull
GET /sync/v1/assignments/windows?userId=<me>&since=<cursor>
Response:
{
"items": [
{ "op": "upsert", "entity": "LearnerWindowProjection", "data": { … } },
{ "op": "delete", "entity": "LearnerWindowProjection", "id": "win_…" }
],
"nextCursor": "op=…",
"serverTime": "…"
}
3.2 Push (admin only, drafts)
POST /sync/v1/assignments/drafts
{
"ops": [
{ "op": "upsert", "entity": "AssignmentDraftProjection", "version": 3, "data": {…} }
]
}
Conflict strategy for drafts: last-writer-wins on server with If-Match: <version> semantic. Loser's edits returned as conflicts[] for client-side reconciliation UI.
4. Change Feed
Sync-service consumes these events to update its sync log:
assignment.created.v1→ admin projection upsert (if draft) or nothing (until activated)assignment.activated.v1→ trigger downstream re-materialize per-user projectionsassignment.window.*→ upsert or deleteLearnerWindowProjectionrows
Mapping table:
| Source event | Sync op | Entity |
|---|---|---|
assignment.window.opened.v1 | upsert | LearnerWindowProjection |
assignment.window.in_progress.v1 | upsert | same |
assignment.window.overdue.v1 | upsert | same |
assignment.window.completed.v1 | upsert | same |
assignment.window.closed_missed.v1 | upsert | same |
assignment.archived.v1 | delete (per user) | same |
5. Offline Mutations (learner)
Learners do not mutate windows directly. All "progress" happens through progress-service; that service has its own sync contract. Assignment-service's projection is fully server-authoritative for learners.
6. Offline Mutations (admin, S5)
Admin mutation scope limited to:
- Create draft assignment
- Edit draft (any field)
- Delete draft
- Cannot activate / pause / archive offline — those require server-side validation against live tenant and catalog projections.
Sync strategy:
- Monotonic local ULID ids (
asn_local_01…) resolved at sync. versioncolumn advances on each offline edit.- Server merges with LWW and returns rewritten ids.
7. Security & Tenancy
Every sync request is bound to:
- User's tenant (
X-Tenant-Id+ JWT claim) - User's role (learner scope enforces
userId === me) - Device binding (sync-service vouches; we trust its
X-Sync-Device-Idwhen forwarded via internal service auth)
8. Conflict Taxonomy
| Conflict | Handling |
|---|---|
| Server-side window deleted while offline | Client reconciles — removes local row |
| Draft edited both places | LWW on server; conflicts[] with server version returned |
| Tenant user removed during offline period | Projection no longer served; client must purge locally on 403 response |
9. Bandwidth Budget
Projection row ~350 bytes gzipped. Typical learner cohort: 20–80 active windows. ≤ 30 KB per full cold sync.
10. Freshness SLO
- p95 from
window.opened.v1to learner projection visible via sync pull: < 10 s while online. - Admin compliance-report offline snapshot: generated daily at 04:00 tenant-local.
11. Interaction with Progress Sync
Learners complete training offline → progress-service buffers progress.completion.recorded.v1 → on reconnect publishes → assignment-service transitions window → learner sees updated state next sync. Assignment projection and Progress projection MUST reconcile independently (no cross-dependency in sync log).