Scalar API reference
How the interactive Scalar reference at /reference relates to the generated OpenAPI contract, and which surface to use for which job.
Scalar renders an interactive API explorer on apps/web at GET /reference. It is a viewer, not a second source — the contract is the generated OpenAPI document, and Scalar reads it live.
Runtime path
Caption: Scalar and the Pages JSON both start from the same generateOpenApiSpec() output, but one reads it live and the other reads a committed snapshot.
The route itself, apps/web/app/reference/route.ts, is three lines:
export const GET = ApiReference({
url: '/api/v1/openapi.json',
title: 'Blinqx Public API',
});
ApiReference (@scalar/nextjs-api-reference) fetches /api/v1/openapi.json at request time, and that route (apps/web/app/api/v1/openapi.json/route.ts) calls generateOpenApiSpec() fresh on every request — it does not read the committed packages/api-contract/openapi.json file at all. So Scalar always reflects whatever is currently registered in packages/api-contract/src/openapi.ts on the running apps/web process, self-hosted with Scalar’s assets bundled (no external CDN), which matches the EU-sovereignty posture documented in the route’s own header comment.
When the two diverge
The committed packages/api-contract/openapi.json (the file Pages, Blume, and OpenWiki symlink) and Scalar’s live view are the same generator, but they can show different content when:
- Deploy lag — a schema change lands in
packages/api-contract/src/, the committed artifact is regenerated and merged, but the deployedapps/webprocess hasn’t picked up that build yet. Scalar on the old deployment still reflects the old registry until redeploy. - Regenerated contract without a deploy — the inverse: someone runs
pnpm --filter @blinqx/api-contract genand commits the refreshedopenapi.json, updating Pages/Blume/OpenWiki immediately, while the runningapps/web(already built with the new code) needs its own deploy to serve the corresponding live spec. - Local dev —
pnpm devrunsapps/webfrom source, solocalhost:3000/referencereflects uncommitted schema edits before anyone has rungenat all. This is the fastest way to see a schema change, and it is ahead of the committed artifact rather than behind it.
packages/api-contract’s generated.drift.test.ts gates the committed-vs-generator drift; it does not (and cannot) gate deploy lag between two running processes.
Authentication — what the code actually does
/reference itself carries no auth check. Reading apps/web/app/reference/route.ts end to end, the exported GET is exactly the ApiReference(...) call above — no requireActor(), no token check, nothing. Authentication in this codebase is not enforced by a global Next.js middleware.ts (there isn’t one in apps/web); it is enforced per route or per route group. The (app)/layout.tsx group calls requireActor() for every authenticated UI page, but app/reference/route.ts sits outside that group and calls nothing equivalent. So on a deployed staff app, GET /reference is reachable without a Keycloak session or any credential — anyone who can reach the host can load the page shell and read the full API description.
That is a materially different trust boundary from the underlying API itself. Every /api/v1/* route calls authenticate(request, scope) from apps/web/src/lib/api/auth.ts, which requires a scoped Bearer token (bnx_…, created via the admin token-CRUD surface referenced in Public API & docs) — a completely separate credential from the Keycloak-issued session cookie that gates the staff UI. So:
- Loading
/referenceand browsing the spec: no auth of any kind today. - Clicking “Try it out” and firing a request from the Scalar UI: the browser sends that request with no Bearer token attached automatically — a signed-in staff session cookie does not satisfy
authenticate(), because the API layer checks for a token, not a cookie. The request fails with401unless the person manually pastes a validbnx_…token into Scalar’s auth panel first.
Whether the page being publicly reachable is intentional or a gap is not something the route code answers — say so explicitly rather than guessing: this page does not state or imply that /reference is meant to be public; it only documents that today it is not gated. If that needs to change, it is a product/security decision, not a docs correction.
Which surface to use when
| You are | Use | Why |
|---|---|---|
| A partner integrating against the public API | Pages /openapi.json |
Stable published artifact; no need to reach an internal apps/web deployment |
| A developer working locally on the contract | http://localhost:3000/reference after pnpm dev |
Reflects uncommitted schema edits immediately, ahead of any committed artifact |
| A reviewer verifying a deployed environment’s live contract | https://<deployed-host>/reference |
Confirms what that specific running process actually serves, catching deploy lag the committed JSON can’t show |
Why Blume does not embed Scalar
docs/runbooks/docs-surfaces.md and openwiki/INSTRUCTIONS.md both record the same constraint: Blume does not embed Scalar because that would require hoisting @scalar/astro into apps/docs, which this repo deliberately has not done. Scalar stays on apps/web — a self-hosted Next.js route that fits the EU-sovereignty posture the /reference route comment describes — while Blume’s job is the committed, symlinked OpenAPI JSON plus the partner/developer narrative pages. Two Scalar instances (one live against the app, one static against a committed snapshot) would be a second place for the two to drift silently; keeping Scalar singular on apps/web means there is exactly one live-rendered surface, and Blume’s role stays “publish the artifact,” never “render another copy.”
See also
- Public API & docs — contract source of truth, symlinks, and the four-layer diagram this page’s runtime path expands on
- OpenWiki — how the same OpenAPI artifact reaches the in-repo wiki
apps/web/src/lib/api/auth.ts— the Bearer-token auth path every/api/v1/*route runs through