Sync Contract
:::info Source
Sourced from services/progress-service/SYNC_CONTRACT.md in the documentation repo.
:::
1. Applicability
Statements and Attempts are replicable for offline. Offline devices:
- Write statements to local outbox.
- Read transcripts from local cache.
- Never update or delete statements (append-only).
2. Sync Registration
{
service: 'progress-service',
entityType: 'Statement',
conflictPolicy: 'append_only', // never conflicts — new ID each time
deltaProjector: 'statements_by_user',
pushHandler: 'IngestStatementBatch',
versionField: 'stored',
schemaRef: 'schemas://progress/statement/v1'
},
{
service: 'progress-service',
entityType: 'Attempt',
conflictPolicy: 'server_authoritative',
deltaProjector: 'attempts_by_user',
pushHandler: 'none (read-only client-side)',
versionField: 'ended_at',
schemaRef: 'schemas://progress/attempt/v1'
},
{
service: 'progress-service',
entityType: 'CompletionRecord',
conflictPolicy: 'server_authoritative',
deltaProjector: 'completions_by_user',
pushHandler: 'none',
versionField: 'completed_at',
schemaRef: 'schemas://progress/completion/v1'
}
3. Delta Format
Server → device delta payload:
{
"cursor": { "lamport": 123456, "scope": "progress:user:u_01H..." },
"upserts": {
"Attempt": [ { "id": "...", "state": "closed", "outcome": "passed", ... } ],
"CompletionRecord": [ { "id": "...", "attemptId": "...", "passed": true, ... } ]
},
"deletes": {
"Statement": [],
"Attempt": [] // per GDPR erasure
}
}
4. Conflict Resolution
- Statement:
append_only. Each statement has unique ULID; duplicates silently deduplicated onstatementId. - Attempt:
server_authoritative. Server computes outcome from statements. Device shows cached state; server version overrides on sync. - CompletionRecord:
server_authoritative.
5. Cursor Rules
- Cursor is
{lamport, scope}. Scope =progress:user:{userId}. - Device maintains cursor per (tenant, user, device, scope).
- Server responds with delta since cursor; new cursor in response.
- Cursor opaque to client; format evolution via
vfield.
6. LocalStore Schema (Client)
statements_outbox: (clientMutationId PK, tenantId, userId, statementId, payload, createdAt, pushedAt?)
attempts: (attemptId PK, tenantId, userId, enrollmentId, state, outcome, score, ...)
completions: (completionRecordId PK, attemptId, ...)
cursor: (tenantId, userId, deviceId, scope PK, lamport, updatedAt)
statements_outboxencrypted with per-tenant key (contains potentially PHI).- 7-day retention offline before force-sync prompt.
7. Push Flow
Client has N statements in outbox.
POST /sync/v1/push with batch:
{ mutations: [
{ clientMutationId, entityType: "Statement", op: "create", payload: <statement> }, ...
] }
Server validates + forwards to progress.ingestStatements().
Response includes applied/conflicted/rejected per clientMutationId.
On apply, server forwards to /xapi/statements pipeline internally.
8. Pull Flow
GET /sync/v1/pull?scope=progress:user:u_01H...&cursor=lamport:123456
Server returns delta (§3).
Client applies upserts/deletes transactionally in LocalStore.
Client advances cursor.
9. Background Sync Behavior
- Client syncs on: app foreground, reconnect, 15-min timer when online.
- Prioritizes revocations and license changes before statement pulls.
- Statements pushed first (learner's in-flight work) before pulls.
10. Offline Statement Expiry
- Statements in outbox retained 7 days. After 7 days offline, user prompted to sync.
- After 14 days, app enters read-only mode until sync.
- Never silently discarded.
11. Tamper Resistance
- Statements-outbox encrypted; integrity via HMAC per entry.
- Server verifies statement signature (if client signed with device key — M5+).
- Replay protection:
clientMutationIdunique;storedserver-side overwrites device-stored value.