On this page

#An AI agent for your React app

To add an AI agent to a React app, create the agent from a chat with the Vatio MCP server, then build it into the code with npx @vatio-ai/skills and /vatio-init (steps below). In the app, the agent is the @vatio-ai/widget package, mounted in an effect. To act for signed-in users, the app gets a short-lived token from your backend, which signs it with a key the browser never sees, and your API verifies the same token when the agent calls it. Vatio runs the conversations, and nothing reaches your users until you publish. The example is a task app built with Vite, whose signed-in users ask what is due.

#Add the agent in two steps

Step 1, from any chat. Add the remote MCP server https://vatio.ai/mcp as a connector in Claude, ChatGPT or Cursor, sign in with a Vatio account and ask for the agent. It is created in a preview you can try; nothing reaches your users until you publish (MCP).

Step 2, build it into the React code. In the project, run:

npx @vatio-ai/skills
/vatio-init

If you built the app in Lovable or Bolt, connect it to GitHub and open it in Claude Code or Cursor first. Run the first line in a terminal at the root of your project, and the second in Claude Code (in Cursor or Codex, ask it to use the vatio-init skill). /vatio-init looks at what already exists, then writes the agent and pushes a preview. The first time it pushes, your browser opens to sign in with email, Google or GitHub. Then ask your coding agent to put the widget in the app. You can say what you want after /vatio-init:

/vatio-init an agent in our React app that answers how-to questions from our docs and tells signed-in users which of their tasks are due, through our API

After launch, /vatio-improve is the daily routine (After launch).

#What vatio-init finds in a React project

vatio-init reads the project and tells you what it found and what it recommends before writing anything. Then it asks only what the code doesn't answer, such as what the agent must never promise and when a person takes over.

  • How your users sign in. An auth provider's SDK or your own login form calling your API. Either way your backend can tell who is signed in, and that is where the token gets signed. With Supabase Auth, an Edge Function signs it (Supabase apps); with Firebase Auth, a Cloud Function signs it (Firebase apps).
  • The API it could call. The fetch calls and API client in src/, and your backend's routes or OpenAPI file. If the API lives in its own repository, that is usually where the agent's files belong: run the two lines there.
  • The docs it could use. Help pages, FAQ content, Markdown docs and your README, as knowledge entries the agent searches before it answers. Vatio doesn't crawl your site.

#Put the agent in your app

Ask your coding agent to add this code; it is shown here so you can review it.

Install @vatio-ai/widget and mount it once, in an effect whose cleanup unmounts it. There is one widget per page. This hook also hands the widget the signed-in user's token, from your backend, below:

src/useVatioWidget.ts
import { useEffect, useRef } from "react";
import { mountVatioWidget, type VatioWidget } from "@vatio-ai/widget";
export function useVatioWidget(signedIn: boolean) {
const widget = useRef<VatioWidget | null>(null);
useEffect(() => {
widget.current = mountVatioWidget({ workspace: "taskwell", token: import.meta.env.VITE_VATIO_TOKEN });
return () => widget.current?.unmount();
}, []);
useEffect(() => {
if (!signedIn) { widget.current?.identify(null); return; }
const refresh = () => fetch("/api/vatio-token", { credentials: "include" })
.then((res) => res.json()).then(({ token }) => widget.current?.identify(token));
refresh();
const timer = setInterval(refresh, 50 * 60 * 1000); // before the token's 1-hour exp
return () => clearInterval(timer);
}, [signedIn]);
}

Call useVatioWidget(Boolean(user)) once, in App.tsx, with your app's own auth state. A page that never reloads keeps a conversation going by passing a fresh token before the old one expires: the same user with a newer token keeps the same conversation, and signing out starts a new one.

The publishable key (vatpub_…) is meant for page source, and it picks the environment: preview's goes in .env.development, live's in .env.production. Each works only from its own origins, exact scheme://host[:port] values with no wildcards:

Terminal window
vatio keys create publishable --env preview --label dev --origin http://localhost:5173
vatio keys create publishable --env live --label web --origin https://app.taskwell.example

Previews on Lovable, Replit, Bolt, v0 and Base44 domains are accepted without listing them. A Netlify or Vercel preview URL isn't, so add the one you test on with vatio keys origins PREFIX URL... (it replaces the list, so repeat the ones you keep) or in Settings → API keys in the inbox. A page whose origin isn't on the key gets origin_not_allowed, and the browser console names the origin to add. Origins live on the key, never in vatio.yml.

#Let it act for signed-in users

Never sign the token in the browser. It is signed with a private key, and anything in your bundle can be read: with that key, anyone could claim to be any of your users. vatio auth --new-key writes the pair. identity.pub stays with the agent's files; identity.pem goes into your backend's environment as VATIO_IDENTITY_PRIVATE_KEY, not into vatio secrets and not into a VITE_ variable. Vatio holds only the public half, so it can check a token but never mint one.

Your coding agent can add two routes to your backend. One signs the widget's token for whoever your session says is signed in. The other is what the agent's private tool calls: it verifies the token it forwards, with RS256 pinned and aud checked, and takes the user from sub:

