테마
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 키 unique —
webhook_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_iddedupe + timestamp 검증 + HMAC 검증 구현 - [ ] webhook receiver 가
user.merged처리 후 자기 DB 에logi_identity_linksrow 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 절차를 따르면 위 항목들이 순서대로 정리됩니다.