Files
2nd/10_Wiki/Topics/Backend/WebHooks_and_Notifications.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

182 lines
5.7 KiB
Markdown

---
id: wiki-2026-0508-webhooks-and-notifications
title: WebHooks and Notifications
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Webhooks, Event Callbacks, Push Notifications]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [webhooks, event-driven, http, async, integration]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
language: TypeScript / Python
framework: Hono / FastAPI / Svix
---
# WebHooks and Notifications
## 매 한 줄
> **"매 reverse-API: 매 server 가 client 의 HTTP endpoint 로 event 를 push 하는 매 async integration pattern"**. 2007 GitHub 가 popularize, 2026 Stripe/Shopify/Slack 의 표준. 매 polling 대비 latency ↓ 99%, 매 challenge: delivery guarantee + signature verification + replay protection.
## 매 핵심
### 매 Webhook anatomy
- `POST https://your-app/hooks/stripe` with JSON body + headers (`Stripe-Signature`, `Webhook-Id`, `Webhook-Timestamp`).
- 매 receiver 의 2xx 응답 < 매 5s — 매 그렇지 않으면 sender 가 retry.
### 매 Delivery guarantees
- **At-least-once**: 매 표준. 매 idempotency key 필수.
- 매 retry: exponential backoff (1m, 5m, 30m, 2h, ...) up to 매 24-72h.
- 매 dead-letter queue + manual replay UI.
### 매 Security
1. HMAC signature (`HMAC-SHA256(secret, timestamp + body)`).
2. Timestamp tolerance (±5 min) → replay 방어.
3. HTTPS only, IP allowlist (optional).
4. 매 secret rotation 의 지원.
### 매 응용
1. Payment events (Stripe, Toss).
2. SCM events (GitHub push, PR).
3. Chat platform commands (Slack, Discord).
4. 매 SaaS integration hub (Zapier, n8n).
## 💻 패턴
### Receiver (Hono + signature verify)
```typescript
import { Hono } from 'hono';
import { createHmac, timingSafeEqual } from 'crypto';
const app = new Hono();
app.post('/hooks/stripe', async (c) => {
const sig = c.req.header('stripe-signature')!;
const body = await c.req.text();
const [t, v1] = sig.split(',').map(p => p.split('=')[1]);
if (Math.abs(Date.now()/1000 - +t) > 300) return c.text('stale', 400);
const expected = createHmac('sha256', process.env.STRIPE_SECRET!)
.update(`${t}.${body}`).digest('hex');
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1)))
return c.text('bad sig', 401);
const event = JSON.parse(body);
await enqueue(event); // 매 fast 200, async process
return c.text('ok', 200);
});
```
### Sender (with retry queue)
```python
from svix import Svix
svix = Svix("sk_...")
svix.message.create("app_xxx", {
"event_type": "user.created",
"payload": {"id": user.id, "email": user.email},
})
# Svix handles signing, retries, replay log
```
### Idempotency
```typescript
async function handle(event: Event) {
const exists = await redis.set(
`evt:${event.id}`, '1', { NX: true, EX: 86400 }
);
if (!exists) return; // 매 already processed
await processEvent(event);
}
```
### Retry policy
```typescript
const RETRY_SCHEDULE = [60, 300, 1800, 7200, 21600, 86400]; // seconds
async function deliver(hook, attempt = 0) {
try {
const r = await fetch(hook.url, { method: 'POST', body: hook.body, headers: hook.headers });
if (r.ok) return;
throw new Error(`status ${r.status}`);
} catch (e) {
if (attempt >= RETRY_SCHEDULE.length) {
await deadLetter(hook); return;
}
await scheduleAt(deliver, hook, attempt + 1, Date.now() + RETRY_SCHEDULE[attempt] * 1000);
}
}
```
### Push notification (FCM HTTP v1)
```typescript
await fetch(`https://fcm.googleapis.com/v1/projects/${PROJECT}/messages:send`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
message: {
token: deviceToken,
notification: { title: 'New message', body: msg.preview },
data: { conversationId: msg.conversationId },
android: { priority: 'HIGH' },
apns: { payload: { aps: { 'mutable-content': 1 } } },
},
}),
});
```
### Webhook → SQS bridge (decouple)
```typescript
app.post('/hooks/:provider', async c => {
const body = await c.req.text();
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.HOOKS_QUEUE,
MessageBody: JSON.stringify({ provider: c.req.param('provider'), body, hdr: c.req.header() }),
}));
return c.text('ok', 200); // 매 fast ack
});
```
## 매 결정 기준
| 상황 | Approach |
|---|---|
| 매 outbound webhooks | Svix / Hookdeck (managed) |
| 매 high-volume inbound | bridge to SQS/Kafka, process async |
| Mobile push | FCM (Android+iOS) / APNs direct |
| Web push | VAPID + Service Worker |
| Internal pub/sub | NATS, Redis Streams (not webhooks) |
**기본값**: HMAC-SHA256 signature + idempotency + async queue + 6-step retry + dead-letter.
## 🔗 Graph
- 부모: [[Event-Driven-Architecture]] · [[HTTP-API]]
- 변형: [[WebSockets_and_Realtime]] · [[Server-Sent-Events]]
- 응용: [[Slack-Bot-Development]]
- Adjacent: [[Idempotency]]
## 🤖 LLM 활용
**언제**: webhook receiver scaffold, signature-verify code, retry policy boilerplate.
**언제 X**: secret 관리, production replay tool 설계 — domain expertise 필요.
## ❌ 안티패턴
- **No signature verification**: 매 anyone 의 spoof 가능.
- **Sync heavy work in handler**: 매 timeout → sender retry storm.
- **No idempotency**: at-least-once 의 duplicate 처리 → double-charge.
- **Storing secret in code**: 매 secret rotation 불가.
- **No dead-letter visibility**: silent failure.
## 🧪 검증 / 중복
- Verified (Stripe/GitHub webhook docs, Svix docs, Standard Webhooks spec 2024).
- 신뢰도 A.
## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — webhook delivery + signature + push 패턴 |