Skip to main content

Application Logic

:::info Source Sourced from services/sync-service/APPLICATION_LOGIC.md in the documentation repo. :::

1. Application Services

  • PushService — receives client mutations; validates; routes to owning service; records outcome.
  • PullService — computes server delta since cursor; returns to client.
  • AckService — advances cursor on client acknowledgment.
  • ConflictService — records conflicts; supports resolution (manual + AI-assisted merge).
  • RegistrationService — manages entity registrations (at boot; driven by service configs).
  • DeltaProjectorService — consumes domain events; materializes deltas per scope.
  • ResyncService — handles full-resync when cursor is out of range.
  • DeviceHealthService — tracks last-seen, mutation backlog, sync frequency per device.

2. Commands

CommandTrigger
PushMutations(batch)Client POST /sync/v1/push
PullDelta(cursor, scope)Client POST /sync/v1/pull
AckCursor(newCursor)Client POST /sync/v1/ack
ResolveConflict(conflictId, resolution)Client or admin POST /sync/v1/resolve-conflict
RegisterEntityType(registration)Service boot
ProjectDelta(domainEvent)Domain event consumption
InitiateFullResync(deviceId, scope)Cursor too old

3. Queries

  • getSyncStatus(tenantId, userId, deviceId) — health + staleness + backlog.
  • listConflicts(tenantId, userId) — pending conflicts.
  • getRegistrations() — all registered entity types.
  • getCursor(tenantId, userId, deviceId, scope).

4. Sagas

  • GDPR Erasure Saga participant: delete all cursors, mutations, conflicts for erased user.
  • Device Unbinding: revoke sync access; purge mutations in-flight.

5. Policies

  • Mutation validation: device must be bound; tenant+user must match JWT; mutation size ≤ 10 MB.
  • Cursor staleness: if cursor.lamport < oldest retained delta, return sync.cursor.out_of_range (410); client must full-resync.
  • Conflict auto-resolution: lww resolved automatically; server_authoritative auto-kept-server; crdt_yjs auto-merged; only failures of CRDT merge escalate to manual.
  • Rate limit: 100 mutations per push batch; 60 pushes/min per device.
  • Prioritization: revocations + license changes first in delta; then mutations; then pull.

6. Use Case Flows

6.1 Push Flow

Client sends batch of LocalMutations via POST /sync/v1/push.


For each mutation:
1. Validate: device bound, tenant match, entityType registered.
2. Resolve conflict policy from registration.
3. Route to owning service's pushHandler (e.g., progress.IngestStatementBatch).
4. Owning service returns: applied | conflicted | rejected.
5. Record outcome.
6. If conflicted: create ConflictRecord; emit sync.mutation.conflicted.v1.


Return batch result: { results: [ { clientMutationId, status, error? } ] }

6.2 Pull Flow

Client sends POST /sync/v1/pull { scope, cursor }.


1. Validate cursor format + freshness.
2. If cursor too old → 410 sync.cursor.out_of_range.
3. Query deltaProjector for scope since cursor.lamport.
4. Return { upserts, deletes, newCursor }.
5. Client applies locally → acks.

6.3 Conflict Resolution

Client or admin: POST /sync/v1/resolve-conflict { conflictId, resolution: 'kept_server' | 'kept_client' | 'merged', mergedPayload? }


1. Validate conflict exists + pending.
2. If 'kept_client': forward client payload to owning service (re-push).
3. If 'kept_server': discard client payload; advance cursor.
4. If 'merged': forward merged payload to owning service.
5. Update ConflictRecord.resolution + resolvedBy + resolvedAt.
6. Emit sync.conflict.resolved.v1.

6.4 Full Resync

Client cursor out of range (410).


Client requests full-resync: POST /sync/v1/pull { scope, cursor: null (full) }.


Server returns full snapshot for scope (paginated).
Client replaces local state entirely.

6.5 Delta Projection (Server-Side)

Domain event consumed (e.g., enrollment.created.v1).


DeltaProjectorService:
1. Lookup SyncRegistration for entityType.
2. Compute delta entry (upsert or delete).
3. Store in deltas table with monotonic lamport per scope.
4. Emit sync.delta.projected.v1 (for monitoring).