Skip to content

API Referencecourier.config.ts

courier.config.ts

Every field of courier.config.ts: the sources, the API imports Studio keeps in sync, Studio, the standalone server, the runtime and what agents may call.

8 min readUpdated both modes

Overview

courier.config.ts, at the project root, names the project's API sources and what the tools around them need. defineConfig types it:

courier.config.ts
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.

FieldType
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
fromstringurl 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
pinHashstringurl only: the one specification the import is generated from, by the hash Studio records; another is refused
outputDirstringThe directory that holds the import's own directory, <outputDir>/<source-name>/
sourceNamestringThe 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, generateSchemasbooleanWhether 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
addAuthMiddlewarebooleanWhether the specification's security scheme becomes middleware
includeCommentsbooleanWhether 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 | voidRun 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.

FieldDefault
port6280The port Studio serves on; --port overrides it
host"localhost"The address it binds; --host overrides it
mockBasePort7001Where the ports Studio assigns to mocks that name none begin
openfalseOpen 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.

FieldDefault
port3001--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
maxBodyBytes1 MBThe largest JSON envelope the proxy reads; a larger one is refused with 413
maxUploadBytes10 MBThe largest multipart/form-data upload the proxy reads

runtime

What attachRuntime() needs in the application's process.

Field
appOriginThe 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 dev records as it starts listening, https only 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.

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