Overview
courier.config.ts, at the project root, names the project's API sources and what the tools around them need. defineConfig types it:
import { defineConfig } from "@noverax/courier/config"
import { api } from "./api/sources"
export default defineConfig({
sources: { api },
studio: { port: 6280 },
server: { allowedOrigins: ["https://app.example.com"] },
runtime: { appOrigin: "http://localhost:3000" },
})Studio reads it, and so do the Studio runtime (attachRuntime()) and the standalone server (courier-server) — both of which run in production. So the file imports nothing meant only for development: mock servers are declared in courier.mocks.ts beside it, which only Studio reads — see Mock Servers.
The file may also be named courier.config.mts, .cts, .js, .mjs or .cjs, looked for in that order after .ts. How Studio Loads the Project says which files Studio watches and how a change reloads them.
sources
Required. The sources the project serves and calls, by the name its callers use: sources: { api } is reached through the proxy as createProxyCaller(apiRoutes), from the routes Courier writes for it. A project without an API yet starts with sources: {}.
imports
The API specifications Studio imported, by key. Studio writes an entry when it imports a specification and reads it on every re-sync, so an entry is usually Studio's to write; editing one by hand is supported.
| Field | Type | |
|---|---|---|
type | "openapi" | "swagger" | "graphql" | "asyncapi" | "postman" | The specification's format, which picks its parser |
importType | "url" | "file" | "raw" | Where it came from. A file's path and a pasted specification are supplied in Studio at each re-sync, never stored |
from | string | url only: the address Studio fetches it from |
auth | { type: "bearer", token } | { type: "basic", username, password } | { type: "header", headers } | url only: sent with every fetch of from, to its own origin only |
pinHash | string | url only: the one specification the import is generated from, by the hash Studio records; another is refused |
outputDir | string | The directory that holds the import's own directory, <outputDir>/<source-name>/ |
sourceName | string | The source's name in the generated defineApi call — default: the key |
validation | "zod" | "valibot" | "arktype" | "none" | The schema library the generated code uses. With ArkType, a reference cycle of more than about 40 schemas — Stripe's core objects form one of 83 — goes past TypeScript's depth limit where an endpoint answers with one of them (TS2321); Zod and Valibot type such a specification |
groupBy | "tag" | "path" | "single" | How endpoints are grouped: by tag, by the resource their path names, or in no group at all. An endpoint with no tag, or with no segment of its path to group by, sits in the API itself, beside the groups — see Import an API |
generateTypes, generateSchemas | boolean | Whether types and schemas are written |
generateClient | "direct" | "proxy" | "both" | "none" | Which callers are generated: the direct caller in the import's index.ts, the proxy caller in proxy.ts, which browser code imports |
addAuthMiddleware | boolean | Whether the specification's security scheme becomes middleware |
includeComments | boolean | Whether descriptions become doc comments. Each endpoint keeps its operation's description as .meta({ description }) either way: an agent's tool describes itself by it, and the OpenAPI written from the definition keeps it |
include, exclude | { tags?, paths?, operationIds? } | The endpoints generated: include names the ones kept, then exclude the ones left out |
transform | (spec) => spec | void | Run on a copy of the parsed specification before anything is generated from it |
See Import an API.
studio
What Studio reads: port, host, mockBasePort and open when it starts, allowedSpecHosts whenever a fetch would reach a private host, and routes each time the config loads. A value Studio cannot use — a port that is not a whole number from 1 to 65535, an empty host, an open that is not true or false, a routes path outside the project — fails the config, and its error names the field.
| Field | Default | |
|---|---|---|
port | 6280 | The port Studio serves on; --port overrides it |
host | "localhost" | The address it binds; --host overrides it |
mockBasePort | 7001 | Where the ports Studio assigns to mocks that name none begin |
open | false | Open Studio in the browser when it starts; --open and --no-open override it |
allowedSpecHosts | — | Hosts on a private network Studio may fetch a specification from, such as ["gitlab.internal", "10.0.0.7:8080"]. This machine needs no entry |
routes | "courier.routes.ts" | Where the routes module goes, from the project root: a .ts, .mts, .js or .mjs file in the project, such as "src/courier.routes.ts". Beside a JavaScript config it is courier.routes.js; false keeps the project without one. Studio writes it whenever the config loads with an endpoint marked .expose(), and courier-cli routes without Studio; neither writes over a file Courier did not write |
server
What the standalone server reads when its command-line flags say nothing.
| Field | Default | |
|---|---|---|
port | 3001 | --port overrides it |
host | "127.0.0.1" | --host overrides it; "0.0.0.0" or "::" to accept other machines |
allowedOrigins | — | The origins of the browser apps that call the server, written as a browser sends them. Left out, only the server's own origin; outside production a page on another port of the local machine is let through as well, so Studio can drive the app, but never reads the answers — list the origin of a dev server whose page calls this one. --allowed-origins overrides it |
allowedHosts | — | The host names the server is reached by; a request to another is refused with 421. --allowed-hosts overrides it |
maxBodyBytes | 1 MB | The largest JSON envelope the proxy reads; a larger one is refused with 413 |
maxUploadBytes | 10 MB | The largest multipart/form-data upload the proxy reads |
runtime
What attachRuntime() needs in the application's process.
| Field | |
|---|---|
appOrigin | The origin the application listens on, such as http://localhost:3000, where Studio's Playground sends its proxy calls — requests, streams and channels alike. Looked up in turn: this field, COURIER_APP_URL, APP_URL, then, in development, the address the server tells of itself: see How the app learns its origin. A deployed server never learns one. With none of them, a Playground call in proxy mode fails with APP_ORIGIN_NOT_CONFIGURED |
How the app learns its origin
In development, with no appOrigin, COURIER_APP_URL or APP_URL, the app learns its origin from the server itself, never from a header a request sends, so a reverse proxy, a tunnel, a container or a *.localhost name in front of it needs no setting:
- Next.js, a Hono app inside a Next.js route among them: the address
next devrecords as it starts listening,httpsonly with its own certificate, before any request. - Express, Fastify, NestJS, Hono on Node or Bun, SolidStart: the address and port of the socket the first request to a Courier route arrived on. Until a Courier route has served a request, a Playground call has nowhere to go: open a page that calls your API first.
- The standalone server: the address it listens on, as it starts.
- SvelteKit, Astro, React Router, Nuxt: the address Studio's Vite plugin or Nuxt module sees the dev server start listening on, before any request. Under an app's own server that runs Vite in middleware mode, as React Router's custom server does, the socket of the first request that server hands Vite.
Set COURIER_APP_URL where Studio cannot see the server — Deno, Cloudflare Workers, a server on a UNIX socket, or an app that routes by the name it is reached at — to the address the app's own process reaches its proxy route at: in a container, the container's own address and port, not the port the host publishes; behind a tunnel, the local address, not the public one, which would send the Playground's calls and their credentials through the tunnel's provider.
agents
What the project offers to AI agents as MCP tools. The server in your editor, Studio and courier-studio diff read it here; an application's own MCP endpoint takes it as createMcpHandler({ agents: config.agents }). Left out, the endpoints marked .agent() are offered and nothing else.
| Field | Default | |
|---|---|---|
offer | "marked" | Which endpoints are offered without .agent(): "marked" — none; "reads" — every read; "all" — every endpoint, with the access its verb implies |
allow | — | Patterns of endpoints to offer although offer leaves them out: ["shop.orders.*"] |
deny | — | Patterns of endpoints no agent sees, whatever offers them: ["billing.**"] |
access | "destructive" | The most a tool may do: "read", "write" or "destructive"; an endpoint that does more is offered to no agent |
confirm | "destructive" | From which access a person confirms each call: "destructive", or "write" for every call that changes something |
A pattern names an endpoint by its call under its source; * stands for any part of one name, ** for any number of names. A key or a value the policy does not know stops the MCP server with AGENT_POLICY_INVALID — a misspelt key under agents is named when Studio loads the config, too. See What agents may call.