ADR 0016: Incremental sync cursors, webhook hints, and instance state
Status
Accepted
Context
Sprint 03 can authorize a mailbox and run checkpointed initial backfill. Sprint 04 Stream A needs ongoing sync that stays correct when Graph (or another adapter) delivers duplicate or out-of-order notifications. Provider callbacks must not become the system of record, and mailbox flags must not clone canonical content.
Decision
- Treat webhooks as authenticated hints. Persist a unique
(provider, provider_notification_id), enqueueINCREMENTAL_SYNC, ack. Never run classification or AI in the request. - Store the incremental cursor in existing
sync_checkpointswith scopeincremental-delta. Advance it only after the delta page is durably applied. - Guard instance upserts with
provider_versionso a stale change cannot overwrite newer mailbox state. - Renew push subscriptions before expiry. On expiry or renewal failure, set
authorization_status = DEGRADED, enable polling, and keep incremental sync running. - Apply read/folder/flag/tombstone only on
MessageInstance. Do not copy body or subject onto a secondMailMessage, and do not rewriteclassificationswhen the user moves a folder.
Alternatives
- Trust webhook payloads as the event log — rejected; notifications are incomplete and reorderable.
- Advance the cursor when the provider page is fetched — rejected; a crash after fetch would skip unprocessed items.
- Clone
MailMessageper folder — rejected; membership is instance state (05 CDM §7).
Consequences
Workers process INCREMENTAL_SYNC the same way they process SYNC_PAGE. Preview and production stay 503 on /api/webhooks/mail until WEBHOOK_SIGNING_SECRET and DATABASE_URL are set. Reconciliation (MAIL-28) and identity matching (Stream C) remain separate.
