Latch
The backend around your AI agent. Saved conversations, approvals, crash recovery, per-customer isolation, usage limits and chat apps, inside your own server and database.
The Vercel AI SDK gives you the model call and the tool loop. Latch is the backend around it: everything an agent needs once real customers use it. Conversations that are saved, risky actions that wait for a person, answers that survive a crashed server, customers who never see each other's data, usage limits, and the same agent in Slack.
It's a library, not a service. It runs inside your own server, on the Postgres or SQLite you already have, behind your own login.
What it looks like
An agent that helps customers with their orders, and asks before refunding:
import { anthropic } from "@ai-sdk/anthropic";
import { createRuntime, defineAgent } from "@intentface/latch-core";
import { createPostgresAdapter } from "@intentface/latch-drizzle";
import { context, find_order, refund } from "./tools";
// Who is asking. Ids only: it's saved with every run and schedule.
// Anything you look up (plan, names, clients) goes in the context.
export type Principal = { orgId: string; userId: string };
export const runtime = createRuntime<Principal>({
// Every conversation belongs to one person in one organization.
storage: createPostgresAdapter(db, {
owner: (p) => [p.orgId, p.userId],
}),
// What tools receive on each turn (see the Tools tab).
context,
agents: {
support: defineAgent({
model: anthropic("claude-sonnet-4-6"),
instructions: "You help customers with their orders.",
tools: { find_order, refund },
// A person clicks Approve before a refund runs.
toolApproval: { refund: "user-approval" },
}),
},
// A crashed answer is finished by another server.
durability: { enabled: true },
});import type { ToolContext } from "@intentface/latch-core";
import { tool } from "ai";
import { z } from "zod";
import type { Principal } from "./latch";
// Your own data access, bound to one organization. A tool gets this
// object, never an org id it could change, so it can't reach another
// customer's orders.
const ordersFor = (orgId: string) => ({
get: (orderId: string) => db.orders.findOne({ orgId, orderId }),
refund: (orderId: string, requestedBy: string) =>
payments.refund({ orgId, orderId, requestedBy }),
});
// Built at the start of every turn, from the principal your route passed.
export const context = ({ principal }: { principal: Principal }) => ({
orders: ordersFor(principal.orgId),
});
// What a tool receives: what `context` returned, plus the principal
// itself, which Latch adds for you.
type Ctx = ToolContext<Principal, ReturnType<typeof context>>;
export const find_order = tool({
description: "Look up an order",
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }, { context }) =>
(context as Ctx).orders.get(orderId),
});
export const refund = tool({
description: "Refund an order",
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }, { context }) => {
const { orders, principal } = context as Ctx;
return orders.refund(orderId, principal.userId);
},
});import { runtime } from "@/lib/latch";
export async function POST(req: Request) {
const user = await requireUser(req); // your login, not ours
const { id, message } = await req.json();
return runtime.handleChat({
agent: "support",
chatId: id,
// Who is asking. Latch carries it everywhere from here.
principal: { orgId: user.orgId, userId: user.id },
message,
});
}// The AI SDK's own useChat, approvals included. Nothing Latch-specific.
const { messages, sendMessage, addToolApprovalResponse } = useChat({
id: chatId,
transport: new DefaultChatTransport({
api: "/api/chat",
prepareSendMessagesRequest: ({ id, messages }) => ({ body: { id, message: messages.at(-1) } }),
}),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});That's the whole integration. The conversation is saved, the refund waits for a click (today, tomorrow, from another device), and if a server dies mid-answer, another one finishes it.
The principal you pass once is what keeps customers apart. Storage scopes every read and write by it, every tool receives it through the context, your limits are keyed from it, connected accounts are stored under it, and it's saved with each run and schedule, so a resumed answer or a 9:00 report still acts as the right person. More in Principal, context and params.
What you get
- Conversations that are saved, safe from two tabs, retries and forged messages.
- Approvals that work tomorrow, on another device, from Slack, and only once.
- Answers that survive a crash, on one server or fifty.
- Customers who never see each other's data, including in runs nobody is logged in for.
- Usage limits per organization, user and agent, that simultaneous requests can't slip past.
- Files and images handled without bloating your database or leaking requests.
Plus schedules, connections to other services (MCP, OAuth), subagents, Slack, Telegram and WhatsApp, and cost per answer.
Build with your coding agent
Install the Latch skill and your agent (Claude Code, Cursor, Codex and more) gets the recipes, the rules that break quietly, and these docs:
npx skills add https://intentface-latch-docs.vercel.appMore, including llms.txt for agents that read the web, in Build with AI.
When not to use it
If you're building a single-user prototype that doesn't need to remember anything, the AI SDK alone is the better tool. Latch earns its place when a second customer, a second server or a second channel shows up.