# Serve OpenAPI Docs from a Fastify API from a Linear Issue

Yes. An agent picks up the Linear issue, serves an OpenAPI document built from your Fastify route schemas, tests that it lists every route and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-openapi-docs/fastify/), in a Project that has your repository.

## What the agent does

The agent starts from the issue and the repository, cloned into its own sandbox, and checks what is already registered, since Fastify apps often have `@fastify/swagger` installed and half set up. In fastify/demo (Fastify `^5.10.0`, `@fastify/autoload`, TypeBox schemas through `@fastify/type-provider-typebox`, Knex on MySQL), `package.json` already depends on `@fastify/swagger` `^9.8.1` and `@fastify/swagger-ui` `^6.1.0`, and `src/plugins/external/swagger.ts` registers both: the document with `openapi.info` titled "Fastify demo API" and `hideUntagged: true`, and the UI with `routePrefix: '/api/docs'`, which puts the JSON document at `/api/docs/json`, not the `/documentation/json` the issue names. So the work is not "register @fastify/swagger"; it is making the document complete and putting it where the issue says, and the agent writes that in its plan.

`hideUntagged: true` is why the document is incomplete today. It leaves out every route whose schema has no `tags`, and in this codebase only `src/routes/api/tasks/index.ts` tags its routes (`tags: ['Tasks']` on ten of them). `POST /api/auth/login` in `src/routes/api/auth/index.ts`, `PUT /api/users/update-password` in `src/routes/api/users/index.ts`, `GET /api` and the home route in `src/routes/home.ts` have none, so they are missing from the document. The agent adds tags such as `Auth` and `Users` to those schemas, gives the routes that have no response schema one built from the TypeBox types in `src/schemas/`, and adds a session cookie security scheme, because `src/routes/api/autohooks.ts` rejects any request without `request.session.user` outside `/api/auth/login`, and a document that hides that is wrong about every route.

On the path, it follows the issue: the document at `/documentation/json`, by setting the UI's `routePrefix` to `/documentation` and saying in the pull request that the old `/api/docs` link moves. It checks two things the code makes easy to miss. The swagger plugins load from `plugins/external` through autoload, outside the `routes/api` folder whose `autohooks.ts` requires a session, so it confirms with a request that the document is reachable without signing in, or says that it is not if your team wants it private. And `@fastify/helmet` is registered with its default options, whose Content-Security-Policy can stop the Swagger UI page from loading its scripts, so the agent opens the UI in the sandbox's browser instead of trusting a 200.

The test goes in `test/routes/`, written with `node:test` like the rest of the suite and using the `build` helper in `test/helper.ts`. It collects every route the app registers with an `onRoute` hook, fetches `/documentation/json` with `app.inject`, converts Fastify's `:id` parameters to OpenAPI's `{id}`, and fails with the list of routes missing from `paths`. It keeps the request count low, because CI sets `RATE_LIMIT_MAX: 4` for the global `@fastify/rate-limit`. Last, it does what `.github/workflows/ci.yml` does against a `mysql:8.4` service started in the sandbox: `npm i --ignore-scripts`, `npm run build -- --noEmit`, `npm run lint`, `touch .env`, then `npm run db:migrate && npm run test` with the same environment variables, until everything passes.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Serve OpenAPI Docs from a Fastify API. Register @fastify/swagger so every route's TypeBox schema appears in a generated OpenAPI document at /documentation/json, and add a test that the document lists every route.\" Before changing anything, check whether @fastify/swagger or @fastify/swagger-ui is already registered, with which options and at which path, and which routes the document leaves out and why (hideUntagged, missing schemas, encapsulation). Say what already exists. Serve the document at /documentation/json and note any old path that moves. Give every route tags and a response schema from the existing TypeBox schemas, and describe the session or token auth the routes require as a security scheme. Confirm with a request whether the document needs a session, and open the UI in the browser to check it loads under the app's security headers. Add a node:test test that collects every registered route with an onRoute hook and fails listing any route missing from the document's paths. Start MySQL in the sandbox, run the build, lint, migrations and npm test the way CI does until they pass, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that links the Linear issue. In an app shaped like fastify/demo it changes `src/plugins/external/swagger.ts` (the `routePrefix`, a `components.securitySchemes` entry for the session cookie), adds `tags` and response schemas to the routes in `src/routes/api/auth/`, `src/routes/api/users/` and `src/routes/api/index.ts`, and adds one test file under `test/routes/`. The description opens with what was already there: both plugins registered, the document at `/api/docs/json`, and `hideUntagged` hiding every route outside tasks. It then lists the routes that were missing and are now present.

The proof is the test and the transcript. The new test fails if a route is added later without making it into the document, so the issue's requirement stays true after this pull request. The session keeps every command with its output: the MySQL start, `npm run build -- --noEmit`, `npm run lint`, `npm run db:migrate` and `npm run test`, plus the request to `/documentation/json` and a screenshot of the UI. The repository's `CI` workflow runs the same steps on the pull request across Node 22 and 24.

Before merging, a reviewer decides whether the document should be public (the agent reports whether it needs a session today), checks that response schemas match what the handlers really return rather than what the agent inferred, and makes sure nothing that linked to `/api/docs` breaks when the path moves.

## Run it on every Linear issue labelled agent

In the console, add an automation to the Project with a Linear trigger on `linear.issue.labeled` and the label `agent`. Each issue that gets the label starts one run in the Project's repository with the issue's text as context, and the agent's own updates through the integration do not start another.

From your own server, the same run is a `POST /o/{org}/sessions` per issue: the repository and the `https://linear.app/<workspace>/issue/<ID>` URL both go in `resources`, and Rush reads the issue through your Linear connection. A webhook on the API key posts `run.completed` or `run.failed` with the `session_url` once the turn ends.

## Why run it on Rush?

Proving that a generated OpenAPI document is complete means booting the whole app with every plugin and route loaded, which for this API means MySQL migrated and seeded, a cookie secret, and the same rate limit CI uses. In a Rush sandbox the agent starts MySQL 8.4, runs the migrations and the full `npm test`, and opens the Swagger UI in its own browser, so the check covers the app as it boots rather than one route file in isolation.

The issue becomes a pull request that explains itself: what was already registered, which routes were missing and why, and a test that keeps the document honest from now on. The transcript keeps each command with its output beside it, and several labelled issues can run at once in separate sandboxes, up to the key's `max_concurrent` limit.

## Frequently asked questions

### Why are some of my routes missing from the Swagger document?

Usually one of three things: `hideUntagged: true` with routes that have no `tags`, routes registered outside the encapsulation context `@fastify/swagger` was registered in, or routes registered before the plugin. In fastify/demo it is the first. The agent finds which applies and fixes the routes, not the test.

### Will it change my route schemas?

It adds `tags`, and a response schema where a route has none, built from the TypeBox types already in the codebase. Because Fastify uses response schemas to serialize replies, a new one can drop fields a handler returns, so the agent runs the full test suite and lists every schema it added for review.

### Does the document need authentication?

The agent checks with a request and reports it. In fastify/demo the session check lives in `src/routes/api/autohooks.ts` and applies to routes under `routes/api`, while the swagger plugins load from `plugins/external`. If you want the document private, say so in the issue and the agent puts it behind the same check.

### How does the test know every route?

It registers an `onRoute` hook before the app loads, collects each method and URL, and compares them with the `paths` in `/documentation/json` after converting `:param` to `{param}`. A route added later without a schema in the document fails the test with its method and path.
