Webhooks
Inbound webhook handlers consumed by the FitKit API. v1 ships one webhook source — Clerk — wired via apps/api/src/webhooks/clerk-webhook.controller.ts. Payment-provider callbacks are handled inside apps/api/src/payments/ directly (each provider exposes its own controller); they are not part of this webhooks module.
What
Public, signature-verified entry points for upstream services to push events into FitKit.
Why
- Clerk owns the canonical user record. We mirror users into our DB so we can join membership rows against
users.idand treat the DB as the system of record for app data while Clerk remains the identity source. - Email change, image change, and self-delete must propagate even when the user never re-opens the FitKit app.
- Auto-accepting pending org invitations on first sign-up is the only place where “the user just signed up” is reliably knowable.
Who
- Clerk — push user lifecycle events (
user.created,user.updated,user.deleted). - FitKit API — receives, verifies, persists.
- Indirectly: every member, coach, owner whose Clerk profile changes.
Persona impact
| Persona | Impact |
|---|---|
| Member | First-time signup auto-accepts any pending invitation, so the dashboard isn’t empty on first load. Profile changes in Clerk show up immediately. |
| Owner | Stale member rows don’t accumulate — when a Clerk user is deleted, our row is soft-deleted in lockstep. |
| Platform | Webhook is the only path that creates an internal users row outside of impersonation/admin tooling. |
Capabilities
- Svix signature verification on every Clerk delivery — bodies with a missing or bad signature are 400’d.
- Handles
user.created,user.updated,user.deleted. - Auto-accepts pending org invitations on first sign-up.
- PostHog
user_signed_upidentify + capture on first sign-up. - Soft-deletes the local
usersrow onuser.deleted(withskipClerk: trueto avoid recursion).
Related features
users-auth— owns theUsersService.{findOrCreateFromClerk, syncFromClerk, deleteSelf}calls invoked by the webhook.event-tracking— Postgres user creation kicks PostHog identify.- Memberships —
MembershipsService.acceptPendingInvitationsis the hook that promotes invited memberships frominvitedtoactive.
Status
Shipped. No outstanding work on the Clerk surface. Roadmap: bring payment-provider webhooks (CardCom, Morning, Bit) into this module for consistency.
Gaps
- No event-level idempotency log — Svix’s
svix-iduniqueness is what we’d dedupe on, but we currently rely on each handler being naturally idempotent (findOrCreateFromClerk, upserts). - No replay endpoint — if Clerk’s retry budget is exhausted, the platform team has no in-product way to replay a missed event.
- No public observability dashboard — failed signature verifications show in Sentry only.
- Payment-provider webhooks live in
apps/api/src/payments/rather than this module; consolidating would simplify ops.