Chelabase

API generation

How the OpenAPI documents, the generated clients, the error shape and the API reference are produced.

Chelabase's APIs describe themselves from the Rust code. Three OpenAPI documents are committed under openapi/, typed clients are generated from them, and this site renders them as the API reference. Nothing here is edited by hand.

The documents

OpenAPI documents are generated from the Rust route handlers with utoipa 6 and utoipa-axum, and the paths come from the endpoint registry in crates/chelabase-routes.

DocumentDescribesBuilt in
openapi/management.jsonThe Management API: the orchestrator's /v1services/orchestrator/src/openapi.rs
openapi/login.jsonThe login API: auth's /auth/v1, Studio's sign-in pages and app users' sign-in flows (/auth/v1/app/*)services/auth/src/openapi.rs
openapi/data.jsonThe data API: rest's /data/v1services/rest/src/openapi.rs

The management and data references are published under /reference/ and the login API under /architecture/login-api; each page is generated from its document when the site is built.

Tests that keep them honest

Each service has tests in its openapi module that fail when a document is stale or a route isn't documented:

  • document_is_current: the committed file equals what the code generates (set CHELABASE_BLESS to rewrite it);
  • document_matches_the_registry: the document's method and path set equals the registry's public endpoints for that service;
  • operation_ids_are_the_contract: operation ids are stable, because generated clients take their function names from them;
  • every_response_has_a_description_and_errors_use_the_envelope: every response is described, and every error status references the shared error envelope;
  • schema_names_are_api_facing (or schema_names_are_specific_and_unique for the login document): component names are meaningful in the generated clients.

The data API document

The data API document is built differently, because the request body is one JSON tree that rest parses by hand rather than through serde types. chelabase_query::openapi (behind the openapi feature) holds marker types for the query language's schemas: QueryRequest (one of the seven top-level operations: select, count, insert, update, delete, call and batch, with CountRequest for count), the batch members (BatchCount among them), and the shared Filter, TableName, SelectItem, OrderBy, Returning and QueryResult. One helper builds each operation twice, once for the top level (v required, database allowed) and once as a batch member (v optional, no database).

services/rest/src/openapi.rs assembles the document: POST /data/v1/query (runQuery), GET /data/v1/schema (getDataSchema) and GET /data/v1/types (getDataTypes), the bearer and user security schemes (the Chelabase-User header), and the data API's own error envelope (code, message, and optionally detail, hint and sqlstate) for every status of 400 and above.

Because the schema is written by hand next to a hand-written parser, a fidelity test keeps the two together. crates/chelabase-query/tests/openapi_fidelity.rs reads the committed openapi/data.json and checks that:

  • every contract fixture of the TypeScript client (packages/client/fixtures) is valid against the request schema;
  • the schema and the parser reject the same invalid inputs, and accept the same edge inputs;
  • the known gaps (inputs the schema accepts but the parser rejects, such as keys that differ between insert rows or an alias of 32 multi-byte characters) are exactly the listed ones.

Regenerating

After changing any route, its request or response types, or an error code, run:

just api-gen

This command:

  • rewrites the OpenAPI documents with CHELABASE_BLESS=1 on the document tests;
  • runs orval 8 to regenerate the TypeScript clients.

Commit the result. just check fails if the documents or the generated code are out of date.

Generated clients

Two packages are generated from the documents with orval 8:

  • packages/management-client (@chelabase/management-client, private for now): TanStack Query hooks (useGetDatabase, useCreateBranch), plain functions (getDatabase(id)) and query key helpers (getGetDatabaseQueryKey(id)). Configure it once with configureManagementClient({ baseUrl, getToken, onUnauthorized }).
  • apps/studio/src/lib/login-client: Studio's internal login client, generated the same way.

Generated files are never edited by hand. Errors from these clients are thrown as ApiError { status, code, message, extra, signInRestarted }, so a caller can switch on the stable code rather than the message.

The application-facing data API client is a separate, hand-written package (@chelabase/client), checked against the data API document by the fidelity test above.

Error shape

All management, login and gateway errors use one shape:

{ "error": { "code": "<code>", "message": "<sentence>" } }

Some codes add extra keys (for example keys on role_in_use). The code is stable snake_case (for example not_found, branch_not_ready, has_children, name_taken, invalid_request, too_large, rate_limited, unavailable, worker_unavailable, not_applied, sign_in_expired, invalid_credentials). The full list is the ErrorCode enum in crates/chelabase-types/src/api_error.rs and the OpenAPI documents; see also Error codes. Another tenant's database answers 404 not_found, exactly like a missing one.

The data API keeps its own codes and extra fields (detail, hint, sqlstate).

API reference UI

Studio's API reference page (/app/api, in the left rail) renders the Management, Data and Auth API documents with "Try it out"; only the Management document gets the signed-in user's token. The docs site has the reference pages for all three APIs. The orchestrator serves no page of its own; GET /v1/openapi.json stays public behind the gateway, so any tool can load the document.

Studio's page uses @scalar/api-reference-react, pinned exactly in Studio and configured never to contact scalar.com: telemetry off, default fonts off, no request proxy, external URLs empty, agent and MCP off, developer tools never. Update it with pnpm (the minimum release age applies).

On this page