Guides
File-based APIs
Export HTTP methods from robodev/api/*.ts. The filename is the path.
Each file under robodev/api/ becomes a route. robodev/api/planets.ts serves /api/planets. Nested folders work the same way: robodev/api/v1/items.ts serves /api/v1/items. Bracket segments become path params: robodev/api/invoices/[id].ts serves /api/invoices/:id and sets ctx.params.id. Exact files win over parameterized ones.
Export get, post, put, patch, or delete as defineApi(...) handlers.
Helper files and bundling
A file under robodev/api/ with any path segment that starts with _ is a helper module, not a route: robodev/api/_lib.ts, everything under robodev/api/_lib/, and robodev/api/users/_shared.ts. Import helpers from routes, jobs, sockets, hooks, and MCP tools. A helper gets no route and no bundle of its own. It is compiled only into the code that imports it, and it is still deployed as source, so robodev load returns it.
If a _ file still exports a handler (get, post, put, patch, del, or delete, including through export { … } or export * from), it stays a route at the same path, and the deploy succeeds with a warning: Warning: api/_health.ts exports get, but files starting with "_" are helper modules. Rename it (for example api/health.ts) to keep it a route; it is still served for now. robodev deploy, the deploy log, and robodev dev (on load and on every reload) print it.
The backend is bundled with code splitting. Code that several routes, jobs, sockets, or hooks share (helpers, robodev/database.ts tables, and bundled npm packages) is emitted once in a shared chunk instead of being copied into every bundle, and each dynamic await import() becomes a chunk that loads on first use.
Module-level state in a shared helper is one instance per process. A const cache = new Map() in robodev/api/_lib.ts is the same Map for every route, job, socket, and hook that imports it. Before code splitting, each route bundle had its own copy. A deploy starts a fresh copy.
Handler context
- db — Drizzle client for the project's tenant database.
- email — injected project email client. Call email.send from the handler. See Email (/docs/email).
- llm — injected BYOK LLM client. Call llm.complete or llm.stream. See LLM (/docs/llm).
- agent — injected tenant agent client. Call agent.createSession and agent.start (enqueue, does not wait). See Agents (/docs/agents).
- storage — injected project file storage. Call storage.upload / get / getUrl / delete / list. See Storage (/docs/storage).
- env — project secrets as plaintext strings. See Secrets (/docs/secrets).
- params — path parameters from robodev/api/invoices/[id].ts.
- query — parsed query string when you pass query.
- body — parsed JSON body when you pass body. Text fields for multipart.
- headers — incoming request headers.
- user — the signed-in project user, or null. Set auth on the definition.
- rawBody — Buffer when defineApi({ rawBody: true }).
- files — uploaded files when defineApi({ multipart: true }).
- push — FCM client. See Push (/docs/push).
- jobs — enqueue durable jobs. See Jobs (/docs/jobs).
- pdf — HTML to PDF. Call pdf.fromHtml. Set timeoutMs: 25_000 when you use it. See PDF (/docs/pdf).
Responses
Return a plain JSON value for HTTP 200, or an envelope { status?, headers?, contentType?, body }. A string body is text/plain, Buffer/Uint8Array is bytes, and { base64 } decodes to bytes. Handlers default to a 10s timeout (max 30s) via timeoutMs.
rawBody and multipart
- rawBody: true — ctx.rawBody is the raw Buffer. JSON and urlencoded still parse into ctx.body. octet-stream stays unparsed. Not for multipart.
- multipart: true — ctx.files[] with Buffer data (1 GiB request limit). Text fields land in ctx.body. rawBody + multipart is a deploy error.
auth on defineApi
Default is false (public). Use "required" or "optional" for Robodev Auth, or a custom verifier that reads headers and returns { id, email, name }. Required or a null custom verifier returns 401 and does not call the handler.
Reserved project-host routes win over api/user.ts and api/user/** (Auth), and over api/storage/objects.ts and api/storage/objects/** (Storage serve). api/storage.ts registers as /api/storage. Only GET /storage/objects/{key} is reserved for serving files. defineApi handlers are served at /api/.... How-to: Auth (/docs/auth) and Storage (/docs/storage).
businessLogic and acl
Optional OpenAPI metadata. businessLogic becomes x-bl, a description of what the endpoint does. acl is a list of subject:action strings, such as planet:update. Each one becomes an x-acl object { action, subject } with a PascalCase subject (planet:update → { action: "update", subject: "Planet" }). When acl is set, the operation also documents 401 Unauthorized and 403 Forbidden. Neither field authorizes the request; auth still does that. A malformed acl entry fails deploy. Set mcp: true to also expose the route as a tool on the project MCP endpoint; see MCP tools (/docs/mcp).
Pagination
Set paginated: true on a GET-only defineApi whose file path contains /paginate (for example robodev/api/planets/paginate.ts → GET /api/planets/paginate). Required: filter (Zod object, optional/nullish on the wire), sortable (Zod enum), and response (the item schema, not the envelope). Do not pass query, body, rawBody, or multipart. Non-GET, a path without /paginate, missing filter/sortable/response, or those forbidden fields fails load/deploy with invalid_api_definition. Omit paginated or set it false to leave defineApi unchanged.
The runtime builds the query schema:
- page — positive int, default 1.
- limit — positive number, max 20, default 20.
- cursor — optional field name or field:value matching /^[A-Za-z_][A-Za-z0-9_]*(?::.+)?$/. A page number like "1" is not a valid request cursor.
- filter — optional nested object (OpenAPI deepObject, for example filter[alienId]=...).
- order — optional comma-separated tokens with an optional leading + or -. Empty is valid. Keys must be in sortable. Invalid query is 400.
The handler owns SQL limit/offset and returns { items, page, cursor, nextCursor, limit, totalItems }. Response cursor is conventionally String(page). limit and totalItems are required numbers. The runtime validates the envelope and does not slice; an invalid return is 500 invalid_response.
OpenAPI names
By default every schema is inlined where it is used, and each operation's operationId is <method>_<path> (for example get__orders). Two opt-in options give generated clients stable names. Both need @robodev-ai/sdk 0.19.0 or later. Without them, /openapi.json does not change.
- namedSchema("OrderStatus", schema) emits the schema once under components.schemas.OrderStatus. Every use site (body, query parameter, paginated filter, response, or nested in another schema) becomes { "$ref": "#/components/schemas/OrderStatus" }. Validation does not change.
- Call namedSchema last. .describe(), .default(), .extend() and similar return a new schema without the name. Using a named schema in .optional(), z.array(), an object property, .refine() or .default() keeps the $ref.
- Some wrappers inline the named schema instead. .nullable() on a named plain z.string(), z.number() or z.boolean() is inlined; .nullable() on a named enum, literal or object keeps the $ref. A z.union whose members are all named enums, all named literals or all named primitives collapses into one inline schema.
- A named schema can contain other named schemas; each becomes its own component.
- operationId: "listOrders" on defineApi replaces the default operationId. Generated client method and DTO names follow it.
The project load checks names. A deploy fails with a 400 naming the file:
- invalid_schema_name: a namedSchema name must match ^[A-Za-z][A-Za-z0-9_]{0,99}$.
- reserved_schema_name: the name is a built-in auth component, such as UserMeResponse or AuthnTokenResponse.
- schema_name_conflict: two different schemas use the same name. The message names both files. The same name on schemas with identical output is fine, even when they are separate zod objects.
- invalid_operation_id: an operationId must match ^[A-Za-z_][A-Za-z0-9_]{0,99}$.
- duplicate_operation_id: the operationId is already used by another route (custom or default) or by a built-in operation, such as UserAuthLocalPasswordController_login or storage_get_object.
A restart of an already-deployed project only logs these as warnings. It inlines the schema at the use site that has the problem, and the route keeps its default operationId.
Swagger
After deploy, open /_robodev/docs on the project host for the generated OpenAPI. Swagger shows x-bl and x-acl when a route sets businessLogic or acl. API-only projects also keep /docs and /swagger as Swagger. The APIs page links to that project-host Swagger. The dashboard also has Auth, Email, Push, Jobs, Storage, Settings (Secrets), Connect (Lovable / Bolt), and Build. Request logs live on APIs → Logs. The Databases browser groups tables by Postgres schema (public, robodev_auth, and others), shows an ER diagram of live tables and foreign keys, and supports row search and filters.