Webhooks
platform-api receives identity events from Clerk (delivered via svix) and syncs them locally. This is a server-to-server entry point — you don’t call it directly.
| Endpoint | POST /api/webhooks/clerk |
| Auth | @Public (no Clerk guard) — verified by svix signature instead |
| Body | Clerk raw event JSON (raw body preserved for verification) |
| Response | 200 · { "ok": true } |
Signature verification
The controller verifies the svix headers (svix-id / svix-timestamp / svix-signature) with CLERK_WEBHOOK_SECRET. A missing secret or missing headers → 400; a bad signature → 401.
main.ts preserves the raw request body for /api/webhooks/* so signatures can be verified — don’t move webhook routes off that prefix (see Architecture & conventions).
Idempotency
Each delivery’s svix-id is the idempotency key, written to the WebhookEvent ledger:
- Check the ledger first — a previously seen id is skipped (no repeated side effects).
- Handle the event.
- Persist the id only after success — a failed delivery stays eligible for retry.
A concurrent duplicate collides on the primary key (P2002), which is exactly the “already processed” outcome and is swallowed safely.
Events handled
| Event | Action |
|---|---|
session.created | Record a sign-in (SignInEvent, upsert by clerkSessionId) |
user.created · user.updated | Sync that user from Clerk into the local mirror |
user.deleted | Delete the local user (ignored if it doesn’t exist locally) |
| anything else | Ignored |
Last updated on