Webhook Schema¶
webhookschema 는 외부에서 수신한 웹훅 이벤트를 영구 기록한다. 서명 검증 결과, idempotency 중복, 처리 결과를 모두 기록하여 보안 조사와 재처리에 활용한다.
Schema purpose¶
- PostgreSQL schema —
webhook - Corresponding domain —
engine/webhook/ - sqlc package —
db/queries/webhook/
단일 테이블 webhook.events. 수신 시점에 무조건 INSERT 하고 처리 후 UPDATE.
Tables¶
webhook.events¶
CREATE TABLE webhook.events (
id UUID PRIMARY KEY,
source TEXT NOT NULL,
external_event_id TEXT NOT NULL,
event_type TEXT,
signature TEXT,
signature_valid BOOLEAN NOT NULL,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ,
processing_result TEXT CHECK (processing_result IN ('success', 'failed', 'duplicate', 'rejected')),
processing_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT webhook_events_external_unique UNIQUE (source, external_event_id)
);
Columns
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID v7 | PK | 내부 ID |
source | TEXT | NOT NULL | toss, 등 |
external_event_id | TEXT | NOT NULL | 외부 시스템의 이벤트 ID |
event_type | TEXT | — | Toss 이벤트 타입 (파싱 실패 시 NULL) |
signature | TEXT | — | 수신한 HMAC 서명 |
signature_valid | BOOLEAN | NOT NULL | 검증 결과 |
payload | JSONB | NOT NULL | 원본 body |
received_at | TIMESTAMPTZ | NOT NULL | 수신 시각 |
processed_at | TIMESTAMPTZ | — | 처리 완료 시각 |
processing_result | TEXT | CHECK | 결과 상태 |
processing_error | TEXT | — | 실패 사유 |
created_at | TIMESTAMPTZ | NOT NULL |
Indexes
| Index | Columns | Purpose |
|---|---|---|
events_pkey | (id) | PK |
webhook_events_external_unique | (source, external_event_id) | 중복 방지 |
events_source_received_idx | (source, received_at DESC) | 최근 수신 이벤트 |
events_result_idx | (processing_result, received_at DESC) WHERE processing_result IS NOT NULL | 실패/중복 통계 |
events_invalid_signature_idx | (received_at DESC) WHERE signature_valid = FALSE | 보안 조사 |
Invariants
(source, external_event_id)UNIQUE — DB 레벨 중복 방지signature_valid = false이면processing_result = 'rejected'(애플리케이션 강제)processing_result = 'success'이면processed_at IS NOT NULLprocessing_result = 'duplicate'이면 외부 이벤트 ID 는 기존 레코드의 것과 같음 (애플리케이션)
Processing states¶
| State | Meaning |
|---|---|
| NULL (initial) | INSERT 직후, 처리 중 |
success | 도메인 처리 완료 |
failed | 도메인 처리 실패 (Toss 가 재전송함) |
duplicate | Redis idempotency 에서 중복으로 판정 |
rejected | 서명 검증 실패 |
Relationships¶
Webhook 은 다른 schema 와 FK 를 두지 않는다. 외부 이벤트 기록 용도이며, 도메인 처리는 다른 schema 의 entity 를 변경하지만 webhook 테이블 자체가 그들을 참조하지는 않음.
Query patterns¶
InsertPending¶
-- InsertWebhookEvent
INSERT INTO webhook.events (
id, source, external_event_id, event_type,
signature, signature_valid, payload, received_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (source, external_event_id) DO NOTHING;
MarkProcessed¶
-- MarkWebhookProcessed
UPDATE webhook.events
SET processing_result = $1,
processing_error = $2,
processed_at = NOW()
WHERE id = $3;
ListRecentBySource¶
-- ListRecentWebhookEvents
SELECT *
FROM webhook.events
WHERE source = $1
ORDER BY received_at DESC
LIMIT $2;
CountInvalidSignatures (보안 대시보드)¶
-- CountInvalidSignatures (for alerting)
SELECT COUNT(*)
FROM webhook.events
WHERE signature_valid = FALSE
AND received_at > NOW() - INTERVAL '1 hour';
Data retention¶
- 90일 보관 — cron 으로 90일 경과 이벤트 삭제
- 단
signature_valid = FALSE이벤트는 1년 보관 (보안 조사용) - 재처리 필요 시 Toss 가 재전송하거나 운영자가 수동으로 원본 payload 를 해당 도메인에 라우팅
Size estimation¶
- Toss 웹훅 payload 평균 1KB
- 월간 이벤트 (MVP): 수천 건
- 90일 보관 시 수백 MB — 부담 없음
Migration history¶
| Date | Change | Rationale |
|---|---|---|
| 2026-04-xx | 초기 스키마 | MVP |
See also¶
domain/webhook.md— Webhook 도메인domain/billing.md— 주 라우팅 대상adr/0013-payment-toss-billing.md