---
title: Scalar API reference
description: How the interactive Scalar reference at /reference relates to the generated OpenAPI contract, and which surface to use for which job.
---

[Scalar](https://github.com/scalar/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

```mermaid
flowchart LR
  zod["packages/api-contract/src/schemas.ts (zod)"]
  gen["generateOpenApiSpec()"]
  live["GET /api/v1/openapi.json (apps/web)"]
  scalar["GET /reference (apps/web, Scalar)"]
  committed["packages/api-contract/openapi.json (committed)"]
  pages["Pages /openapi.json + this site's /developers/public-api"]

  zod --> gen --> live --> scalar
  gen -->|"pnpm --filter @blinqx/api-contract gen"| committed --> pages
```

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:

```ts
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 dev** — `pnpm 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](/developers/public-api)) — 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`](/developers/public-api) | 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`](https://github.com/blinqx-hq/basenet-rebuild-poc/blob/main/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](/developers/public-api) — contract source of truth, symlinks, and the four-layer diagram this page's runtime path expands on
- [OpenWiki](/developers/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
