Drop an agent into your SaaS,
in 5 minutes.
Your REST API is the toolset. The agent acts on behalf of your user. The agent is a folder in your repo.
Screen 2 · The folder
Your agent is files in your repo.
Standard files. Standard formats. Edited by Claude Code, reviewed in PRs, deployed by CLI. The source of truth is your repo, not a vendor database.
tavora/ ├── tavora.jsonc # project manifest ├── agents/ │ └── copilot/ │ ├── agent.jsonc # model · capabilities · skills · evals │ ├── persona.md # system prompt │ ├── skills/ │ │ └── board/main.js # require()-able module · your REST adapter │ └── evals/ │ └── basic.json # { input, criteria, pass_threshold } └── .runs/ # session logs — gitignored
Screen 3 · How it works
One data path. No surprises.
Your user clicks the chat. The agent's plan is JavaScript you can read. Calls go out as the user, against your existing API. That's the whole loop.
User signed into your app · holds a JWT
<TavoraChat /> React component in your UI
Tavora sandbox LLM emits JS · runs in a JS sandbox · trace on disk
adapter.js your code · in your repo · ~20 lines
Your REST API PocketBase / Supabase / your own Go
The agent can only do what the user can already do. No new permission surface, no new audit trail.
"Agents that reason by writing code" is the implementation that makes the rest of this readable, debuggable, and replayable.
Multi-step agents your users
can actually rely on.
Each one is a thin .js adapter over your existing API. Cursor and Claude Code write these in minutes from your OpenAPI spec.
support.js Triage, route, and resolve tier-1 tickets
Classify priority, look up the customer in your CRM, draft a reply with citations, escalate to oncall when stuck.
getTicket(id) { return req('GET', '/tickets/' + id);},replyToTicket(id, body) { return req('POST', '/tickets/' + id + '/reply', { body });},
refunds.js Refund eligibility & approval flows
Check the policy matrix, run the calculation, request human approval over a threshold.
getOrder(id) { return req('GET', '/orders/' + id);},requestApproval(amount, reason) { return req('POST', '/approvals', { amount, reason });},
project.js Guided project setup
Reads the user's data, asks clarifying questions, calls your APIs to make changes — with confirmations.
getProject(id) { return req('GET', '/projects/' + id);},updateProject(id, patch) { return req('PATCH', '/projects/' + id, patch);},
analytics.js Natural-language queries on your data
A user asks in plain English; the agent writes the query against your schema, executes it, renders the result inline.
describeSchema() { return req('GET', '/schema');},runQuery(sql) { return req('POST', '/query', { sql });},
Each adapter file is ~20 lines on a real API. The JWT lives in the egress shim, not in this code.
See all 10 patterns across 4 bucketsScreen 5 · The eval story
Promotion is gated by your tests.
Evals are JSON files in the same folder. tavora dev runs them on every save. Behavior changes are reviewed like code changes — diffable in a PR, blocking a deploy when they regress.
{
"name": "adds a card to the named list",
"input": "Add a card called \"order coffee\" to Todo.",
"criteria": "Trace contains a single board.createCard call with listId 'l_todo' and title 'order coffee'. No other writes.",
"pass_threshold": 7
} $ tavora dev⠋ syncing draft… synced (sha256:800b43c…)⠋ running 4 eval cases against the draft ✓ basic 9.1 ✓ adds a card to the named list 8.4 ✗ moves a card across lists 4.2 ← below 7.0 ✓ refuses to delete without yes 9.6 3 / 4 passing · log: .runs/2026-05-22T08-30-12Z-…
$ tavora dev⠋ syncing draft… synced (sha256:9c4f17b…)⠋ running 4 eval cases against the draft ✓ basic 9.1 ✓ adds a card to the named list 8.4 ✓ moves a card across lists 8.7 ✓ refuses to delete without yes 9.6 4 / 4 passing · ready to deploy
No bolt-on eval product. No "set up tracing later." The pass/fail signal is on disk, in the same loop the AI coding tool already uses.
Screen 6 · Security
The agent calls your backend as the user.
The sandbox attaches the user's JWT at the egress shim. The adapter JS never sees it, can't log it, can't leak it. Your backend authorizes the call the same way it authorizes a normal request from the user's browser.
// agents/copilot/agent.jsonc
"fetchPolicies": [
{
"origin": "${env.BACKEND_URL}",
"headers": {
"Authorization": "Bearer ${session.jwt}"
}
}
] - JWT lives in the egress shim
Templates in agent.jsonc — never in the .js code, never in a vault you have to rotate. The session passes the user's token; the runtime stamps the header.
- Tenant scoping is a prop
Pass the tenant on <TavoraChat tenant={orgId} />. The runtime makes the session carry it; your backend filters by it. Same agent, every tenant, scoped by the call.
- No new permission surface
The agent can only do what the user can already do. No service account to audit. No "agent role" your security team has to model.
- Hardened JS sandbox
No filesystem, no subprocess, no arbitrary network. Memory and time are capped. Outbound fetches go through policies, not raw fetch.
"The agent can only do what the user can do" is a one-line answer to the scariest question a CTO will ask.
Your stack
Tavora ♥ your existing backend.
We don't replace your CRUD. We run the agent on top of whatever you've already got — your REST API is the toolset, your auth is the auth, your DB stays your DB.
Not on the list? If it speaks HTTP and JSON, it works.
Drop an agent into your SaaS, in 5 minutes.
Early access — limited spots while we onboard the first wave. Tell us about your app or grab a 30-minute slot.