Current Architecture (Problem)
Currently, every API request triggers a round-trip from arc-api/atlas-api to the auth service, which queries the database. This adds latency and creates a coupling between API services and the auth service. Every API request triggers a round-trip from arc-api/atlas-api to the auth service, which queries the database. This adds latency and creates a coupling between API services and the auth service.Target Architecture
JWT Payload Structure
The JWT uses ak0r namespace for custom claims, following the structure you specified:
Changes Required
1. Database: Add JWKS Table
The Better Auth JWT plugin requires ajwks table to store key pairs. Add it to the Drizzle schema for type safety, then run the Better Auth migration.
File: packages/db/src/schema/auth.ts
- Add a
jwkstable definition (id, publicKey, privateKey, createdAt, expiresAt) - Register in the drizzle adapter schema in
auth.ts
pnpm dlx @better-auth/cli migrate from the auth service directory.
2. Auth Service: Configure JWT Plugin with Rich Payload
File: services/auth/src/auth.ts Update thejwt() plugin configuration:
definePayload function needs to:
- Query
organizationMembersjoined withorganizationsfor the user - Query
projectMembersjoined withprojectsfor the user - Build the
k0rnamespace with org and project role maps - Determine
principal_typefrom user role
3. Auth Service: Set JWT as HttpOnly Cookie
The JWT plugin returns tokens via headers/endpoints but does not set cookies. We need to add a mechanism to set the JWT as a cookie. Approach: Add Hono middleware to the auth service that intercepts responses from session-creating endpoints (sign-in, OAuth callback, session refresh). When a session cookie is being set, also mint a JWT and set it as ak0r-jwt cookie.
File: services/auth/src/lib/jwt-cookie.ts (new)
- Hono middleware that runs after Better Auth processes requests
- Detects when
better-auth.session_tokencookie is set in the response - Calls
auth.api.getToken()(internal JWT plugin API) to mint a JWT - Appends a
Set-Cookie: k0r-jwt=<token>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900header
- Apply the JWT cookie middleware to the Better Auth catch-all handler
/session), clear the k0r-jwt cookie by setting it with Max-Age=0.
4. Auth Service: Custom Session Endpoint Enhancement
File: services/auth/src/routes/v1/session.ts- In the
GET /sessionhandler, after returning the enriched session, also set/refresh thek0r-jwtcookie so it stays in sync with session refreshes - In the
DELETE /sessionhandler, clear thek0r-jwtcookie - In the
POST /session(login) handler, the middleware from step 3 handles this
5. API Services: JWT Verification Middleware
Create a shared JWT verification utility and update both API services. File: packages/api/src/jwt.ts (new)createJwtMiddleware(options)factory function for Hono middleware- Uses
joselibrary (createRemoteJWKSet,jwtVerify) to verify JWTs - Reads JWT from
k0r-jwtcookie - Caches the JWKS (remote JWKS set handles this automatically)
- Extracts and types the
k0rclaims namespace - Exports TypeScript types for the JWT payload (
K0rJwtPayload,K0rClaims)
- Add JWT verification as the primary auth path
- Extract user, org, and project data from JWT claims
- Fall back to current cookie-forwarding session validation if no JWT cookie (migration period)
- Set context variables (
user,session,currentOrganization,orgRole) from JWT payload
- Same pattern as arc-api
- Extract
userRolefrom JWT claims (user.roleork0r.principal_type)
jose to packages/api/package.json and the catalog.
6. Cookie Size Consideration
Standard cookies have a ~4KB limit. A JWT with many org/project memberships could exceed this. Mitigation options:- Short-term: Most users have few orgs/projects; monitor payload size
- Long-term: If payload grows too large, store a JWT reference in the cookie and look up claims from a cache, or split into essential claims (current org only) + a separate endpoint for full claims
7. Next.js Apps: Forward JWT Cookie
No changes needed for the Next.js Server Actions — they already forward all cookies (includingk0r-jwt) via getCookieHeader() in apps/arc/src/lib/auth.utils.ts. The JWT cookie will be automatically forwarded alongside the session cookie.
The auth client plugins (jwtClient()) are optional and only needed if the frontend wants to explicitly request JWT tokens. Since the JWT is set as a cookie by the auth service, no client-side plugin changes are required.
Key Design Decisions
- JWT as cookie (not Bearer): Cookies are automatically forwarded by the browser and Next.js Server Actions, requiring zero changes to the API call pattern
- 15-minute JWT expiry: Short enough to limit stale claims, long enough to avoid constant refresh overhead. Refreshed on every
getSessioncall (which happens on page loads viacookieCache) - Fallback to session validation: During migration, if no JWT cookie is present, the API services fall back to the current cookie-forwarding approach
**josefor verification**: Standard, well-maintained library for JWT/JWKS operations. Already transitively available (in pnpm-lock.yaml via better-auth)- JWKS caching:
createRemoteJWKSetfromjosehandles caching and key rotation automatically