Your backend can call the model now — and there's no key to leak
We wanted one small thing: our ship button — the vibe our team presses to deploy the platform — should tell you what's about to ship. Gather the commit messages that haven't deployed yet, ask a model to compress them into two honest sentences, and pin that above the button.
The frontend could almost do it. Vibes have had callAI in the browser forever. But this summary isn't a private answer for one viewer — it's a fact about the deploy, the same for everyone, written once and readable by anyone who walks past the button. That's a server-side job. And until this week, a vibe's server side — backend.js — had a database, secrets, webhooks, and a cron lane… but no way to think.
Now it does:
jsexport async function onChange(event, ctx) {
if (event.dbName !== "shipper" || event.doc.type !== "whatsnext" || event.doc.summary) return;
const summary = await ctx.callAI(
`These commits are merged but not deployed:\n${event.doc.commits.join("\n")}\n` +
`In two sentences, tell the team what this deploy will change.`,
{ max_tokens: 300 },
);
await ctx.db.put({ ...event.doc, summary });
}
Text in, text out, one await. That's the whole API.
There is no key. Anywhere.
The usual price of "my backend calls an LLM" is key management: provision an API key, store it, rotate it, and hope no code path ever logs it. ctx.callAI deletes the whole category. Your backend.js runs in an isolated worker with no worker env bindings and no platform credential — the only secrets it ever sees are the ones you explicitly set for your own app (ctx.secrets). When it calls ctx.callAI, the request travels over the same nonce-gated capability channel as ctx.db: an opaque, single-invocation token that the host resolves back to this call, from this vibe, triggered by this user. The platform's model credential lives on the other side of that boundary and never crosses it.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
Like a switchboard operator: you don't get the trunk line. You ask, the operator patches you through, and the exchange keeps its own books.
The books: billed to whoever pressed the button
Those books are the interesting design decision. Every ctx.callAI call meters into the same AI-credits ledger as the chat that built your app — and it's attributed to the user who triggered the handler. The person whose write fired your onChange. The signed-in caller of your /_api endpoint. Automation your users trigger is spend your users own. Only when there's no user at all — a cron tick, an anonymous webhook — does the bill fall to the app's owner, because at that point the app is acting on its own behalf (PR #3268 has the full rule).
There are guardrails where you'd want them — a handful of calls per invocation, a prompt-size cap, clamped output length — but the real spend control is that the meter points at a person who chose to press something.
What server-side AI is for
Browser callAI answers a question for one viewer. ctx.callAI produces facts the whole app trusts: results users can't forge, computed when nobody's watching. Each backend lane has its own natural uses.
When data changes (onChange) — react to every committed write:
- Moderation with teeth. Score each post as it lands; quarantine before other users ever sync it. The client can't skip a check it never runs.
- Auto-tagging and filing. Classify notes, expenses, tickets into the categories your access rules route on.
- Rolling summaries. Keep a pinned "what happened in this thread" doc that updates as messages arrive.
- Extraction from paste. Someone pastes a wall of free text; the backend writes back the structured fields — dates, amounts, names.
- Triage. New support ticket → urgency score + suggested assignee, written server-side where the ranking can't be gamed.
- Translation fan-out. A post commits in one language; translated sibling docs appear for every channel that wants them.
- Narration. Turn a game move into a dungeon-master sentence; turn a workout log into a coach's one-liner.
- Deploy previews. Our origin story: commit messages in, "here's what this ship changes" out.
On request (fetch) — your app's /_api endpoint, reachable by webhooks and your own frontend:
- Inbound email and webhook triage. A form service or mail hook POSTs raw payload; the handler summarizes, classifies, and files it as documents.
- A bot with server memory. A support endpoint that reads the app's own docs (
ctx.db.query), asks the model with that context, and answers — RAG where the retrieval is your Fireproof database. - Lead enrichment. Signup form → one call → fit score and a drafted follow-up, stored before a human looks.
- Feed digestion. POST a week of GitHub webhook noise; get back the three sentences worth reading.
On a timer (scheduled) — the cron lane, running as the app owner:
- The nightly digest. Read yesterday's activity, write this morning's summary doc. Every team app wants this; now it's ~ten lines.
- Leaderboard color commentary. Weekly standings plus one paragraph of trash talk, regenerated every Sunday.
- Data hygiene. Sweep for near-duplicate entries and stale tasks; write suggestions a human approves.
- Rotating content. Tonight's trivia questions, this week's writing prompt, tomorrow's quest — generated while everyone sleeps.
- The standup writer. Summarize what changed across the workspace into one doc before the team logs on.
The pattern behind most of these is the one the ship button uses, and it's worth naming: button → request doc → onChange → ctx.callAI → write-back. The UI writes a small doc describing what it wants; the backend notices, thinks, and writes the answer next to it; every viewer's live query picks it up. No endpoint to poll, no loading-state plumbing — the database is the request/response channel, and the AI step is gated behind a deliberate user action instead of firing on every page load.
The fine print that keeps it honest
v1 is deliberately plain: a string prompt in, completion text out. No streaming, no tool use. Want structure? Ask for JSON in the prompt and parse defensively — a failed call throws, so catch it and write an error field back onto the doc rather than losing the event (our ship button shows the failure in amber instead of pretending nothing happened). The full contract lives in the backend.js docs — which means the code generator reads it too: you can just ask for "a backend that summarizes new entries every night" and the builder knows how to wire it.
One await for your code. A switchboard, a meter, and a locked exchange behind it.
Give your app a backend that thinks
Describe it — "moderate new posts", "write me a nightly digest" — and the builder wires ctx.callAI for you.
Start building →