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/nextjsmiddleware (apps/web/src/middleware.ts) enforces protection on/[lang]/dashboard,/[lang]/schedule, etc. - API —
AuthGuard(apps/api/src/auth/auth.guard.ts) verifies Clerk JWT on every request. Default protection; opt out via@Public()decorator. - DI —
CLERK_CLIENTtoken (fromAuthModule) makes the Clerk admin SDK available to any service. - User sync — Clerk webhook (
apps/api/src/webhooks/clerk*) keeps theuserstable in sync with Clerk onuser.created,user.updated,user.deleted. - E2E test mode —
TEST_AUTH_BYPASS=trueletsx-test-user-idreplace 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
userstable 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
membershipstable 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 inapps/api/src/webhooks/.
Related
- architecture/auth.md — operational details
- ADR-0004 — multi-org model