Skip to main content

Sync Contract

:::info Source Sourced from services/identity-service/SYNC_CONTRACT.md in the documentation repo. :::

Companion: sync-service · 05 API Design §17 · DOMAIN_MODEL

Identity-service participates minimally in offline sync. Authentication is inherently online-first: users must have a valid session to access tenant resources. However, device binding is the identity entity that must be synced to support offline bundle encryption and offline token validation.

1. Scope

Only one identity entity is synced to client devices:

EntitySync DirectionRationale
Device (own device only)server → deviceDevice must know its own offline certificate, trust status, expiry

The following identity entities are NOT synced:

EntityWhy Not
UserUser profile lives in tenant-service; identity-service stores only authentication-side projection
CredentialNever leaves the server (contains hashes)
SessionSession is the sync authentication layer itself; cannot be its own subject
MFAFactorSecurity-sensitive; never stored client-side beyond transient challenge
APIKeyServer-side only
ExternalIdentityServer-side only; client sees only the link status via profile

2. Cursor Rules

Identity syncs are device-scoped (one cursor per device). Identity syncs are always server-authoritative — clients cannot push changes to identity entities.

2.1 Cursor Shape

interface IdentitySyncCursor {
lamport: number; // monotonically increasing
deviceId: DeviceId;
lastDeviceVersion: number; // version of the Device aggregate
scope: 'identity';
}

2.2 Pull Flow

Client calls GET /api/v1/sync/pull?since={lamport}&scope=identity (handled by sync-service, which fans out to identity-service).

Sync-service asks identity-service: "What changed for this device since cursor X?"

Identity-service responds with a delta:

interface IdentitySyncDelta {
entities: {
type: 'Device';
id: DeviceId;
version: number;
data: {
id: DeviceId;
userId: UserId;
fingerprint: string;
trusted: boolean;
trustedAt?: ISODate;
offlineCertificate?: {
cert: string;
expiresAt: ISODate;
issuedAt: ISODate;
kid: string;
};
revoked: boolean;
revokedAt?: ISODate;
};
}[];
deletes: { type: 'Device'; id: DeviceId }[];
cursor: IdentitySyncCursor;
hasMore: boolean;
}

2.3 Push Flow

Identity entities are server-authoritative. Client pushes targeting Device with op != read are rejected:

409 sync.mutation.rejected
{
"code": "sync.mutation.rejected",
"reason": "server_authoritative",
"serverState": { /* current Device aggregate */ }
}

Exception: Client may push lastSeenAt as a hint (pseudo-mutation):

{
"clientMutationId": "9a3f...",
"service": "identity",
"entityType": "Device",
"entityId": "dev_01HN...",
"op": "touch",
"payload": { "lastSeenAt": "2026-04-15T10:00:00Z" },
"occurredAt": "2026-04-15T10:00:00Z"
}

The server accepts this as a liveness ping and updates devices.last_seen_at. No conflict possible (LWW by timestamp).

3. Conflict Policy

EntityPolicy
Device (server-side fields: certificate, trust, revocation)Server-authoritative. No client conflict possible.
Device.lastSeenAtLWW by timestamp; client hints accepted only if more recent.

4. Revocation Propagation

When a device is revoked server-side:

  1. Identity-service emits identity.session.revoked.v1 for all sessions on that device.
  2. Identity-service emits a device-revocation delta on next sync pull.
  3. Client receives delta; player unmounts any active offline bundle bound to this device.
  4. Sync-service refuses further sync on revoked device.

Offline-only devices: Revocation takes effect on next online contact. Until then, the device continues to function with its already-downloaded bundles, limited by:

  • Bundle's own license envelope expiry (typically ≤ 90 days)
  • Offline certificate expiry (≤ 90 days)

5. Tamper Evidence

Device deltas are signed by identity-service when they cross to the client:

X-Sync-Signature: eddsa.ed25519.kid=idsvc-2026-01.sig=...

Client verifies signature against JWKS-cached public key. Invalid signature → delta discarded, event emitted: sync.tamper_detected.v1.

6. Scope Boundary

Identity-service exposes exactly one sync endpoint (internal, called by sync-service):

POST /internal/sync/identity/delta
Authorization: mTLS service cert

Body:
{
"deviceId": "dev_01HN...",
"since": { "lamport": 1024 }
}

Response:
{
"entities": [...],
"deletes": [...],
"cursor": { "lamport": 1055, "deviceId": "dev_01HN...", "lastDeviceVersion": 7, "scope": "identity" },
"hasMore": false
}

This endpoint is not exposed to clients; sync-service is the only caller.

7. Offline Authentication

Clients holding a valid offline certificate can still play offline bundles — the bundle itself is decrypted using the device public key (see content-service sync contract). Identity-service plays no runtime role offline beyond what the certificate encodes.

Offline session tokens: NOT issued. All API calls require online session. Offline player operates in a bundle-only mode; no API calls to identity-service are made offline.

8. Why This Design

  • Minimal surface: Sync is complex; keeping identity-sync to one entity limits blast radius.
  • Server-authoritative: Device trust and revocation are security decisions that must not be overridable by the client.
  • Signature on delta: Prevents a compromised sync server from silently granting or revoking offline capability.
  • No offline tokens: Avoids the complexity of offline JWT validation with revocation; offline access is gated by bundle license, not by identity.