Skip to content
Basenet
🇳🇱 Nederlands
Esc
navigateopen⌘Jpreview

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 deployed apps/web process 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 gen and commits the refreshed openapi.json, updating Pages/Blume/OpenWiki immediately, while the running apps/web (already built with the new code) needs its own deploy to serve the corresponding live spec.
  • Local devpnpm dev runs apps/web from source, so localhost:3000/reference reflects uncommitted schema edits before anyone has run gen at 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 /reference and 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 with 401 unless the person manually pastes a valid bnx_… 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

Was this page helpful?