Handler: app/api/install/webhook/route.ts
Verification logic: lib/auth/server/webhook.ts
Security
GitHub signs every webhook payload with X-Hub-Signature-256:
X-Hub-Signature-256: sha256=<hex-digest>Verification:
HMAC-SHA256(GITHUB_WEBHOOK_SECRET, rawBody)Compared with timingSafeEqual to prevent timing attacks.
If GITHUB_WEBHOOK_SECRET is empty or unset: the handler logs a warning and accepts all requests. This is insecure. Do not run without a secret in production.
Only POST requests are accepted. All other methods return 405.
Response codes
| Status | Condition |
|---|---|
200 { ok: true } | Event processed |
| 400 | Invalid JSON body |
| 403 | HMAC signature mismatch |
| 500 | Error during processing |
Supported events
| Event | Action | DB mutation |
|---|---|---|
installation | created, unsuspend | Fetch full details from GitHub API → upsert github_installations |
installation | deleted | DELETE FROM github_installations |
installation | suspend | UPDATE github_installations SET suspended_at, suspended_by |
installation_repositories | added, removed | Refresh installation details + repo list |
organization | any | Log only, no DB mutation |
membership | any | Log only, no DB mutation |
member | any | Log only, no DB mutation |
Events not listed above are silently ignored with a 200 response.
Idempotency
All handlers are idempotent. Processing the same event payload twice produces the same database state. Upserts are used for inserts and updates; deletes are safe to repeat.
This means webhook delivery retries from GitHub are safe and will not produce duplicate or inconsistent state.
Payload flow
Configuration
| Env var | Required | Purpose |
|---|---|---|
GITHUB_WEBHOOK_SECRET | Yes (prod) | Shared secret for HMAC signature verification |
Missing secret → webhook accepts all requests → do not deploy without this set.