Skip to content

Event Delivery

logi 는 user identity 와 관련된 상태 변화를 세 가지 경로 로 RP 에게 전달합니다. 어떤 경로 하나가 실패해도 결국 다른 경로를 통해 RP 가 정정 상태에 수렴하도록 설계되어 있습니다.

Why three tiers

웹훅 단독에 의존하면 다음과 같은 모드에서 RP 가 영구적으로 stale 상태에 빠집니다:

  • RP 서버가 webhook delivery 시점에 잠시 죽어 있고 retry budget 을 다 썼을 때.
  • HMAC 서명 키 회전 중 RP 가 구 키만 알고 있을 때.
  • 네트워크 partition 으로 webhook 만 막혔을 때 (token 회전은 통과).
  • RP-side 의 webhook receiver 에 버그가 있어 event 를 처리하지 못한 채 200 OK 만 돌려줬을 때.

3-tier 는 이런 실패에 대해 reliable convergence 를 보장합니다.

Tier 1 — Transactional Outbox + Webhook

이벤트가 발생한 transaction (예: MergeService 의 통합 transaction) 안에서 webhook_outbox 테이블에 row 가 들어갑니다. 이는 Stripe / Shopify / Linear 등이 표준으로 채택한 outbox 패턴입니다:

  • transaction 안에서 INSERT — 이벤트가 발생했지만 전송에 실패하는 케이스가 원천적으로 불가능. logi DB 상의 identity 변경과 outbox row 가 atomic.
  • 서명 시점 = INSERT 시점raw_payload 컬럼에 canonical JSON (RFC 8785 subset) 으로 직렬화된 문자열이 그대로 저장되고 HMAC-SHA256 서명이 함께 박힙니다. 이후 dispatcher 는 byte-for-byte 그대로 전송, 재직렬화 시 키 순서 차이로 서명이 깨지는 사고를 차단.
  • idempotency 키 uniquewebhook_outbox(oauth_application_id, event_id) 가 unique. dispatcher 가 같은 row 를 두 번 보내도 RP 쪽에서 dedupe 가능.

dispatcher (background job) 는 FOR UPDATE SKIP LOCKED 로 outbox 를 polling 하고 exponential backoff 로 재전송합니다. 일정 횟수 이상 실패하면 dlq_at 으로 marking 되고 admin UI (/api/v1/admin/webhook_outbox) 에서 수동 replay 할 수 있습니다.

Webhook 페이로드 예시 — 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_..."
  }
}

서명 헤더:

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

RP 는 (timestamp ± 5min) 범위 안인지 확인하고 X-Logi-Event-Id 로 dedupe 한 뒤 HMAC 을 검증해야 합니다. 검증 절차는 Webhook HMAC 서명 검증 참고.

Tier 2 — Polling API

RP 는 GET /api/v1/events?since=<cursor> 를 주기적으로 호출해 자기 cursor 이후의 이벤트를 받습니다. 이는:

  • webhook 을 못 받았던 RP 의 catch-up 경로 — receiver 버그 또는 retry 소진으로 누락된 이벤트가 cursor 가 advance 하지 않은 상태로 계속 노출됨.
  • drift recovery — RP 가 자기 상태가 stale 하다고 의심할 때 cursor 를 의도적으로 뒤로 돌려 재처리 가능.

호출 형식과 cursor 의미는 Polling Events API 참고.

⚠️ 마이그레이션 순서 invariant: RP-side polling reconciler 와 webhook receiver 는 logi 의 첫 통합 트리거(T1) 가 production 에서 켜지기 이전 에 active 상태여야 합니다. 그렇지 않으면 webhook 은 발사되지만 reconciler 가 no-op → polling cursor 가 그 이벤트를 넘어 advance → 영구 손실. RP 통합 체크리스트의 첫 항목입니다.

Tier 3 — Login-time Fallback

다음 logi token 회전 (refresh 또는 새 SSO) 시 RP 의 BearerAuthentication (또는 동등 concern) 은 userinfo 의 canonical_sub / linked_subs 를 보고 자기 DB 의 logi_identity_links 와 비교, 차이가 있으면 즉시 보정합니다.

이는 마지막 안전망 입니다:

  • webhook 도 polling 도 모두 못 잡았던 RP 가 어쨌든 그 user 의 다음 로그인 때는 정정됨.
  • 단점은 "로그인 안 한 user 의 RP-side state 는 절대 보정되지 않음" — 그래서 Tier 1/2 가 우선이고 Tier 3 은 fallback.

RP 통합 체크리스트

production 에서 logi 통합 트리거가 켜지기 전에 확인해야 할 항목:

  • [ ] RP 콘솔에서 webhook_url 이 등록되었고 HMAC 서명 키를 안전하게 저장 (등록 surface 는 application 당 단일 URL 컬럼)
  • [ ] webhook receiver 가 event_id dedupe + timestamp 검증 + HMAC 검증 구현
  • [ ] webhook receiver 가 user.merged 처리 후 자기 DB 에 logi_identity_links row INSERT
  • [ ] polling reconciler 가 분 단위로 GET /api/v1/events?since=<cursor> 호출
  • [ ] login-time canonical resolver (BearerAuthentication concern) 가 active
  • [ ] enforce_canonical_resolution 플래그가 true 로 flip 완료

migration-public-clients 의 deployment-order 절차를 따르면 위 항목들이 순서대로 정리됩니다.

관련 문서

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