Data flow: Resync
sequenceDiagram
autonumber
participant Admin as MSP admin
participant Next as Next.js (Vercel)
participant SbDB as Supabase Postgres
participant Edge as Edge Function graph-policies-read
participant Graph as Microsoft Graph
participant Vault as Supabase Vault
Admin->>Next: Click "Resync now" on /tenants/[id]
Next->>SbDB: requireSession() → check msp_user + role
Next->>SbDB: select tenant where id = ? (RLS-scoped)
Next->>Edge: invoke graph-policies-read { tenant_id }
Edge->>Edge: verify JWT msp_id == tenant.msp_id
Edge->>SbDB: select graph_credential where tenant_id=? and kind='app_only'
Edge->>Vault: read_graph_credential(credential_id) → client_id+secret
Edge->>Graph: POST /token (client_credentials)
Graph-->>Edge: access_token
Edge->>Graph: GET /identity/conditionalAccess/policies
Graph-->>Edge: { value: [...] }
Edge-->>Next: { policies, snapshot_at, source }
Next->>SbDB: insert policy_snapshot (tenant_<id>) IF hash changed
Next->>Next: if hash changed, fire policy.changed_in_portal alert (dedupe)
Next->>Next: if source='import', also seed policy_intent
Next->>SbDB: update tenant set last_resynced_at = now()
Next->>Edge: invoke graph-licenses-read (non-fatal if it fails)
Next->>SbDB: insert audit_log
Next-->>Admin: revalidate page, show new snapshot id
Key points
- No raw Graph response stored. Only the curated
policy_snapshotrow. - Hash dedupe -
snapshotHash()is a deterministic SHA-256 over sorted-key canonical JSON. If the new snapshot's hash matches the previous one, no new row is written; the existing snapshot id is returned withunchanged: true. - Portal-edit drift - when the hash changes,
firePolicyChangedInPortalAlertIfNeededinserts apolicy.changed_in_portalalert (one per tenant per hour). There is no Graph change-notification subscription - Microsoft does not support CA policy webhooks. - License refresh is non-fatal. If license refresh fails, capability gating just stays stale - the snapshot itself is fine.
- Audit log entry records
policy_countand theunchangedflag so an admin can confirm the click actually did something.
Failure modes
| Stage | Failure | Recovery |
|---|---|---|
| Token acquisition | 401 from Microsoft | Credential rotation or consent revoked |
| Graph call | 502 with value field missing |
Microsoft 365 status check |
| Snapshot insert | Postgres unique constraint or schema not provisioned | provision_tenant_schema if missing |
| License refresh | 401 - missing Directory.Read.All |
Update app reg permissions + re-consent |