---
title: "Ship It"
description: "Part 9 of the Build an Agent tutorial. Put a web dashboard on the agent with useEveAgent, replace placeholderAuth, and deploy to Vercel."
---

The analytics assistant runs fine in the TUI. Now ship it for real, as a web dashboard your team logs into, behind actual auth, deployed on Vercel. There are three pieces to wire up. A React UI, the channel's auth, and the deploy itself.

## Add the Web Chat app

Step 1 scaffolded the agent without a web frontend. Add one now with `eve channels add`, run from the `analytics-assistant/` directory:

```bash
npx eve channels add web
```

This adds a Next.js app (`next.config.ts`, `app/page.tsx`, `app/_components/`) wired to the existing eve channel, plus the chat UI components and their dependencies. Run `npm install` afterward to install the added packages. The generated `next.config.ts` wraps your config with `withEve`, which wires the eve routes automatically:

```ts title="next.config.ts"
import type { NextConfig } from "next";
import { withEve } from "eve/next";

const nextConfig: NextConfig = {};

export default withEve(nextConfig);
```

## A dashboard with `useEveAgent`

The dashboard talks to the built-in eve HTTP channel (`agent/channels/eve.ts`). On the browser side, `useEveAgent` handles session creation, streaming, and HITL. The scaffold renders its chat from `app/_components/agent-chat.tsx`, mounted by `app/page.tsx`. That component is fuller than you need to start, so replace its contents with this minimal version:

```tsx title="app/_components/agent-chat.tsx"
"use client";

import { useEveAgent } from "eve/react";

export function AgentChat() {
  const agent = useEveAgent();
  const isBusy = agent.status === "submitted" || agent.status === "streaming";

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        const message = String(data.get("q") ?? "").trim();
        if (message) void agent.send({ message });
      }}
    >
      {agent.data.messages.map((message) => (
        <article key={message.id}>
          <header>{message.role}</header>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.text}</p> : null,
          )}
        </article>
      ))}
      <input name="q" disabled={isBusy} placeholder="Ask about the data…" />
      <button type="submit" disabled={isBusy}>
        Ask
      </button>
    </form>
  );
}
```

The generated `app/page.tsx` already imports and renders this `AgentChat` export, so no other wiring is needed:

```tsx title="app/page.tsx"
import { AgentChat } from "@/app/_components/agent-chat";

export default function Page() {
  return <AgentChat />;
}
```

`agent.data.messages` and `agent.status` cover most chat UIs. The hook also surfaces HITL prompts (the spend approval from [Step 8](./guard-the-spend)), so the dashboard can render approve/deny controls. For the full API, see [Frontend](../guides/frontend/overview).

## Replace `placeholderAuth`

The scaffold's channel ships with `placeholderAuth()`, which fails closed. It rejects production traffic so an unauthenticated app can't go live by accident. Swap it for your app's real auth before you deploy.

Your auth lives in one module that turns a request into a user. Create `agent/lib/auth.ts` and wire your real provider (a cookie session, Auth.js, Clerk) in here. The stub below returns a fixed user so the page compiles and runs end to end:

```ts title="agent/lib/auth.ts"
export interface AppUser {
  id: string;
  team: string;
}

// Replace with your real session/provider lookup.
export async function authenticate(_request: Request): Promise<AppUser | null> {
  return { id: "demo-user", team: "growth" };
}
```

Now point the channel at it. Replace the contents of `agent/channels/eve.ts`, which Step 7 left with a dev-only `devTeam` entry and `placeholderAuth()`. List your app auth first, ahead of the catch-all helpers, so any entry that doesn't recognize the caller falls through to the next one:

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";
import { authenticate } from "../lib/auth.js";

const appAuth: AuthFn<Request> = async (request) => {
  const user = await authenticate(request); // your cookie/session/provider
  if (!user) return null;
  return {
    attributes: { team: user.team }, // the claim Step 7's playbook reads
    principalType: "user",
    principalId: user.id,
    authenticator: "app",
    issuer: "analytics-dashboard",
  };
};

export default eveChannel({
  auth: [appAuth, vercelOidc(), localDev()],
});
```

That `team` attribute is exactly what the dynamic playbook in [Step 7](./team-playbooks) reads from `ctx.session.auth`. Identity is set in this one place and flows out to every capability from there.

## Deploy to Vercel

```bash
vercel deploy
```

On Vercel, the web app stays public and the eve runtime sits behind it on the same origin, with the sandbox running on Vercel Sandbox. You can smoke-test the deployment without leaving the CLI:

```bash
npx eve dev https://your-analytics-app.vercel.app
```

That's the full assistant, deployed and authed. It queries the warehouse, runs analysis in a sandbox, charts the results, remembers your team's definitions, loads the right playbook per team, and asks before it spends.

## What you learned

Across the nine steps you built and shipped one agent, and along the way you used:

- **Tools** to give the model typed actions (`run_sql`, `chart_series`, `define_metric`).
- **Connections** to reach a warehouse over an OAuth MCP, with per-user tokens eve resolves for you.
- **The sandbox** to compute and chart beyond SQL in an isolated `/workspace`.
- **State** (`defineState`) to remember the team's glossary across turns.
- **Dynamic skills** (`defineDynamic`) to load the right team playbook per caller.
- **Human-in-the-loop** approval (`approval`) to gate expensive queries.
- **Channel auth** to turn a request into an authenticated principal.
- **Deployment** to Vercel, with the runtime behind your web app.

## Next steps

- [MCP connections](../connections/mcp) for tool allowlists and per-connection approval.
- [Sandbox](../sandbox) for backends, lifecycle, and network policy.
- [Dynamic capabilities](../guides/dynamic-capabilities) for schema-derived dynamic tools, a read-only analyst subagent, and model-authored report workflows on this same example.
- [Auth and route protection](../guides/auth-and-route-protection) for production auth patterns.

Learn more: [Frontend](../guides/frontend/overview) · [Auth and route protection](../guides/auth-and-route-protection) · [Deployment](../guides/deployment)
