spec
Write, check and validate the OpenAPI, AsyncAPI and Arazzo documents your defineApi module renders, with a content-hash cache, watch mode and a diagnostics manifest.
spec renders the documents your defineApi module
describes and writes them to disk, so they live in the repository and CI can
check them for drift.
pnpm better-supabase spec emit # write every configured document
pnpm better-supabase spec check # exit 1 when a file is out of date
pnpm better-supabase spec validate # also check against the official schemas| Action | What it does |
|---|---|
emit | Renders each output and writes the files that changed |
check | Renders each output and compares it with the file on disk, ignoring line endings; writes nothing |
validate | Renders each output, writes nothing, and checks the document against its format's official JSON Schema as well |
A second positional argument keeps one format: spec emit asyncapi. When
no output of that format is configured, spec renders it on its own to the
format's default file name.
The entry module
spec imports specs.entry (default src/lib/openapi.ts). The module
exports one of:
api, or a default export: adefineApi()result. Every format and version renders from its model.openapi,asyncapiorarazzo: a finished document of that format.specwrites it as it is, applies the overlays and runs the format's lint. Asking for another version than the document declares reportsdocument-version.
Each export may also be a function that returns the value. Two more exports are optional:
| Export | Effect |
|---|---|
formats | An array of extra document formats; one with a built-in id replaces it |
cacheKey | A string folded into the cache key, for inputs the model doesn't show |
import { defineApi } from "better-supabase/spec";
import { betterSupabase } from "./supabase";
export const api = defineApi(betterSupabase, {
info: { title: "CRM API", version: "1.0.0" },
basePath: "/api",
resources: { customers: true },
});The module is loaded fresh on every run, so --watch sees your edits.
Configure the outputs
The specs key in better-supabase.config.ts lists the documents to
write:
import { defineConfig } from "better-supabase/config";
export default defineConfig({
specs: {
entry: "src/lib/api.ts",
outputs: [
{
format: "openapi",
version: "3.1",
output: "openapi.json",
ui: "scalar",
},
{
format: "openapi",
version: "3.2",
output: "public/openapi.yaml",
overlays: ["specs/public.overlay.yaml"],
},
{ format: "asyncapi", output: "asyncapi.json" },
{ format: "arazzo" },
],
failOn: "warning",
},
});| Key | Default | What it sets |
|---|---|---|
entry | openapi.entry, else src/lib/openapi.ts | The entry module |
outputs | [{ format: "openapi", output: "openapi.json" }] | The documents to write |
failOn | "error" | The lowest severity that fails emit, check and validate: error, warning or never |
Each output takes:
| Key | Default | What it sets |
|---|---|---|
format | required | openapi, asyncapi, arazzo, or the id of a format the entry adds |
version | The format's default (3.1 for OpenAPI) | The version to render |
output | The format's file name (openapi.json, asyncapi.json, arazzo.json) | The file to write, relative to the project root |
overlays | [] | Overlay files (JSON or YAML), applied in order |
serialize | yaml when output ends in .yaml or .yml, else json | The file format |
ui | none | Also write a reference page next to an OpenAPI output (see Reference pages) |
Two outputs that write the same file are a usage error. Overlay files that
don't parse, or that have no overlay and actions, report
overlay-invalid.
JSON keeps the generated key order and ends in a newline. YAML is YAML 1.2 in the same order, with no anchors and no line folding, so the same document always gives the same bytes.
The openapi key
openapi: { entry, output } is a deprecated alias of specs. When specs
is unset, openapi.entry becomes specs.entry and openapi.output becomes
the output of the one OpenAPI document. The
openapi emit command still reads it.
Options
| Option | Effect |
|---|---|
--spec-version <version> | Render this version instead of the configured one (see below) |
--out <file> | Write the one selected output to this file. More than one match is a usage error |
--entry <file> | Use this entry module instead of specs.entry |
--yaml | Write YAML instead of JSON |
--ui <name> | Also write a reference page next to each OpenAPI output: scalar, swagger, redoc, elements or rapidoc |
--fail-on <level> | Exit 1 on findings at this level: error, warning or never. Defaults to specs.failOn |
--manifest <file> | Also write the diagnostics manifest to this file. Defaults to specs.manifest |
--cache, --no-cache | Reuse renders whose inputs did not change (the default), or render every output again |
--watch | With emit, render again whenever a watched file changes |
--interval <ms> | Milliseconds between checks in --watch. Defaults to gen.watchInterval, then 1000 |
--spec-version keeps the configured outputs at that version. When none
match, and the selection has one format, it renders that version to a file
of its own (openapi-3.2.json), so it never overwrites the output of the
configured version; with --out it writes there instead. When the
selection spans more than one format, name one:
pnpm better-supabase spec emit openapi --spec-version 3.2
pnpm better-supabase spec emit openapi --spec-version 3.0 --out legacy/openapi.jsonAn unknown format or version, a bad --fail-on or --ui, --watch with
another action than emit, and an --interval that is not a positive
number exit with code 2.
Findings and --fail-on
Every output reports the diagnostics of its render: the model's, the
format's own checks (see diagnostics) and the
ones spec adds. They print grouped by severity, each with its output
path, code and JSON Pointer:
--fail-on | Fails on |
|---|---|
error | errors |
warning | errors and warnings |
never | no finding; a stale or missing file still fails check |
The codes spec adds:
| Code | Severity | Meaning |
|---|---|---|
output-stale | error | check: the file on disk differs from the rendered document |
output-missing | error | check: the file does not exist yet |
schema-invalid | error | validate: the document does not match the format's official JSON Schema |
schema-unavailable | info | validate: no official schema is bundled for this version; only the semantic checks ran |
overlay-invalid | error | An overlay file does not parse or is not an Overlay document |
document-version | error | The entry exports a finished document in another version than the one asked for |
lint-failed | warning | The format's lint threw on a finished document the entry exports |
ui-unsupported | info | ui is set on an output that is not OpenAPI |
validate checks against the official schemas the CLI bundles: OpenAPI
3.0, 3.1 and 3.2, AsyncAPI 3.0 and 3.1, and Arazzo 1.0 and 1.1. The OpenAPI
3.3-preview and formats an entry adds get schema-unavailable. It reports
at most 50 schema errors per document, then one line with the count of the
rest. The validator and the schemas load only for validate.
The cache
Rendering a large model takes time, so spec keeps each render under
node_modules/.cache/better-supabase/specs, one file per output path. The
key is a SHA-256 of the CLI version, the format, the version and its pin,
the serialization, the model (as canonical JSON), the overlay files' text
and the entry's cacheKey. A hit prints cached next to the output, and a
hit from validate keeps its schema findings.
A model that can't be serialized (for example one that holds functions) is
never cached. Export a cacheKey from the entry when the document depends
on something the model doesn't show. --no-cache renders every output
again. A read-only node_modules only costs the next run a render.
Watch mode
pnpm better-supabase spec emit --watch--watch runs emit, then checks every --interval milliseconds whether
the config file, the entry module, the generated module (output) or an
overlay file changed, and runs again when one did. A changed config file is
loaded again first. A failed run is retried on the next change, and the same
error prints once. Files the entry imports are not watched, apart from the
generated module.
The manifest
Every run writes a diagnostics manifest to
node_modules/.cache/better-supabase/specs/diagnostics.json,
--manifest <file> writes a copy, and --json prints it on stdout. It follows
schemas/spec-manifest-v1.json
and has no timestamps, so the same inputs give the same bytes:
{
"$schema": "https://unpkg.com/better-supabase/schemas/spec-manifest-v1.json",
"version": 1,
"generator": "better-supabase 0.7.1",
"failOn": "error",
"ok": true,
"outputs": [
{
"format": "openapi",
"version": "3.1",
"pin": "3.1.2",
"path": "openapi.json",
"sha256": "9f2c…",
"status": "unchanged",
"cached": true,
"diagnostics": []
}
],
"summary": { "error": 0, "warning": 0, "info": 0 }
}| Field | What it holds |
|---|---|
ok | false when a finding fails failOn or check found a stale file |
outputs | Per output: format, version, the exact pin it follows, path, SHA-256 of the text, status, whether it came from the cache, and its diagnostics |
status | written or unchanged (emit), up-to-date or stale (check), validated (validate) |
summary | The number of findings per severity |
Reference pages
ui on an output, or --ui <name>, also writes an HTML reference page next
to each OpenAPI document: openapi.html for openapi.json. The page loads
the document from ./openapi.json and takes its title from info.title.
check compares the page too. The UIs, their pinned versions and the
self-hosted mode are on Reference UIs.
In CI
pnpm better-supabase spec check --fail-on warning
pnpm better-supabase spec validate --manifest spec-manifest.jsoncheck exits 1 when a file is stale or missing, or when a finding fails
--fail-on. Usage errors exit 2.
Last updated on