ADR 0002: Clerk for user authentication
Context
Section titled “Context”Rhumbatron needs user sign-in and a product sign-up flow for end users at
https://rhumbatron.localhost.
Decision
Section titled “Decision”Rhumbatron uses Clerk (application Rhumbatron) with @clerk/tanstack-react-start in apps/web.
The Compose stack uses a Clerk development instance. Clerk development keys accept the host
rhumbatron.localhost.
src/start.tsrunsclerkMiddleware()after the server-function CSRF middleware.ClerkProviderwraps the body contents insrc/routes/__root.tsx./sign-inand/sign-uphost the Clerk components.- When the server sees a visitor as signed out,
src/client.tsxdelays the Clerk browser script (src/lib/clerk-deferral.ts). The script loads on the first pointer, touch, key, or focus event, or at the Clerk 15 s script timeout. Clerk loads at once on/sign-inand/sign-up, with a non-zero__client_uatcookie, or with a__clerkURL parameter. Server auth is the same in all cases. - The API verifies tokens with
@clerk/backendverifyToken(workers/api/src/auth.ts). It checksazpagainstCLERK_AUTHORIZED_PARTIES(https://rhumbatron.localhost). A token withoutazppasses only whenCLERK_ALLOW_MISSING_AZPis"true".compose.yamlsets"true", because the development instance mints Backend API session tokens withoutazpfor the scripts.
Secrets
Section titled “Secrets”apps/web/.env.localholds the development keys, written byclerk init. Git ignores it.bun run stack:env(scripts/local-env.ts) copies the keys into.env. Git ignores.env. Docker Compose givesCLERK_SECRET_KEYto thewebandcoreservices at runtime. The value never appears in an image or in source.- The publishable key is public. The
webimage build inlines it as the build argumentVITE_CLERK_PUBLISHABLE_KEY. Thewebservice also gets it at runtime asCLERK_PUBLISHABLE_KEY.
Consequences
Section titled “Consequences”- Rhumbatron code does all authorization. Clerk proves identity only.
- Every
/v1route of the API runsrequireAuth, except/v1/version./healthand/v1/versionreturn only the service name and environment.
Diagram
Section titled “Diagram”One API call from a page. The browser never calls the API directly. Server functions call the
core service at API_INTERNAL_URL through apiFetch (apps/web/src/lib/server-env.ts).
sequenceDiagram participant B as Browser participant C as Clerk participant W as Web server (apps/web) participant A as API in core (requireAuth) participant D as Product database B->>C: Sign in on /sign-in (Clerk components) C-->>B: Session B->>W: Request (clerkMiddleware after CSRF middleware) W->>W: Server function calls auth().getToken() W->>A: apiFetch to API_INTERNAL_URL with Authorization Bearer token A->>A: verifyToken(CLERK_SECRET_KEY), then azp check (CLERK_AUTHORIZED_PARTIES) A->>D: ensureIdentity (users, organizations, memberships) A->>D: projectRole (organization membership or project_members row) A-->>W: JSON response, or 401 on any failed check