Incremental sync, webhooks, and instance state
Duckbill™ wakes on provider hints, reads authoritative delta, and applies mailbox state to MessageInstance only. This is Stream A of Sprint 04 (MAIL-24, MAIL-26).
Rules
- Notifications are hints. They are not the canonical event log.
- Webhook ingress validates, persists, queues
INCREMENTAL_SYNC, and acks. It does not classify or call AI. runIncrementalSyncreadsadapter.getChanges(cursor)and advancessync_checkpointsscopeincremental-deltaonly after the page is durably processed.- A stale
provider_versioncannot overwrite a newer instance. - Expired or failed push subscriptions mark the account
DEGRADED, enable polling, and keep the mailbox syncable. - Read, flag, folder, trash/spam, and tombstone live on
MessageInstance. Deleting one instance does not deleteMailMessageand does not rewrite classification.
HTTP
GET|POST /api/webhooks/mail?validationToken=...echoes the Graph validation challenge astext/plain.POST /api/webhooks/mailrequiresWEBHOOK_SIGNING_SECRET(16+ characters) andDATABASE_URL. Sign the raw body with HMAC-SHA256 inx-webhook-signature, or send a matching GraphclientState.
Do not put the signing secret in git, Jira, or logs.
Code
| Module | Role |
|---|---|
packages/application/src/webhook-ingress.ts | Validate → persist → queue → ack |
packages/application/src/incremental-sync.ts | Delta retrieval and cursor advance |
packages/application/src/subscriptions.ts | Renewal and polling fallback |
packages/application/src/instance-state.ts | Instance-only mailbox state |
database/migrations/0013_incremental_sync.sql | Notification and subscription tables |
Reconciliation and MailMessage identity matching stay on Streams B and C.
