# An AI agent for your Firebase app

To add an AI agent to a Firebase 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](#add-the-agent-in-two-steps)). Your coding agent writes tools that call your Cloud Functions or other HTTPS endpoints. In the app, the agent is the `@vatio-ai/widget` package. Vatio doesn't verify Firebase ID tokens, so to act for signed-in users a Cloud Function checks the Firebase session and signs a short-lived token that Vatio does verify; the agent's tools forward it to your functions, which verify it and read the user from it. Nothing reaches your users until you publish. The example is a running app whose signed-in users ask about their training plan.

## 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](https://vatio.ai/docs/mcp)).

**Step 2, build it into the Firebase app.** In the project, run:

```text
npx @vatio-ai/skills
/vatio-init
```

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's entry file. You can say what you want after `/vatio-init`:

```text
/vatio-init an agent in our Firebase app that answers from our help pages and tells signed-in runners what is on their plan this week, through our Cloud Functions
```

After launch, `/vatio-improve` is the daily routine ([After launch](#after-launch)).

## What vatio-init finds in a Firebase 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.** Firebase Auth, from `firebase/auth` and your sign-in screens. Vatio verifies a token signed with the key `vatio auth --new-key` makes, and a Firebase ID token is not one: its audience is your Firebase project, not your workspace. So a Cloud Function, or your own backend, signs the token Vatio checks, and your coding agent can add it.
- **What the agent could call.** HTTPS functions (`onRequest`) in `functions/`, and any other API of yours. Callable functions and Firestore's own API expect Firebase credentials, so the agent's tools call HTTPS functions that verify Vatio's token instead.
- **The docs it could use.** Help and FAQ pages, policies 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.

Mount the widget once, from your app's entry file, and tell it who is signed in whenever Firebase does, with a token from the `vatioToken` function in the next section:

```ts
// src/vatio.ts, imported once from your entry file
import { onIdTokenChanged } from "firebase/auth";
import { getFunctions, httpsCallable } from "firebase/functions";
import { mountVatioWidget } from "@vatio-ai/widget";
import { app, auth } from "./firebase"; // your existing Firebase setup

const widget = mountVatioWidget({ workspace: "stride-coach", token: import.meta.env.VITE_VATIO_TOKEN });
const vatioToken = httpsCallable<void, { token: string }>(getFunctions(app), "vatioToken");

// Sign-in, sign-out and Firebase's hourly token refresh all land here.
onIdTokenChanged(auth, async (user) => {
  widget.identify(user ? (await vatioToken()).data.token : null);
});
```

The same user with a newer token keeps the same conversation; signing out, or someone else signing in, 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` (Vite here). Each works only from its own origins, exact `scheme://host[:port]` values with no wildcards:

```bash
vatio keys create publishable --env preview --label dev --origin http://localhost:5173
vatio keys create publishable --env live --label web --origin https://stride-coach.web.app --origin https://stride-coach.firebaseapp.com
```

List every address your users reach: both Firebase Hosting domains and your custom domain. A Hosting preview channel has a URL of its own, 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

`vatio auth --new-key` writes a keypair. `identity.pub` stays with the agent's files. `identity.pem` becomes a Cloud Functions secret, and then you delete the file; it never goes into the web app or `vatio secrets`. Vatio holds only the public half, so it can check a token but never mint one.

```bash
firebase functions:secrets:set VATIO_IDENTITY_PRIVATE_KEY --data-file identity.pem
```

Your coding agent can add two functions. `vatioToken` is callable, so Firebase verifies the caller's ID token before it runs, and it signs Vatio's token for that user. `myPlan` is what the agent's private tool calls: it verifies the token the tool forwards, with RS256 pinned and `aud` checked, and takes the user from `sub`:

```ts
// functions/src/index.ts
import { onCall, onRequest, HttpsError } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";
import { sign, verify, type JwtPayload } from "jsonwebtoken";

const privateKey = defineSecret("VATIO_IDENTITY_PRIVATE_KEY");
const aud = "stride-coach"; // your workspace slug

export const vatioToken = onCall({ secrets: [privateKey] }, (request) => {
  if (!request.auth) throw new HttpsError("unauthenticated", "Sign in first.");
  const { uid, token } = request.auth;
  return { token: sign({ sub: uid, name: token.name, email: token.email }, privateKey.value(),
    { algorithm: "RS256", audience: aud, expiresIn: "1h" }) };
});

export const myPlan = onRequest(async (req, res) => {
  const bearer = req.get("authorization")?.replace(/^Bearer /i, "") ?? "";
  let uid: string | undefined;
  try {
    uid = (verify(bearer, process.env.VATIO_IDENTITY_PUBLIC_KEY!, { algorithms: ["RS256"], audience: aud }) as JwtPayload).sub;
  } catch { /* not a valid Vatio token */ }
  if (!uid) { res.status(401).json({ message: "Not signed in." }); return; }
  res.json({ plan: await planFor(uid) }); // your Firestore query, scoped to uid
});
```

`VATIO_IDENTITY_PUBLIC_KEY` holds the contents of `identity.pub`. `sub` is the Firebase uid, `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](https://vatio.ai/docs/authentication/)). The Admin SDK bypasses your Security Rules, so `planFor` reads only that user's documents. 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](https://vatio.ai/docs/authentication/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 tools.

```yaml
# vatio.yml
workspace: stride-coach
auth:
  public_key: identity.pub
agent:
  instructions: |
    You help runners who use Stride. Answer questions about training and the
    app from knowledge_lookup. For a runner's own plan, call my_plan.
  tools: [knowledge_lookup, my_plan, ask_for_a_person]
  knowledge: [help]

# tools/my_plan.yml
description: The signed-in runner's training plan for this week.
when_to_use: The runner asks what to run today or this week, or about their plan.
access: private
request:
  method: GET
  base_url: "$env.MY_PLAN_URL"
  path: /
  headers: { Authorization: "Bearer $auth.token" }
respond:
  data: { plan: "$.plan" }

# tools/ask_for_a_person.yml
description: Tell the team that this conversation needs a person.
when_to_use: >
  The runner describes pain or an injury, 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.ASK_FOR_A_PERSON_URL"
  path: /
  requires: [chat.url]
  body: { reason: "$params.reason", chat_url: "$chat.url", name: "$contact.name" }
respond:
  message: >
    The team was told. Tell the runner plainly that you are checking this
    with a colleague, who will answer here.
```

`MY_PLAN_URL` is a Vatio secret holding the URL `firebase deploy` printed for `myPlan`. Preview can have its own (`--env preview`), so it calls a staging Firebase project while live calls production. Tools run on Vatio's servers and can't reach the emulators on `localhost` unless you expose them through a tunnel.

`ASK_FOR_A_PERSON_URL` holds the URL of one more HTTPS function of yours. It alerts whoever should pick the conversation up, a coach on WhatsApp or your support email, with `chat_url`, which opens it in the inbox. Their reply there takes the conversation over, and the agent stops answering on that chat; until then it keeps helping. See [Hand off to a person](https://vatio.ai/docs/handoff).

## 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_plan`, run your dev build with preview's widget token, sign in as a test user and ask in the widget "what's on my plan this week?", then for another runner's plan 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 function 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 functions) and pushes it to preview, and you review the results and approve the update ([Improve](https://vatio.ai/docs/improve)). The same agent answers on WhatsApp and Instagram ([Pricing](https://vatio.ai/pricing)). There, private tools send a **Sign in** button to a page of yours that signs the user in with Firebase and redirects to Vatio with the same token ([On WhatsApp and Instagram](https://vatio.ai/docs/authentication/sessions#on-whatsapp)).

## Frequently asked questions

### How do I add an AI agent to a Firebase 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, mount the @vatio-ai/widget package with a publishable key that allows your app's origins, and give it the signed-in user's token from a Cloud Function.

### Does Vatio verify Firebase ID tokens?

No. Vatio verifies a token signed with the key vatio auth --new-key generates, whose aud is your workspace slug. A Firebase ID token is not one. So a Cloud Function, or your own backend, checks the Firebase session and signs a short-lived RS256 token for that user, and the app passes it to the widget.

### Can the agent read Firestore directly?

No. Its tools call HTTP endpoints with the token Vatio verified, and Firestore's API expects Firebase credentials instead. The tools call your HTTPS Cloud Functions, which verify the token, take the user from its sub claim and read Firestore for that user. The Admin SDK bypasses Security Rules, so each function scopes its queries to that user itself.

### Where does the signing key go in a Firebase project?

vatio auth --new-key writes identity.pub, which stays with the agent's files, and identity.pem, the private key. Store identity.pem as a Cloud Functions secret, such as VATIO_IDENTITY_PRIVATE_KEY, then delete the file. Never put it in the web app, a Firestore document or vatio secrets: Vatio holds only the public key.

### What happens when the user's Firebase token refreshes?

Listen with onIdTokenChanged, which fires on sign-in, sign-out and each hourly refresh, and ask your Cloud Function for a fresh Vatio token each time. The same user with a newer token keeps the same conversation, and signing out starts a new one.

## Related guides

- [Build an AI agent for your app](https://vatio.ai/docs/guides/build-an-ai-agent): The two-step path with your coding agent, from the first chat to publishing.
- [An AI chat for your web app or site](https://vatio.ai/docs/guides/ai-chat-widget-for-your-website): One script tag puts the agent in your web app or site, themed to your brand and able to recognize signed-in users.
- [React apps](https://vatio.ai/docs/guides/ai-agent-for-react-apps): The widget mounted in an effect; your backend signs the user's token and your API verifies it.
- [Authentication](https://vatio.ai/docs/authentication/): The token Vatio verifies, private tools and signing in on WhatsApp and Instagram.
