# Frontend types The backend's `fastrag.serve` Pydantic models and the `chatbot`/`local-chatbot` widget in `ui/registry/` need to agree about the `/v1` wire format. Generating widget types from the backend's OpenAPI document prevents hand-maintained shapes from drifting; CI fails when fresh generation changes either artifact. The dashboard reads Postgres directly and derives its database types from a separate Drizzle-introspected schema. It does not import the widget's types. ## The one-way flow ```text fastrag.serve's Pydantic models (source of truth) │ task openapi ▼ sdk/openapi.json (checked in, generated) │ task generate:api-types (from ui/registry/) ▼ ui/registry/.../lib/chatbot/api.d.ts (checked in, generated) │ imported locally └──────────────► widget chatbot/local-chatbot types ``` Never the reverse, and never a third hand-maintained schema file both sides are supposed to follow. To change the contract: 1. Edit the Pydantic models in `fastrag.serve` (`_schemas.py` for request/response bodies, `_generation.py`/`_schemas.py` for the SSE event payloads — see {doc}`serving-internals` and {doc}`/guide/serving/streaming`). 2. `task openapi` — rebuilds an app via `create_app` (no server, no real `QueryEngine`/`ChatStore` needed — see `scripts/generate_openapi.py`) and overwrites `sdk/openapi.json`. 3. `cd ui/registry && task generate:api-types` — runs `openapi-typescript` against `../../sdk/openapi.json` and overwrites `src/registry/new-york/chatbot/lib/chatbot/api.d.ts`. 4. Commit `openapi.json` and `api.d.ts` together with the model change. The "Frontend types" GitHub Actions workflow repeats steps 2–3 on every pull request touching `sdk/` or `ui/registry/` and fails if either file comes out different from what is checked in. ## Why the generated file lives inside the widget `api.d.ts` lives inside the `chatbot` registry item's own directory, not in a shared package, because `ui/registry/` is a shadcn-style registry: a consumer runs `npx shadcn add .../chatbot.json` and gets a **copy** of every file the item lists, `api.d.ts` included — the widget has to be self-contained, so its own generated types travel with it. `types.ts` (the widget's hand-maintained, friendly names — `Role`, `Source`, `Message`, …) is a thin layer of type aliases onto `api.d.ts`'s `components["schemas"]`, not a second hand-typed copy of the server's shapes. The dashboard uses `task db:pull` (`drizzle-kit pull`) to introspect the SDK-owned Postgres schema into `dashboard/lib/chats/generated/schema.ts`. `dashboard/lib/chats/postgres.ts` derives `MessageRole`, `FinishReason`, and `ErrorCode` from Drizzle's inferred message row type. JSONB source URLs are validated at the read boundary because database introspection does not describe nested JSON shapes. This database schema is separate from the widget's HTTP contract and is not regenerated by the Frontend types workflow. ## Two `error.code` vocabularies, two schemas The OpenAPI document publishes two different enums for what looks like the same field: - **`ErrorCode`** — the nine values a *generation* that started and then failed can record (`fastrag.query.chat.base.ERROR_CODES`), carried in the SSE `message_end` event's `error.code` and in a stored message's `MessageResponse.error.code`. - **`ApiErrorCode`** — the values a *request-level* failure carries instead (a `401`/`404`/`409`/`413`/`415`/`422`/`429`/`500` raised before any stream opens), derived from `_errors.py`'s `HTTP_STATUS_ERROR_CODES` plus its two default codes. See {doc}`/guide/serving/streaming` for why a request that never reaches generation can never produce an `ErrorCode`. Both are real Python `enum.Enum` classes (`fastrag.serve._schemas.ErrorCode` / `ApiErrorCode`, and the matching `MessageRole` / `FinishReason`), built from the same tuples `fastrag.query.chat.base` already exposes — not a second hand-typed list — so they can never enumerate a value the store itself does not know about. ## SSE event schemas FastAPI only publishes a Pydantic model into the OpenAPI document's `components/schemas` when some route's request or response references it. The three streaming routes' actual response is a bare `StreamingResponse` with no declared body, so the SSE event payloads (`MessageStartEvent`, `SourcesEvent`, `DeltaEvent`, `TextDelta`, `MessageEndEvent`) would never appear on their own. `_openapi.py`'s `openapi_schema()` force-publishes them after the fact — `scripts/generate_openapi.py` calls it instead of `app.openapi()` directly — purely so `api.d.ts` still gets a named type for every event {doc}`/guide/serving/streaming` documents. ## See also {doc}`/guide/serving/http-api` and {doc}`/guide/serving/streaming` for the contract itself; {doc}`serving-internals` for `fastrag.serve`'s module layout; `ui/registry/src/registry/new-york/chatbot/lib/chatbot/types.ts` for the widget's friendly aliases.