On this page

#An AI agent for your Next.js app

To add an AI agent to a Next.js 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, rendered from your App Router root layout. To act for signed-in users, your server signs a short-lived token with jose, and your Route Handlers verify it when the agent calls them. Vatio runs the conversations, and your team follows them in the inbox. Nothing reaches your users until you publish. The example is a shop whose signed-in customers ask about their orders.

#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 Next.js code. In the project, run:

npx @vatio-ai/skills
/vatio-init

Run the first line in a terminal at the root of your Next.js 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 your root layout. You can say what you want after /vatio-init:

/vatio-init an agent in our Next.js app that answers from our help pages and tells signed-in customers where their orders are, through our app/api routes

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

#What vatio-init finds in a Next.js 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. Auth.js, Clerk, Supabase Auth or a session of your own. Whichever it is, your server signs a token, as below; on Supabase, a route or an Edge Function can (Supabase apps).
  • The API it could call. Route Handlers under app/api/**/route.ts, or an OpenAPI file. A tool is one HTTP request, and a Server Action has no stable URL to call, so your coding agent can add a Route Handler that does the same job.
  • The docs it could use. Help and FAQ pages, MDX content, policies and your README, as knowledge: Markdown 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.

The widget runs in the browser, so a client component mounts it in an effect. Your root layout renders it, in the next section, so it is on every page:

app/chat-widget.tsx
"use client";
import { useEffect, useRef } from "react";
import { mountVatioWidget, type VatioWidget } from "@vatio-ai/widget";
export function ChatWidget({ visitorToken }: { visitorToken: string | null }) {
const widget = useRef<VatioWidget | null>(null);
useEffect(() => {
widget.current = mountVatioWidget({ workspace: "kettle-shop", token: process.env.NEXT_PUBLIC_VATIO_TOKEN! });
return () => widget.current?.unmount();
}, []);
useEffect(() => widget.current?.identify(visitorToken), [visitorToken]); // null: signed out
return null;
}

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:3000
vatio keys create publishable --env live --label web --origin https://kettle.example

A Vercel preview deployment has a URL of its own: 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. v0's preview domains are accepted without listing. 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

vatio auth --new-key writes a keypair. identity.pub stays with the agent's files; identity.pem goes into your server environment as VATIO_IDENTITY_PRIVATE_KEY, never into vatio secrets or a NEXT_PUBLIC_ variable. Vatio holds only the public half, so it can check a token but never mint one. Your coding agent can add the signing to your root layout, a Server Component:

app/layout.tsx
import { SignJWT, importPKCS8 } from "jose";
import { ChatWidget } from "./chat-widget";
import { getCurrentUser } from "@/lib/auth"; // however your app reads its session
async function vatioToken(user: { id: string; name?: string; email?: string }) {
const key = await importPKCS8(process.env.VATIO_IDENTITY_PRIVATE_KEY!, "RS256");
return new SignJWT({ name: user.name, email: user.email }).setProtectedHeader({ alg: "RS256" })
.setSubject(user.id).setAudience("kettle-shop").setExpirationTime("1h").sign(key);
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
const visitorToken = user ? await vatioToken(user) : null;
return <html lang="en"><body>{children}<ChatWidget visitorToken={visitorToken} /></body></html>;
}

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). Never let a cached page carry a token, or the next visitor gets this one's identity. If people stay on one page past exp, a Route Handler such as app/api/vatio-token/route.ts can return a fresh one for identify(): the same user with a newer token keeps the conversation.

A tool marked access: private runs only for a signed-in user, forwards their token as Authorization: Bearer $auth.token, and cannot take a user id as a parameter: the deploy fails if one tries, because the model fills parameters and could be talked into filling in someone else's (Private tools). Your Route Handler verifies the token, with RS256 pinned and aud checked, and takes the user from sub:

