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.