Skip to content

Event Delivery

logi delivers state changes related to user identity to the RP via three paths. The design ensures that even if one path fails, the RP eventually converges to the correct state through another path.

Why three tiers

If you rely on webhooks alone, an RP falls permanently into a stale state in modes like these:

  • The RP server is briefly down at webhook delivery time and exhausts its retry budget.
  • During HMAC signing key rotation, the RP knows only the old key.
  • A network partition blocks only the webhook (token rotation still gets through).
  • A bug in the RP-side webhook receiver returns 200 OK without actually processing the event.

The 3-tier design guarantees reliable convergence against such failures.

Tier 1 — Transactional Outbox + Webhook

A row is inserted into the webhook_outbox table inside the transaction where the event occurs (for example, MergeService's merge transaction). This is the outbox pattern adopted as a standard by Stripe / Shopify / Linear and others:

  • INSERT inside the transaction — it is fundamentally impossible for an event to occur but fail to be sent. The identity change in the logi DB and the outbox row are atomic.
  • Signing time = INSERT time — the raw_payload column stores the string serialized as canonical JSON (an RFC 8785 subset) as-is, with an HMAC-SHA256 signature baked in alongside it. The dispatcher then sends it byte-for-byte, which blocks the failure mode where the signature breaks due to key-order differences on re-serialization.
  • Unique idempotency keywebhook_outbox(oauth_application_id, event_id) is unique. Even if the dispatcher sends the same row twice, the RP can dedupe.

The dispatcher (a background job) polls the outbox with FOR UPDATE SKIP LOCKED and re-sends with exponential backoff. After a certain number of failures, the row is marked with dlq_at and can be replayed manually from the admin UI (/api/v1/admin/webhook_outbox).

Webhook payload example — user.merged

json
{
  "event_id":  "evt_01HE3...",
  "event_type": "user.merged",
  "occurred_at": "2026-05-11T12:34:56Z",
  "data": {
    "survivor_canonical_sub":     "9182",
    "merged_sub":                 "7341",
    "merged_canonical_sub_before": "7341",
    "merged_via":                 "t3_otp",
    "triggered_at":               "2026-05-11T12:34:55Z",
    "source_event_id":            "trg_..."
  }
}

Signature headers:

X-Logi-Signature:  sha256=<hex-hmac>
X-Logi-Key-Id:     whk_01HE3...
X-Logi-Timestamp:  1715432095
X-Logi-Event-Id:   evt_01HE3...

The RP must check that the value is within the (timestamp ± 5min) range, dedupe by X-Logi-Event-Id, and then verify the HMAC. For the verification procedure, see Webhook HMAC signature verification.

Tier 2 — Polling API

The RP periodically calls GET /api/v1/events?since=<cursor> to receive events after its own cursor. This is:

  • A catch-up path for an RP that missed webhooks — events dropped due to a receiver bug or exhausted retries remain visible with the polling cursor not advancing.
  • Drift recovery — when an RP suspects its own state is stale, it can intentionally rewind the cursor to reprocess.

For the call format and the meaning of the cursor, see Polling Events API.

⚠️ Migration-order invariant: the RP-side polling reconciler and webhook receiver must be active before logi's first merge trigger (T1) is turned on in production. Otherwise the webhook fires but the reconciler is a no-op → the polling cursor advances past that event → permanent loss. This is the first item on the RP integration checklist.

Tier 3 — Login-time Fallback

On the next logi token rotation (a refresh or a new SSO), the RP's BearerAuthentication (or equivalent concern) looks at userinfo's canonical_sub / linked_subs, compares them against its own DB's logi_identity_links, and corrects any difference immediately.

This is the last safety net:

  • An RP that missed both the webhook and polling is corrected at that user's next sign-in anyway.
  • The downside is that "the RP-side state of a user who never signs in is never corrected" — which is why Tier 1/2 take priority and Tier 3 is a fallback.

RP integration checklist

Items to confirm before the logi merge trigger is turned on in production:

  • [ ] webhook_url is registered in the RP console and the HMAC signing key is stored securely (the registration surface is a single URL column per application)
  • [ ] The webhook receiver implements event_id dedupe + timestamp verification + HMAC verification
  • [ ] The webhook receiver INSERTs a logi_identity_links row into its own DB after processing user.merged
  • [ ] The polling reconciler calls GET /api/v1/events?since=<cursor> on a per-minute basis
  • [ ] The login-time canonical resolver (the BearerAuthentication concern) is active
  • [ ] The enforce_canonical_resolution flag has been flipped to true

Following the deployment-order procedure in migration-public-clients sorts the items above out in order.

Identity가 제품의 신뢰를 만듭니다.