> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oncoeverything.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Web app and API

> How the Next.js app talks to the API, and how sign-in works across the two.

## The two services

The web app (`apps/web`) renders pages and runs server actions. The API (`apps/api`) owns the database, sign-in, files, email and payments.

* **Server to server.** Server components and server actions call the API through the client in `apps/web/lib/api.ts`. It forwards the visitor's cookies and address, so the API can identify the caller.
* **Browser to API.** A few admin screens call tRPC from the browser. They go through `/api/trpc` on the site, which the site passes to the API.
* **Type only.** The web app imports the API's `AppRouter` type and nothing else. No API runtime code, and no database client, can reach the web bundle.

The routers are in `apps/api/src/routers`, one file per area: requests, quotes, reports, deliveries, invoices, organizations, users and the rest.

## Rewrites on the site

`apps/web/next.config.ts` passes three paths to the API. The browser sees only the site's own address, so the session cookie is first-party.

| Site path | API path | Used for |
| - | - | - |
| `/api/auth/*` | `/api/auth/*` | Sign-in, sign-out, Google callback |
| `/api/trpc/*` | `/trpc/*` | The admin's browser-side calls |
| `/api/identify` | `/identify` | **Identify a file** in Deliveries |
| `/api/v1/*` | `/v1/*` | The public REST API |
| `/openapi.json` | `/openapi.json` | The public API's OpenAPI description |
| `/mcp` | `/mcp` | The MCP server |

## The API's routes

| Path | What |
| - | - |
| `/trpc/*` | Every data call |
| `/api/auth/*` | Better Auth |
| `/health` | The load balancer's health check |
| `/events` | First-party analytics beacons |
| `/webhooks/stripe` | Stripe's payment events |
| `/identify` | Reads the copy number from a PDF |
| `/v1/*`, `/openapi.json` | The public REST API and its description |
| `/mcp` | The MCP server |
| `/internal/deliveries/run`, `/wake` | Makes report copies. Needs the runner key. |

In production the API answers on its CloudFront address. It has no custom domain yet.

## The public API and MCP server

A read-only API over the report catalog, for other people's code and AI agents. No key is needed. The public page is [oncoeverything.com/developers](https://www.oncoeverything.com/developers).