app/api/me/orders/route.ts
import { importSPKI, jwtVerify } from "jose";
export async function GET(request: Request) {
const token = request.headers.get("authorization")?.replace(/^Bearer /i, "") ?? "";
const key = await importSPKI(process.env.VATIO_IDENTITY_PUBLIC_KEY!, "RS256"); // identity.pub
const verified = await jwtVerify(token, key, { audience: "kettle-shop", algorithms: ["RS256"] }).catch(() => null);
if (!verified?.payload.sub) return Response.json({ message: "Not signed in." }, { status: 401 });
return Response.json({ orders: await ordersFor(verified.payload.sub) }); // your own query
}

#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 tools.

vatio.yml
workspace: kettle-shop
auth:
public_key: identity.pub
agent:
instructions: |
You help customers of Kettle & Co with orders, shipping and returns. For a
customer's own orders, call my_orders. Never promise a date it did not return.
tools: [knowledge_lookup, my_orders, ask_for_a_person]
knowledge: [help]
# tools/my_orders.yml
description: The signed-in customer's recent orders, with status and tracking.
when_to_use: The customer asks about their own orders, a delivery or a return.
access: private
request:
method: GET
base_url: "$env.APP_URL"
path: /api/me/orders
headers: { Authorization: "Bearer $auth.token" }
respond:
data: { orders: "$.orders" }
# tools/ask_for_a_person.yml
description: Tell the team that this conversation needs a person.
when_to_use: >
The customer asks for a refund or reports a damaged order, you cannot
resolve it with what you know and your other tools, or they ask to talk
to a person.
parameters:
type: object
properties:
reason:
type: string
description: Why it needs a person, in one line, for whoever picks it up.
required: [reason]
access: public
request:
method: POST
base_url: "$env.APP_URL"
path: /api/vatio/ask-for-a-person
requires: [chat.url]
body: { reason: "$params.reason", chat_url: "$chat.url", name: "$contact.name" }
respond:
message: >
The team was told. Tell the customer plainly that you are checking this
with a colleague, who will answer here.

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

The Route Handler behind ask_for_a_person alerts whoever handles refunds, in Slack, by email or on WhatsApp, with chat_url, which opens the conversation in the inbox. Their reply there takes it over, and the agent stops answering on that chat; until then it keeps helping. See Hand off to a person.

#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_orders, run your dev build with preview's key, sign in as a test customer and ask in the widget:

  1. "Where's my last order?" It should answer from my_orders.
  2. "Where's the order of jane@example.com?" It should find nothing but your own.

In the inbox, each reply shows the tool calls behind it, with their arguments and what your Route Handler 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 one of your Route Handlers) 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 Next.js 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, the agent is the @vatio-ai/widget package, mounted in an effect from a client component that your root layout renders, with a publishable key that allows your app's origins.

#Where do I sign the user's token in a Next.js app?

On the server: in a Server Component such as app/layout.tsx, or in a Route Handler. Sign an RS256 JWT with the private key from vatio auth --new-key, kept in a server environment variable, with sub set to your user id, aud to your workspace slug and a short exp. Never sign it in a client component or put the key in a NEXT_PUBLIC_ variable.

#How does my Route Handler know which user the agent is acting for?

A tool marked access: private forwards the visitor's token as Authorization: Bearer $auth.token. Your Route Handler verifies it with jose, with RS256 pinned and aud checked against your workspace slug, and reads the user from sub. A private tool cannot take a user id as a parameter, so the agent cannot be talked into reading someone else's data.

#Does the widget work on localhost and on Vercel preview deployments?

Yes, once their origins are on the publishable key: add http://localhost:3000 and the preview URL you test on with vatio keys create publishable --origin or vatio keys origins, or in Settings → API keys in the inbox. Origins are exact, with no wildcards. v0's preview domains are accepted without listing them.

#My Next.js app uses Supabase Auth. Do I still have to sign a token?

Yes. A Route Handler (or an Edge Function) reads the user's Supabase session, including the cookies @supabase/ssr writes, and signs the Vatio token as below.

Last updated:

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