// Your API (Express here)
import jwt from "jsonwebtoken";
import { importSPKI, jwtVerify } from "jose";
app.get("/api/vatio-token", requireUser, (req, res) => {
res.json({ token: jwt.sign(
{ sub: String(req.user.id), name: req.user.name, email: req.user.email },
process.env.VATIO_IDENTITY_PRIVATE_KEY,
{ algorithm: "RS256", audience: "taskwell", expiresIn: "1h" }
) });
});
const key = await importSPKI(process.env.VATIO_IDENTITY_PUBLIC_KEY, "RS256"); // identity.pub
app.get("/api/me/tasks", async (req, res) => {
const token = req.headers.authorization?.replace(/^Bearer /i, "") ?? "";
const verified = await jwtVerify(token, key, { audience: "taskwell", algorithms: ["RS256"] }).catch(() => null);
if (!verified) return res.status(401).json({ message: "Not signed in." });
res.json({ tasks: await tasksFor(verified.payload.sub) }); // your own query
});

sub is your user id, aud your workspace slug exactly, and exp short, because Vatio re-checks it on every tool call. Optional name, email and phone_number fill the contact in the inbox, and metadata adds a profile link and badges (Authentication). The tool forwards the token as Authorization: Bearer $auth.token, and it cannot take a user id as a parameter: the deploy fails if a private tool tries, because the model fills parameters and could be talked into filling in someone else's (Private tools).

#What your coding agent writes

These files live in your project, so you can review them like the rest of your code: an excerpt of vatio.yml, and the tool.

vatio.yml
workspace: taskwell
auth:
public_key: identity.pub
agent:
instructions: |
You help users of Taskwell. Answer how-to questions from knowledge_lookup.
For a user's own tasks, call my_tasks. Never change or delete a task.
tools: [knowledge_lookup, my_tasks]
knowledge: [help]
# tools/my_tasks.yml
description: The signed-in user's open tasks, with due dates and projects.
when_to_use: The user asks what is due, what they are working on, or about one of their tasks.
access: private
request:
method: GET
base_url: "$env.TASKWELL_API"
path: /api/me/tasks
headers: { Authorization: "Bearer $auth.token" }
respond:
data: { tasks: "$.tasks" }

TASKWELL_API is a Vatio secret, and preview can have its own (--env preview), so it calls a staging backend while live calls production. Tools run on Vatio's servers and can't reach localhost: to try a backend on your machine, expose it through a tunnel and set that URL for preview.

#Try it, then publish

vatio-init gives you a link to the inbox, with Try the agent open, where you try the preview on WhatsApp, Instagram or a web chat. The web chat is a visitor who isn't signed in, so it only reaches public tools. To try my_tasks, run your dev server with preview's key, sign in as a test user and ask in the widget "what's due this week?", then for another user's tasks by name or email: it should find nothing but your own. In the inbox, each reply shows the tool calls behind it, with their arguments and what your API answered.

When the agent does what you want, tell your coding agent to publish. Nothing reaches your users until you do.

#After launch

Your team reads real conversations in the inbox. On a wrong reply, Report this reply asks what should have happened, and that note becomes a test. Once a day, run /vatio-improve in your project: your coding agent fixes the cause where it lives (the instructions, a knowledge entry, a tool or your API) and pushes it to preview, and you review the results and approve the update (Improve). The same agent answers on WhatsApp and Instagram (Pricing).

#Frequently asked questions

#How do I add an AI agent to a React app?

Step 1: add the MCP server https://vatio.ai/mcp as a connector in Claude, ChatGPT or Cursor, sign in and ask for the agent; it is created in a preview. Step 2: in the project run npx @vatio-ai/skills, then /vatio-init in Claude Code (in Cursor or Codex, ask it to use that skill). In the app, install @vatio-ai/widget and mount it in an effect, returning unmount as the cleanup, with a publishable key that allows your app's origins.

#Can my React app sign the visitor token in the browser?

No. The token is signed with a private key, and anything in a browser bundle can be read by anyone, who could then sign a token for any of your users. Your backend signs it for whoever its own session says is signed in, and the app fetches it and passes it to the widget with identify(). Vatio holds only the public key.

#How does the agent act for the signed-in user through my API?

A tool marked access: private forwards the user's token as Authorization: Bearer $auth.token. Your API verifies it with the public key, with RS256 pinned and aud set to your workspace slug, and reads the user from sub. A private tool cannot take a user id as a parameter, so nobody can talk the agent into reading another user's data.

#Does the widget work on localhost and in Lovable or Bolt previews?

Yes. Add your dev server's origin, such as http://localhost:5173 for Vite, to the publishable key with vatio keys create publishable --origin or vatio keys origins, or in Settings → API keys in the inbox. Previews on Lovable, Replit, Bolt, v0 and Base44 domains are accepted without listing them; any other domain has to be added exactly.

#My React app uses Supabase or Firebase for sign-in. What changes?

Vatio verifies only the token your backend signs, so with Supabase Auth an Edge Function checks the Supabase session and signs it, and with Firebase Auth a Cloud Function or your backend checks the Firebase session and signs the token the widget gets.

  • Build an AI agent for your app: The two-step path with your coding agent, from the first chat to publishing.
  • An AI chat for your web app or site: One script tag puts the agent in your web app or site, themed to your brand and able to recognize signed-in users.
  • Supabase apps: Apps on Supabase: an Edge Function signs the user's token, and tools call your Edge Functions.
  • Firebase apps: A Cloud Function turns Firebase sign-in into a token Vatio verifies; tools call your HTTPS functions.

Last updated:

Latest CLI version: 0.114.0 · Changelog · The whole contract as Markdown: /docs/docs.md · or vatio docs