# 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.

Source: https://bettersupabase.com/docs/cli/spec

`spec` renders the documents your [`defineApi`](/docs/specs) module
describes and writes them to disk, so they live in the repository and CI can
check them for drift.

```bash
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 [#the-entry-module]

`spec` imports `specs.entry` (default `src/lib/openapi.ts`). The module
exports one of:

* `api`, or a default export: a `defineApi()` result. Every format and
  version renders from its model.
* `openapi`, `asyncapi` or `arazzo`: a finished document of that format.
  `spec` writes it as it is, applies the overlays and runs the format's
  lint. Asking for another version than the document declares reports
  `document-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](/docs/extending/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                                      |

```ts title="src/lib/openapi.ts"
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 [#configure-the-outputs]

The `specs` key in `better-supabase.config.ts` lists the documents to
write:

```ts title="better-supabase.config.ts"
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](/docs/specs/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](#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 [#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`](/docs/cli/local#openapi-emit) command still reads it.

## Options [#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:

```bash
pnpm better-supabase spec emit openapi --spec-version 3.2
pnpm better-supabase spec emit openapi --spec-version 3.0 --out legacy/openapi.json
```

An 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` [#findings-and---fail-on]

Every output reports the diagnostics of its render: the model's, the
format's own checks (see [diagnostics](/docs/specs/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 [#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 [#watch-mode]

```bash
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 [#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`](https://unpkg.com/better-supabase/schemas/spec-manifest-v1.json)
and has no timestamps, so the same inputs give the same bytes:

```json title="spec-manifest.json"
{
  "$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 [#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](/docs/specs/reference-ui).

## In CI [#in-ci]

```bash
pnpm better-supabase spec check --fail-on warning
pnpm better-supabase spec validate --manifest spec-manifest.json
```

`check` exits 1 when a file is stale or missing, or when a finding fails
`--fail-on`. Usage errors exit 2.