Problem
The frontend's typed API client is built for a header the backend no longer uses. api-client.ts attaches no auth header and still documents API keys:
// app/frontend/src/lib/api-client.ts
/**
* - Auth: the backend uses x-api-key headers. If a default key is needed,
* add `headers: { 'x-api-key': '...' }` to the createClient options.
*/
export const apiClient = createClient<paths>({
baseUrl: apiUrl,
fetch: withTimeoutFetch(fetchClient as typeof fetch) as typeof fetch,
});
But the generated spec this client is typed against declares JWT auth on its routes:
// app/frontend/src/lib/generated/api.ts (multiple endpoints)
/** @description Unauthorized - valid JWT token required. */
The backend issued OIDC JWTs (tracked as issue #218), so the live API now expects a bearer token, while the generated client neither acquires, attaches, nor refreshes one.
Consequence: every request through apiClient to a JWT-required endpoint is sent without an Authorization header and fails with 401 unless each caller hand-rolls token attachment. Auth is fragmented: there is no single place that acquires a token, attaches it to openapi-fetch requests, and refreshes it on expiry, so the typed client's compile-time guarantees mask a runtime auth failure. The x-api-key comment actively misleads a contributor into wiring the wrong credential.
Root cause
The OIDC/JWT backend work (#218) changed the auth model after api-client.ts was written, but the client was never updated to a token-aware fetch, and no CI/test exercises an authenticated request end-to-end.
Why this is architecturally hard
- Token lifecycle is a cross-cutting concern. The fix is not a one-line header: it needs token acquisition, storage, attach-on-request, and refresh-on-401 (with single-flight refresh so concurrent requests share one refresh). A naive
headers: { Authorization: token } baked at client construction breaks the moment the token rotates.
- Mock and SSR paths must stay coherent.
api-client.ts routes through fetchClient (mock interception via NEXT_PUBLIC_USE_MOCKS) and withTimeoutFetch; token attachment must compose with both, and with Next.js server/client boundaries (next.config.ts, src/proxy.ts, src/i18n.ts) rather than bypassing them.
- The generated types are the source of truth. The client types come from
openapi.json via pnpm generate:api; the auth wiring must not fight the generated paths types, and adding auth headers must keep the typed request/response contract intact.
- Refresh failure must be observable. A 401 caused by an expired token must be distinguishable from a 401 caused by an unauthorized role so the client refreshes the former and surfaces the latter.
Proposed design
Introduce a token-aware fetch wrapper (or openapi-fetch middleware) that reads the token from a store, attaches Authorization: Bearer <token>, and performs single-flight refresh on 401. Keep createClient typed against paths, and update the x-api-key comment to reflect the JWT model.
Downstream impact
apiClient is the entry point for all frontend API calls (including verification-api.ts, verification-inbox-api.ts, and dashboard hooks under src/hooks). The auth model must stay consistent with app/backend/src/auth-oidc (JWT issuance/revocation). Regenerating types after any schema change is cd app/frontend && pnpm generate:api.
Acceptance criteria
Client
Tests
Out of scope
The OpenAPI spec-drift CI gate and backend JWT issuance/revocation are separate issues.
Getting started
Files: app/frontend/src/lib/api-client.ts, app/frontend/src/lib/generated/api.ts, app/frontend/src/lib/env.ts, app/frontend/src/lib/retry.ts.
cd app/frontend
pnpm type-check
pnpm test
Good first files to read: lib/api-client.ts (the client + stale comment) and a few 401 ... valid JWT token required entries in lib/generated/api.ts.
Problem
The frontend's typed API client is built for a header the backend no longer uses.
api-client.tsattaches no auth header and still documents API keys:But the generated spec this client is typed against declares JWT auth on its routes:
The backend issued OIDC JWTs (tracked as issue #218), so the live API now expects a bearer token, while the generated client neither acquires, attaches, nor refreshes one.
Consequence: every request through
apiClientto a JWT-required endpoint is sent without anAuthorizationheader and fails with 401 unless each caller hand-rolls token attachment. Auth is fragmented: there is no single place that acquires a token, attaches it toopenapi-fetchrequests, and refreshes it on expiry, so the typed client's compile-time guarantees mask a runtime auth failure. Thex-api-keycomment actively misleads a contributor into wiring the wrong credential.Root cause
The OIDC/JWT backend work (#218) changed the auth model after
api-client.tswas written, but the client was never updated to a token-aware fetch, and no CI/test exercises an authenticated request end-to-end.Why this is architecturally hard
headers: { Authorization: token }baked at client construction breaks the moment the token rotates.api-client.tsroutes throughfetchClient(mock interception viaNEXT_PUBLIC_USE_MOCKS) andwithTimeoutFetch; token attachment must compose with both, and with Next.js server/client boundaries (next.config.ts,src/proxy.ts,src/i18n.ts) rather than bypassing them.openapi.jsonviapnpm generate:api; the auth wiring must not fight the generatedpathstypes, and adding auth headers must keep the typed request/response contract intact.Proposed design
Introduce a token-aware fetch wrapper (or
openapi-fetchmiddleware) that reads the token from a store, attachesAuthorization: Bearer <token>, and performs single-flight refresh on 401. KeepcreateClienttyped againstpaths, and update thex-api-keycomment to reflect the JWT model.Downstream impact
apiClientis the entry point for all frontend API calls (includingverification-api.ts,verification-inbox-api.ts, and dashboard hooks undersrc/hooks). The auth model must stay consistent withapp/backend/src/auth-oidc(JWT issuance/revocation). Regenerating types after any schema change iscd app/frontend && pnpm generate:api.Acceptance criteria
Client
apiClientattaches a valid bearer token to authenticated requests without per-caller manual headers.x-api-keycomment is removed/replaced with the correct JWT guidance.Tests
Out of scope
The OpenAPI spec-drift CI gate and backend JWT issuance/revocation are separate issues.
Getting started
Files:
app/frontend/src/lib/api-client.ts,app/frontend/src/lib/generated/api.ts,app/frontend/src/lib/env.ts,app/frontend/src/lib/retry.ts.Good first files to read:
lib/api-client.ts(the client + stale comment) and a few401 ... valid JWT token requiredentries inlib/generated/api.ts.