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:
| Entity | Sync Direction | Rationale |
|---|---|---|
Device (own device only) | server → device | Device must know its own offline certificate, trust status, expiry |
The following identity entities are NOT synced:
| Entity | Why Not |
|---|---|
User | User profile lives in tenant-service; identity-service stores only authentication-side projection |
Credential | Never leaves the server (contains hashes) |
Session | Session is the sync authentication layer itself; cannot be its own subject |
MFAFactor | Security-sensitive; never stored client-side beyond transient challenge |
APIKey | Server-side only |
ExternalIdentity | Server-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
| Entity | Policy |
|---|---|
Device (server-side fields: certificate, trust, revocation) | Server-authoritative. No client conflict possible. |
Device.lastSeenAt | LWW by timestamp; client hints accepted only if more recent. |
4. Revocation Propagation
When a device is revoked server-side:
- Identity-service emits
identity.session.revoked.v1for all sessions on that device. - Identity-service emits a device-revocation delta on next sync pull.
- Client receives delta; player unmounts any active offline bundle bound to this device.
- 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.