# 11. tRPC Foundation — Auth Context, Middleware, and Protected Procedures meta: id: kordant-unified-restructure-11 feature: kordant-unified-restructure priority: P0 depends_on: [kordant-unified-restructure-10] tags: [backend, trpc, auth, api] objective: - Establish the tRPC foundation in the unified monolith: router factory, context builder with auth, middleware for protected routes, and error handling. Replace the stub tRPC setup with a production-ready auth-aware API layer. deliverables: - `web/src/server/api/utils.ts` — tRPC initialization and helpers: - `t` object from `initTRPC.context().create()` - `createTRPCRouter` factory - `publicProcedure` — unauthenticated - `protectedProcedure` — requires valid JWT/session - `adminProcedure` — requires admin role - `rateLimitedProcedure` — with basic rate limiting middleware - `web/src/server/api/trpc.ts` — Context builder: - `createTRPCContext({ req, res })` that extracts auth from: - Session cookie (SolidStart session) - `Authorization: Bearer ` header - `x-api-key` header (for service-to-service or extension calls) - Returns `{ db, user, session, apiKey }` in context - Uses Drizzle `db` instance from `web/src/server/db/index.ts` - `web/src/server/api/root.ts` — Root router: - Imports all sub-routers - Exports `appRouter` and `AppRouter` type - `web/src/routes/api/trpc/[trpc].ts` — tRPC API handler: - SolidStart API route that handles all tRPC requests - Uses `fetchRequestHandler` from `@trpc/server/adapters/fetch` - Wires `createTRPCContext` - `web/src/lib/api.ts` — tRPC client: - Updates existing file to use correct `AppRouter` type - Adds auth token injection from localStorage/cookie - Handles unauthorized responses (401 → redirect to login) - Auth utilities: - `web/src/server/auth/jwt.ts` — JWT sign/verify using `jose` or `jsonwebtoken` - `web/src/server/auth/session.ts` — Session creation and validation - `web/src/server/auth/password.ts` — Password hashing with `bcrypt` or `argon2` steps: 1. Install dependencies in `web/`: - `@trpc/server`, `@trpc/client` (already present, verify versions match) - `jose` (modern JWT library, works in Edge runtime) - `bcryptjs` or `argon2` for password hashing - `zod` for input validation (already present via valibot, but standardize on one — recommend `zod` for wider ecosystem compatibility) 2. Create `web/src/server/auth/jwt.ts`: - `signJWT(payload, secret, expiresIn)` - `verifyJWT(token, secret)` - Use `jose` for Edge compatibility 3. Create `web/src/server/auth/password.ts`: - `hashPassword(password)` → async hash - `verifyPassword(password, hash)` → boolean 4. Create `web/src/server/api/trpc.ts` (context builder): - Parse cookies from request headers - Try session cookie first (SolidStart session) - Fall back to Bearer JWT - Fall back to x-api-key - Look up user in database via Drizzle - Return context with `user`, `session`, `db`, `apiKey` 5. Update `web/src/server/api/utils.ts`: - Initialize tRPC with context type - Define `publicProcedure` - Define `protectedProcedure` using middleware that checks `ctx.user` and throws `TRPCError.UNAUTHORIZED` if missing - Define `adminProcedure` that checks `ctx.user.role === 'admin'` 6. Update `web/src/server/api/root.ts`: - Keep `exampleRouter` for now - Add placeholder imports for future routers 7. Update `web/src/routes/api/trpc/[trpc].ts`: - Create SolidStart API route - Use `fetchRequestHandler` with `router: appRouter`, `createContext: createTRPCContext` 8. Update `web/src/lib/api.ts`: - Import `AppRouter` from `~/server/api/root` - Add `httpBatchLink` with `headers` function that reads auth token from cookie/localStorage - Add error link that handles 401 by redirecting 9. Test with a temporary protected route that returns `ctx.user`. steps: - Unit: Context builder returns anonymous user for unauthenticated requests - Unit: Context builder returns full user for valid JWT - Unit: `protectedProcedure` rejects unauthenticated requests with 401 - Unit: `adminProcedure` rejects non-admin users with 403 - Integration: tRPC client can call public and protected endpoints - E2E: Login flow sets cookie/JWT and subsequent requests are authenticated acceptance_criteria: - [ ] `createTRPCContext` correctly builds context from session, JWT, or API key - [ ] `publicProcedure` allows unauthenticated access - [ ] `protectedProcedure` requires authentication and provides `ctx.user` - [ ] `adminProcedure` requires admin role - [ ] tRPC API handler responds correctly at `/api/trpc` - [ ] Client library (`web/src/lib/api.ts`) connects and handles auth automatically - [ ] JWT sign/verify works with `jose` library - [ ] Password hashing uses bcrypt or argon2 validation: - Create a temporary tRPC router with public and protected queries - Call public query from browser → should succeed - Call protected query without auth → should return 401 - Call protected query with valid JWT → should return user data - Run `cd web && pnpm test` to verify auth unit tests notes: - The legacy Fastify auth middleware (`packages/api/src/middleware/auth.middleware.ts`) is the reference for auth logic. Port its JWT verification and API key handling. - SolidStart sessions are cookie-based. The tRPC context should read the SolidStart session cookie and validate it. - For the extension (task 27), API key auth will be important. Ensure `x-api-key` handling is robust. - Consider using `lucia-auth` or `next-auth` SolidStart integration for session management, but a custom JWT approach is fine if it matches the legacy system. - Standardize on `zod` for input validation across all tRPC routers. The current setup uses valibot — migrate to zod for consistency. - Keep auth utilities in `web/src/server/auth/` separate from tRPC to allow reuse by other server code (e.g., background jobs).