* **REST.** A tRPC procedure joins the REST API when it sets `meta.openapi`, through [trpc-to-openapi](https://github.com/mcampa/trpc-to-openapi). Today that is five catalog reads: search, one report, a report's sample, the topics, and one topic. The handler is `apps/api/src/public-api.ts`.
* **Output schemas are the contract.** Each public procedure declares `.output()`. The OpenAPI document is generated from them, and the site's own calls go through the same schemas. A field removed from a schema is removed from the API, so treat a change there as a change to a published API.
* **MCP.** `apps/api/src/mcp.ts` offers the same reads as five tools over Streamable HTTP. It is stateless: each request builds its own server and answers plain JSON.
* **Never signed in.** Both run with no session, whatever cookie a browser sends.
* **Rate limit.** The Vercel firewall allows 120 requests a minute per IP on these paths (`infra/vercel.tf`). Past that it answers 429.

To add an endpoint, give a public procedure a `.meta({ openapi: { method: "GET", path: "/…" } })` and an `.output()` schema, and add a test to `apps/api/src/public-api.test.ts`. The directory data gets its endpoints this way when it moves into the database.

## The staff MCP server

`/mcp/admin` gives Claude the admin's reads and two safe writes, for staff: a note on a request, and a blog draft from Markdown (`posts.createDraft`, `apps/api/src/blog/markdown.ts`). A person publishes the draft in the admin. Setup for staff is in [Use Claude with the admin](https://docs.oncoeverything.com/start/claude.md). The code is `apps/api/src/staff-mcp.ts`.

* **OAuth 2.1.** Better Auth's MCP plugin (`@better-auth/mcp`, in `apps/api/src/auth.ts`) makes the platform an OAuth server. Claude registers itself by dynamic client registration, signs the person in on `/sign-in`, and asks them to approve it on `/connect`. Tokens are signed JWTs bound to the `/mcp/admin` address, with the `staff` scope.
* **Discovery** is at `/.well-known/oauth-protected-resource/mcp/admin` and `/.well-known/oauth-authorization-server/api/auth`. The site passes both to the API.
* **Three checks on every call.** The token (signature, issuer, audience, expiry, scope). The person (an active owner, read from the database). The connection (their consent for that app still exists). A disconnect on **Connect Claude** therefore stops the app at once.
* **Tools reuse the admin's procedures.** Each tool calls an owner-only tRPC procedure as the signed-in person and takes that procedure's input schema. Nothing sends email.
* **Only owners can approve.** A hook on `/oauth2/consent` refuses anyone else.
* **Sign-in resumes the request.** After any sign-in, `/oauth/resume` carries the person back to the authorization request (`apps/web/lib/oauth-resume.ts`).
* **Migrations first.** The OAuth provider writes to its tables as Better Auth starts, so `apps/api/src/index.ts` runs migrations before it loads the server.

To add a tool, add a row to `STAFF_TOOLS` naming an owner procedure, and a case to `apps/api/src/staff-mcp.test.ts` if it writes.

## Cached pages

The public pages that show reports are static. The Vercel CDN serves them, and the site does not render them again for each visitor.

* **Cached reads.** `apps/web/lib/catalog.ts` reads the catalog through its own client. The client sends no cookies, because the catalog is the same for everyone. Next.js caches each answer under the tag `catalog`.
* **Change notices.** After each change to a report, the API posts to `/api/revalidate` on the site (`apps/api/src/site-revalidate.ts`). The API also posts when it starts. The request carries `SITE_REVALIDATE_KEY`, which Terraform generates.
* **Stale, then new.** The notice marks the cached pages stale. The first visit after a change gets the old page and starts a render. The next visit gets the new page.
* **Backstop.** Each page also refreshes every five minutes. This covers a change that the API did not report, such as a direct database edit.
* **API down.** A page keeps its last good copy. A build with no API makes pages that say the catalog is unavailable. They refresh within five minutes after the API is back.
* **Per request.** The report search page (`/segments/market/reports`) and checkout render for each visit, because they depend on the query string or the buyer. Their catalog reads still come from the cache.

To add a public page that shows catalog data, read it through `lib/catalog.ts` and export `revalidate = 300` from the page.

## Markdown for agents

Every public page also answers in Markdown: at its `.md` address, when a request sends `Accept: text/markdown`, or when a known AI agent asks without naming a format. `apps/web/proxy.ts` decides, and `app/markdown` converts the page's own HTML. `/llms.txt` indexes the site for agents.

## Sign-in

[Better Auth](https://www.better-auth.com) runs inside the API. Its settings are in `apps/api/src/auth.ts`.

* **Emailed link.** This is the main sign-in method. A link works once and expires in 10 minutes.
* **Google.** Google sign-in shows only when the Google client ID and secret are in the app secret.
* **Password.** Password sign-in is on, with a 12-character minimum. Only the `create-owner` script sets one, for the owner. There is no reset page yet, so everyone else signs in with a link or Google.
* **Sessions** last seven days and renew once a day with use. Sessions are rows in Postgres.
* **Sign-up** through Better Auth is closed. Accounts come from the site's own sign-up form, an invitation, an accepted quote or the admin.

The owner account is made at boot from `OWNER_EMAIL`. Changing it later adds another owner. It never changes an existing account.

## Roles

| Role | Where it applies |
| - | - |
| `owner` | Staff. Sees and changes everything in the admin. |
| `org_admin` | A buyer company's admin. Can invite colleagues. |
| `org_member` | A buyer company's member |

Each tRPC procedure checks the role. Buyer procedures filter by the caller's organization. A CI test against a real Postgres database checks that one organization cannot read the requests of another.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.