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
| Command | Trigger |
|---|---|
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:
lwwresolved automatically;server_authoritativeauto-kept-server;crdt_yjsauto-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).