Auth Module
This document describes authentication and related HTTP routes in the API at apps/api/src/modules/auth, and how Clerk is integrated via @clerk/fastify in the current runtime path.
Purpose
The auth module currently:
- relies on Clerk’s official Fastify plugin (
@clerk/fastify) and reads auth state withgetAuth(request) - rejects unauthenticated requests with
401 Unauthorized - resolves the internal application user id (database) from the identity-provider user id (Clerk), with Redis caching
- exposes an
authGuard(FastifypreHandler) for other modules (for example users routes) - exposes
POST /auth/v1/registerto register/resolve user identity links via users module use-cases
Why @clerk/fastify in this architecture
The API runs on Fastify. Clerk exposes a first-class @clerk/fastify package that:
- registers
clerkPluginon the Fastify instance withsecretKeyandpublishableKey, matching Clerk’s documented setup - runs Clerk’s request processing in the correct Fastify lifecycle (configurable hook; default works with
preHandlerguards) - exposes
getAuth(request)so HTTP adapters can readisAuthenticatedanduserIdwithout hand-rollingRequestbridges or duplicating Clerk’sauthenticateRequestwiring
That choice trades a small amount of framework coupling in the HTTP adapter (the guard calls getAuth) for:
- fewer custom integration bugs (cookies, headers, and future Clerk behavior changes are handled by the maintained plugin)
- simpler guard code (no cloning
FastifyRequestinto a WebRequestfor Clerk in middleware) - clear ownership: Clerk + Fastify concerns live in adapters and the app bootstrap; use-cases stay free of
FastifyRequest
Application-layer types such as AuthRequestContext and IdentityProviderPort remain framework-agnostic so you can still test authentication delegation or swap to another provider implementation without importing Fastify in use-cases.
At the moment, the live guard path uses getAuth directly, and createAuthenticateRequestUseCase is not wired into createAuthModule.
Module structure
| Path | Role |
|---|---|
domain/types.ts | AuthenticatedIdentity (ids as returned from an identity provider abstraction) |
application/ports/identity-provider.port.ts | IdentityProviderPort — authenticate(AuthRequestContext) (no Fastify types) |
application/use-cases/authenticate-request.ts | Delegates authentication to the port (useful for tests or non-Fastify entrypoints) |
adapters/provider/clerk-identity-provider.impl.disabled.ts | Clerk adapter for the port contract (currently disabled/not used by composition) |
application/use-cases/resolve-internal-user-id.ts | Cache + registerByIdentityProviderId to map Clerk id → internal userId |
adapters/http/middleware.ts | Fastify authGuard preHandler: getAuth(request) from @clerk/fastify, then internal id resolution |
adapters/http/routes.ts | Fastify plugin registering POST /auth/v1/register with preHandler: authGuard |
composition.ts | Wires users repo/use case, cache, guard, and routes |
::: tip
To swap Clerk for another provider while keeping the same guard shape, you could replace the getAuth-based guard with one that calls createAuthenticateRequestUseCase + a new IdentityProviderPort implementation. The port already uses AuthRequestContext, not FastifyRequest, so application code stays decoupled from Fastify.
:::
App bootstrap (Clerk plugin)
Clerk is registered once on the root Fastify app in apps/api/src/index.ts:
await app.register(clerkPlugin, {
secretKey: process.env.CLERK_SECRET_KEY!,
publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
});
Module routes (for example authRoutes) are registered on the same app so getAuth(request) sees Clerk’s populated request state.
Composition dependencies
createAuthModule({ db, cache }) requires:
db—PrismaClient(buildsuserRepofrom the users module persistence layer)cache— shared RedisCache(used to store internal user id by identity-provider user id)
It composes the users registration use case registerByIdentityProviderId so the guard and register route can ensure a row exists in your database for the signed-in Clerk user.
Request authentication flow (authGuard)
OPTIONSrequests skip the guard.- The guard calls
getAuth(request)from@clerk/fastify(Clerk plugin must be registered on the app). - If auth is missing, not authenticated, or
userIdis absent, the guard responds with401and body{ message: "Unauthorized" }. - Otherwise the guard calls
resolveInternalUserId(auth.userId)(Clerk user id string):- reads Redis key
auth:user-id-by-idp:<identityProviderUserId>if present; - on miss, calls
registerByIdentityProviderId(creates or resolves the user), then caches the internal id (default TTL 10 minutes).
- reads Redis key
- The guard sets
request.auth(Fastify request decoration used by handlers):
// request.auth (after authGuard)
{
isAuthenticated: true;
userId: string; // internal DB user id (from cache or registration)
identityProviderUserId: string; // Clerk user id (same as getAuth().userId here)
}
Downstream handlers (users module) should treat request.auth.userId as the stable id for your own tables.
Identity model
After the guard, identityProviderUserId is the Clerk userId string from getAuth. userId is your internal database user id resolved via cache + registration.
HTTP routes (/auth)
| Method | Path | Behavior |
|---|---|---|
POST | /auth/v1/register | Runs authGuard as preHandler, then calls registerByIdentityProviderId with the JSON body (RegisterUserRequest). Returns 201 if a new user was created, 200 if the user already existed. |
Current request contract for POST /auth/v1/register requires:
{
identityProviderUserId: string;
}
The handler currently forwards this body to the use-case directly. It does not currently enforce equality between request.auth.identityProviderUserId and the body value.
authRoutes is a Fastify plugin (FastifyPluginAsync). Register it on the root app with await app.register(authModule.routes) when you wire modules into index.ts.
Environment variables (Clerk)
Validated in apps/api/src/shared/kernel/env.ts and used by Clerk:
CLERK_SECRET_KEYCLERK_PUBLISHABLE_KEY(required forclerkPlugin)
Other Clerk-related secrets (for example webhooks) live outside this module’s guard path; see webhook docs if applicable.
Composition overview
Related docs
- Error handling — how failures are represented without per-handler
try/catch. - Logging — HTTP request and error logging.