Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0002: Clerk for authentication

ADR-0002: Clerk for authentication

Status: Accepted Date: ~2026-01 (estimate) Context owner: Owner

Context

FitKit needs:

  • Email + password sign-in.
  • Social sign-in (Google, Apple — likely for the future native app).
  • Magic-link sign-in for invitation flows.
  • Multi-org membership (a user belongs to many orgs as different roles).
  • Hebrew + English UI for sign-in / sign-up.
  • A path to native mobile sign-in later.

Rolling our own would mean reinventing token rotation, session management, MFA, password reset, magic links, webhook handling for user lifecycle events, and the security review that goes with all of that. As a solo-dev codebase, that’s a non-starter.

Decision

Use Clerk as the authentication provider on both web and API.

  • Web@clerk/nextjs middleware (apps/web/src/middleware.ts) enforces protection on /[lang]/dashboard, /[lang]/schedule, etc.
  • APIAuthGuard (apps/api/src/auth/auth.guard.ts) verifies Clerk JWT on every request. Default protection; opt out via @Public() decorator.
  • DICLERK_CLIENT token (from AuthModule) makes the Clerk admin SDK available to any service.
  • User sync — Clerk webhook (apps/api/src/webhooks/clerk*) keeps the users table in sync with Clerk on user.created, user.updated, user.deleted.
  • E2E test modeTEST_AUTH_BYPASS=true lets x-test-user-id replace JWT verification. Never enabled in production.

Consequences

Positive

  • Zero auth code to maintain. Sign-in, magic links, password reset, MFA all “just work.”
  • Webhooks make it easy to keep a local users table for joins / FK constraints.
  • The Clerk SDK is well-typed; CurrentUser decorator yields a typed userId.
  • Their Hebrew localization is workable (not perfect, but acceptable).

Negative

  • Vendor lock-in. Migrating auth providers later means re-mapping the user table and forcing all users to reset credentials.
  • Cost scales with MAU once free tier is exceeded.
  • The Clerk webhook signing secret is load-bearing: a wrong/expired secret silently breaks the user-sync pipeline. See runbooks/incident-response.md.
  • Org membership is managed in our DB, not Clerk’s orgs feature — we use Clerk only for the user identity, and our memberships table is the source of truth for “this user belongs to this org as this role.” Reason: we needed richer role + tier semantics than Clerk’s org primitive supports today.

Open questions / risks

  • If we adopt Clerk Organizations later for invite UX, we’ll need to reconcile their org model with ours.
  • Account deletion: when Clerk fires user.deleted, our webhook removes/redacts the user row, but membership history rows reference that user. Soft-redaction (anonymize PII) rather than hard-delete is the current pattern; verify this in apps/api/src/webhooks/.