Server
createLatchApp and createLatchHandler from @intentface/latch-server, every option, and the routes they serve.
@intentface/latch-server serves the runtime over HTTP. What it's for: Server. Mounting it in your framework: Mount in your framework.
createLatchApp
Builds the runtime with sensible defaults (crash recovery, schedules, the model catalog, built-in tools) and the handler over it. Returns { runtime, handler, models, stop }.
const { runtime, handler } = await createLatchApp<Principal>({ storage, agents, resolveContext });| Option | Type | Default | Details |
|---|---|---|---|
storage | StorageAdapter<P> | — | |
agents | union | — | |
resolveContext | (request: Request) => P | Promise<P | null> | null | — | |
context? | ContextInput<P, RC> | `{}` | |
models? | ModelCatalog | `loadModelCatalog()` | |
principalKey? | ((principal: P) => string) | — | |
runtime? | Partial<Omit<RuntimeConfig<P, RC>, "agents" | "context" | "storage">> | — | |
handler? | Partial<Omit<LatchHandlerConfig<P>, "resolveContext" | "runtime">> | — | |
cronIntervalMs? | number | false | 30000 on a long-running server |
createLatchHandler
The handler on its own, over a runtime you built. Returns (request) => Promise<Response | undefined>: undefined means the path isn't one of its routes.
const handler = createLatchHandler<Principal>({ runtime, resolveContext, basePath: "/api/latch" });| Option | Type | Default | Details |
|---|---|---|---|
runtime | union | — | |
resolveContext | (request: Request) => P | null | Promise<P | null> | — | |
reconstructPrincipal? | function | — | |
oauthFlowMaxAgeMs? | number | 30 minutes | |
connections? | union | — | |
onConnected? | ((principal: P, connectionName: string) => void | Promise<void>) | — | |
onError? | function | — | |
connectedParam? | string | "connected" | |
basePath? | string | "/" | |
cronSecret? | string | — | |
cronRun? | (() => Promise<unknown>) | `runtime.cron()` |
Routes
Paths are relative to basePath. Every route except the cron route and the connection sign-in callback calls resolveContext first. The callback restores the principal that started the sign-in instead (see reconstructPrincipal above).
| Route | What it does |
|---|---|
GET agents | The agents the caller can use, for a picker. |
POST agents/:agent/:chatId | Send a message; streams the answer. Body: { message } or the AI SDK's { messages }. |
POST agents/:agent/:chatId/approvals | Approve or deny paused tools; streams the rest of the answer. |
POST agents/:agent/:chatId/tool-result | Answer a tool the browser handles, such as a question for the user. |
GET chats | The caller's chats. |
GET chats/:chatId | One chat's messages. |
DELETE chats/:chatId | Delete the chat with its messages, runs and usage, and the subagent threads it spawned. 404 if it isn't the caller's, 409 while an answer runs, 501 if storage can't delete. |
GET runs | The caller's runs, with usage and cost. |
GET schedules, POST schedules | List or create the caller's schedules. |
GET schedules/:id, DELETE schedules/:id | Read or delete one. |
POST schedules/:id/run-now | Fire it on the next cron tick. |
GET connections | The services the caller can connect, and which are connected. |
GET connections/:name/authorize | Start signing in; redirects to the provider. |
GET connections/:name/callback | Where the provider sends the user back. |
POST connections/:name/disconnect | Remove the caller's credentials. |
POST connections/:name/test | Check the connection works; returns its tool names. |
GET / POST cron/run | Your cronRun, or by default runtime.cron() for everyone (with a runtime per customer and no cronRun, it answers 400). Needs cronSecret, as a bearer token or x-latch-cron-secret, so Vercel Cron can call it. |
Refusals come back as JSON with a code: see Errors.