The Studio (apps/stream) gets its own generated API client and JWT auth, not @repo/api's
frontend/apps/stream/ already depended on @repo/api (Orval-generated hooks shared with apps/web), but that package's mutator (authedInstance in frontend/packages/api/src/mutator.ts) calls apiProxy, a TanStack Start server function. apps/stream is a plain Vite SPA with no SSR server — that import cannot resolve there, so none of the existing generated hooks (including the standalone-media ones like useApiV1VideosCreate) actually work inside it today.
We decided the Studio needs its own Orval client: a second orval.config.ts (same OpenAPI schema as source) generating into its own package/output, paired with its own mutator that attaches a JWT Bearer token directly, with no server-side proxy involved.
Considered Options
- Plain fetch, no Orval (mirror
apps/admin'sapiClient.ts/useDataTablepattern): simplest, zero new infra, but throws away type safety and gives up the schema-driven contract that catches drift when the backend changes. - Make
@repo/api's mutator pluggable (swapapiProxyfor a fetch+Bearer implementation depending on which app calls it): keeps one generated client, but couplesapps/web's server-function-based auth model to a second, incompatible one in the same file — risks regressingapps/webfor the sake of an app with fundamentally different auth. - Separate Orval client for the Studio (chosen): own
orval.config.ts, own generated output, own mutator. Keeps type-safety/schema-drift protection for both apps without forcing them to share an auth model that doesn't fit both.
Why this was chosen
apps/stream's Studio is standalone by design (ADR-0009's Standalone Media, extended to the full pipeline) — it authenticates aCustomUserdirectly with no Community/Membership context, unlike every other Orval consumer in the repo.- Auth mirrors
apps/admin's existing, working pattern: JWT (access + refresh) inlocalStorage,Authorization: Bearerattached per request, refresh-on-401 against/api/v1/auth/token/refresh/. No SSR, no session held server-side. - The two mutators solve genuinely different problems (server-held session vs. client-held JWT) — collapsing them into one pluggable mutator would make
frontend/packages/api/src/mutator.tsharder to reason about for no shared benefit.
Consequences
- Two Orval configs now read the same
openapi.jsonand must both be regenerated (make generate-apineeds to cover both, or a second script) whenever the backend schema changes — a schema change affecting standalone-media endpoints needs both clients regenerated, not just one. - The Studio holds a JWT in browser storage (unlike
apps/web, which never does — seemutator.ts's comment). This is an accepted, scoped exception: same trade-offapps/adminalready made, not a new risk pattern. frontend/apps/stream/package.json's existing@repo/apidependency becomes dead weight for the Studio's own calls (it may still be useful if the Embed player wants a typed client for the publicPublicMediaDetailViewresponse) and should be re-evaluated once the new client lands.