Webhooks — Data Model
The webhooks module owns no tables of its own. It writes into the user + membership domain.
Tables written
| Table | Operation | Trigger |
|---|---|---|
users | INSERT or UPDATE | user.created (insert) and user.updated (mirror profile). |
users | UPDATE (soft delete: deleted_at, is_active) | user.deleted. |
memberships | UPDATE (invited → active) | user.created when pending invitations exist for the email. |
External state
- Clerk — source of truth for identity. Webhook is the inbound channel; outbound calls happen in
apps/api/src/users/users.service.ts(e.g.deleteSelfcalls Clerk’s API unlessskipClerk: true). - Svix — Clerk’s delivery infrastructure. Each delivery has a unique
svix-idwe could persist for idempotency but currently don’t. - PostHog —
user_signed_upevent captured on firstuser.created.
Audit trail
Webhook actions are not currently written to audit_logs. Rationale: actor is “Clerk” (not a human), and the resulting writes are mechanical mirrors. Investigation goes through Pino + Sentry breadcrumb.
Idempotency
No dedupe table. Idempotency emerges from:
- Clerk id uniqueness on
users.clerk_id. syncFromClerkissuing an UPDATE withwhere clerk_id = ?— repeated identical updates are no-ops at the row level.acceptPendingInvitationschecking the membership status before flipping.deleteSelfonly settingdeleted_atif currently null.