Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWebhooksWebhooks

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.id and 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

PersonaImpact
MemberFirst-time signup auto-accepts any pending invitation, so the dashboard isn’t empty on first load. Profile changes in Clerk show up immediately.
OwnerStale member rows don’t accumulate — when a Clerk user is deleted, our row is soft-deleted in lockstep.
PlatformWebhook 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_up identify + capture on first sign-up.
  • Soft-deletes the local users row on user.deleted (with skipClerk: true to avoid recursion).
  • users-auth — owns the UsersService.{findOrCreateFromClerk, syncFromClerk, deleteSelf} calls invoked by the webhook.
  • event-tracking — Postgres user creation kicks PostHog identify.
  • Memberships — MembershipsService.acceptPendingInvitations is the hook that promotes invited memberships from invited to active.

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-id uniqueness 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.