mastra-chat-kit is a free, open-source workspace — a Next.js chat frontend and a Mastra agent server, already wired to each other — that helps you build agent applications where the agent's work is visible and approved rather than hidden behind a text stream. Clone it and the whole stack runs. Or install just the chat layer into a project you already have.
Features · Getting started · Install the chat layer · Capabilities · Deploy · FAQ
The thing that makes this a controller and not a chat box: the agent's work is visible and gated. Tools do not silently run — they surface as a card and wait.
"What's the weather in Los Angeles?" — a tool call, paused for approval
Three things are on screen at once, each a different AI Element driven by a different controller event:
- Steps — the agent's reasoning trace (
Called getWeather) - A live tool chip —
getWeatherwith its status, whose arguments streamed in token by token before the call settled - The approval gate —
Run getWeather?with the exact arguments, and Approve / Reject
Nothing has executed yet. The controller run is parked server-side on an open SSE connection; approving resumes that same session, and the tool result flows back into the transcript.
"Use the code subagent to create hello.txt" — delegation to a specialist
The chat agent recognises the intent and hands off to the code subagent — a real specialist with its own instructions, model, and tools, including a shell it can actually run commands in. The subagent's file write lands in the shared workspace, so the workbench's Files tab reflects it live, and the Terminal tab shows the shell output the run produced.
Same pattern for research (browse + search + cite), writer (long-form drafting), review (read-only audit), and data (versioned SQL).
Both walkthroughs are from real runs against AIMock fixtures, so the screenshots are reproducible with no API spend — see Screenshots. Wording varies with the model you point it at.
- Mastra Agent Harness (the
AgentControllerclass)- Every side-effecting tool pauses for approve / decline before it runs — human-in-the-loop is the default posture, not a feature you bolt on
- Subagents, plan modes, goals, task tracking, observational memory and cron schedules, all agent-driven
- A real workspace the agent drives: filesystem, shell sandbox, and a headless browser
- AI SDK v7 + AI Elements
- Installable, not copy-pasted
shadcn addpulls the chat layer into any project, so fixes flow to consumers instead of drifting across forks- The engine is UI-free — bring your own frontend and render it over one hook
- Zero-friction storage
- Provider-agnostic
CHAT_MODELis aprovider/modelstring resolved by Mastra's model router — 600+ models across 40+ providers, one gateway key or one key per provider
- Testable before it costs anything
- Unit, integration, component and e2e tiers all run against deterministic AIMock fixtures
- Real provider keys are absent by design, so an accidental live call fails loudly instead of billing you
Every capability is agent-driven — no manual buttons; the agent calls the right tool when it recognises the intent. Open /events in the app for the full event map with a copy-paste prompt per capability.
| Capability | What it does | Try it |
|---|---|---|
| Tool approvals (HITL) | Every side-effecting tool pauses for approve / decline before it runs. | "What's the weather in Tokyo?" |
| Modes (Chat / Plan) | The agent proposes a plan, then switches to Chat to execute it on approval. | "Propose a plan to add a dark-mode toggle, then wait for approval." |
| Goals | A standing objective the agent iterates toward; a judge scores each turn until it passes. | "Keep refining a haiku about the ocean until it's excellent." |
| Subagents | Delegates to a specialist — code (build/run in the sandbox), research (browse + search + cite), writer (long-form), review (read-only audit), data (versioned SQL, when Dolt is on) — each with its own instructions, model, and tools. | "Use the code subagent to create hello.js that prints 1–10, then run it." |
| ask_user | On a genuinely ambiguous request the agent asks you a question and resumes with the answer. | "Deploy my app." |
| Task tracking | Multi-step work rendered as a live checklist. | "Plan and build a tiny counter in tracked steps." |
| Observational memory | A background Observer distills durable facts across chats (the Memory panel). | have a short back-and-forth |
| Schedules | Recurring, persisted cron runs that survive a restart. | "Remind me every morning to check the changelog." |
| Workspace | A real filesystem + shell sandbox + browser the agent and its subagents drive. | "Use the code subagent to create notes.txt." |
| Live tool streaming | Tool arguments stream into an input-streaming Tool before the call settles. | (fires on any tool call) |
| Semantic search | The sidebar searches message bodies via a local embedding index. | type in the sidebar search |
Prerequisites: Node.js 22+ · pnpm 10+ · one model provider key. No Docker, no Postgres — storage defaults to a local file: libSQL DB.
# 1. Install all workspace deps
pnpm install
# 2. Configure the server env
cp packages/server/.env.example packages/server/.env
# Set APP_SECRET (openssl rand -hex 32) + model access. CHAT_MODEL is a
# `provider/model` string resolved by Mastra's model router. Two ways to key it:
# • GATEWAY (one key, every provider, swap models freely):
# AI_GATEWAY_API_KEY + CHAT_MODEL=vercel/… (Vercel AI Gateway), or
# MASTRA_GATEWAY_API_KEY + CHAT_MODEL=mastra/… (Mastra Gateway)
# • DIRECT (one key per provider):
# ANTHROPIC_API_KEY + anthropic/…, OPENAI_API_KEY + openai/…, etc.
# Leave TURSO_DATABASE_URL as-is for the local file: DB.
# 3. Configure the web env — optional locally, REQUIRED to deploy
cp packages/web/.env.local.example packages/web/.env.local
# Only MASTRA_SERVER_URL matters, and it defaults to http://localhost:4111,
# so `pnpm dev` works without this. On a deployment the fallback resolves to
# nothing and every proxied request fails, without a build or deploy error.
# 4. Run server (:4111) + web (:3000) together
pnpm devOpen http://localhost:3000 for the chat and /events for the controller event → element map, with a copy-paste prompt per capability.
Zero-cost dev. Run against AIMock instead of a real provider:
pnpm --filter @mastra-chat-kit/server dev:mock # AIMock on :4010
# then set USE_AIMOCK=true in packages/server/.env and start the serverLoading env in dev: a plain
.envworks everywhere. We inject secrets with Infisical instead of a committed file —infisical run --path=/<project> -- pnpm dev— so nothing sensitive lands on disk.
npx shadcn@latest init --base radixThen confirm components.json says "iconLibrary": "lucide".
| Requirement | Why | If you get it wrong |
|---|---|---|
--base radix |
The upstream Vercel AI Elements this kit depends on are authored against Radix and don't survive the CLI's Base UI transform. | 14 type errors, in upstream files |
iconLibrary: lucide |
Every component here imports lucide-react. On hugeicons, shadcn's own ui/spinner.tsx fails to typecheck. |
1 type error, failed build |
A bare
npx shadcn@latest initgives you Base UI, not Radix — since CLI 4.x,--defaultsresolves to--preset=base-nova. This kit's own components port to either base fine (the CLI rewritesasChildto Base UI'srender); it's the upstream elements that don't.
Verified end-to-end on shadcn CLI 4.16.0 / Next 16.2.6: with both prerequisites met, a fresh install typechecks with 0 errors and next build exits 0.
Add the namespace to components.json, then install:
npx shadcn@latest add @mastra-chat-kit/chat # the full shell
npx shadcn@latest add @mastra-chat-kit/chat-minimal # or the embeddable onechat lands 34 files from this registry — 9 shell components, the shared tool
renderers, 14 app/api/* proxy routes plus the proxy lib, the 4-file chat-engine,
and our 5 vendored AI Elements — plus the upstream AI Elements and shadcn/ui
primitives they resolve to.
Then mount a shell and point it at a server:
import { ChatSwitcher } from '@/components/chat/chat-switcher'; // sidebar │ chat │ workbench
// or
import { MinimalChat } from '@/components/chat-minimal/minimal-chat'; // just the conversation# .env.local
MASTRA_SERVER_URL=http://localhost:4111The UI is pure frontend — it talks to a Mastra server over the same-origin route handlers it installed. Note that a stock mastra dev server does not expose the shape those routes expect; it serves Mastra's own /api/agents/*. This repo's packages/server registers the full contract and is the reference implementation. The 14-endpoint contract is in docs/registry.md.
The look and the engine are separate registry items, so you can change the first without losing the second:
| Item | What it is |
|---|---|
chat-engine |
The brains — SSE client, transcript reducer, and the hooks that own every /api/* call. UI-free: imports only React. |
chat-tool-views |
Shared renderers turning real tool output into elements. Used by every skin. |
chat · chat-minimal |
Just the looks. Neither depends on the other. |
Both skins drive the same AgentController session — same threads, same tool
approvals, same subagents, same workspace. A third skin is one file rendering over
useAgentControllerChat(); docs/registry.md covers how, and the build fails if a
skin imports another skin or forgets a shared dependency.
Colors and fonts need no work: the registry ships no cssVars, so every skin
inherits your project's own shadcn theme.
Being straight about this:
chatandchat-minimaldiffer in layout, not yet in visual style — both render messages through the same AI Elements at default styling. The engine/skin split is real and tested; a skin with its own visual identity is still to come.
Every capability above comes from one place.
On the name: Mastra announced this as the Agent Harness. The class you import from
@mastra/coreisAgentController. They are the same thing — this README saysAgentControllerwherever it names real code.
| Component | <ChatSwitcher /> — sidebar │ chat │ workbench |
| Backend | AgentController → Session; commands in, events out |
| Route | POST /agent-controller/stream (SSE) + POST /agent-controller/approve |
| Web client | useAgentControllerChat hook (command POST + SSE) |
| Wire format | AgentControllerEvents → AgentControllerDisplayState |
It runs on Mastra's AgentController — the session controller Mastra's docs describe as handling "managing conversation threads, switching between agent modes, persisting state, gating tool execution with approvals, and coordinating subagents."
→ docs/agent-controller.md covers the controller in full, using Mastra's exact vocabulary.
Browser · AI Elements + useAgentControllerChat · packages/web (Next.js 16, :3000)
│ │
▼ │
POST /agent-controller/stream (SSE) · POST /agent-controller/approve │
│ │
▼ │
AgentController → Session │
modes · goals · approvals · subagents · tasks · workspace │
· schedules · follow-ups → AgentControllerEvents │
│ │
▼ │
packages/server · Mastra + Hono (:4111) │
│ │
▼ │
LibSQLStore + LibSQLVector ── fastembed (local, 384-d) │
file: local · libsql:// Turso prod │
memory · threads · observability · semantic recall ◀───────────┘
The controller wraps the chatAgent. Storage, threads, observability, and vector recall land in one libSQL database; embeddings run locally via fastembed (no embedding API).
Every image in this README is generated from the running app, not mocked up:
node packages/web/scripts/screenshot.mjsIt drives real AgentController sessions and captures the resulting UI. Read the header comment first — the live captures depend on a freshly-wiped packages/server/mastra.db and on scenario order, because the AIMock fixtures match on turnIndex (assistant messages in the request) and Mastra's semantic recall pulls earlier threads into later ones. The script refuses to write an image whose transcript shows bleed-through from another scenario rather than emitting a misleading one.
Every tier is AIMock-backed, zero LLM spend — real provider keys are intentionally absent, so an accidental real call fails loudly instead of billing you.
| Tier | Command | What it covers |
|---|---|---|
| unit + integration | pnpm --filter server test |
Agent + AgentController flows via AIMock — a globalSetup boots the mock on :4010 |
| evals | pnpm --filter server eval |
@mastra/evals scorers (run with USE_AIMOCK=true) |
| component | pnpm --filter web test |
AgentController reducer, transport + chat views, element rendering (Vitest + RTL) |
| e2e | pnpm test:e2e |
Full chat flow (Playwright, AIMock-backed) |
pnpm test runs the unit/integration + component tiers across both packages. CI runs lint, both test suites, a production web build, and a .dockerignore check on every PR — no secrets, no spend. A separate Container workflow builds the real server image whenever anything it's made of changes, and asserts the result carries no secrets or host files and boots to healthy.
e2e note: the Playwright config runs the web as a production build (
next build && next start), notnext dev— Turbopack's HMR socket breaks React hydration under headless Chromium.
A two-package pnpm-workspaces monorepo: a Mastra + Hono server and a Next.js web frontend.
packages/
├─ server/ Mastra + Hono agent server (:4111)
│ └─ src/
│ ├─ lib/env.ts Zod-validated env — crashes on bad config
│ └─ mastra/
│ ├─ index.ts Boot: env → AIMock → Mastra; AgentController routes
│ ├─ agents/ chat · code · research · writer · reviewer · data (spawned as specialists)
│ ├─ lib/
│ │ ├─ agent-controller.ts AgentController + Session
│ │ ├─ memory.ts shared Memory: LibSQLVector + fastembed recall
│ │ └─ dolt.ts optional versioned data, Git-style (mysql2)
│ └─ tools/ agent tools (getWeather, dolt, image, schedules …)
└─ web/ Next.js 16 App Router + AI Elements (:3000)
├─ app/ chat (/) + /events — the controller-event → element map
├─ components/chat/ full skin: sidebar · workbench (Files/Terminal/Browser/Memory/Schedules) · approvals
├─ components/chat-minimal/ second skin: conversation + composer only
├─ components/ai-elements/ vendored AI Elements (you own these files)
├─ lib/agent-controller/ the engine — SSE client, reducer, data hooks (UI-free)
├─ lib/agent-controller-event-map.ts the 50 events → elements + prompts (drives /events)
└─ scripts/ gen-registry.mjs (registry manifest) · screenshot.mjs
| Layer | Technology |
|---|---|
| Agent framework | Mastra — @mastra/core (Agent + AgentController + Workspace), @mastra/memory, @mastra/ai-sdk, @mastra/evals, @mastra/observability, @mastra/editor, @mastra/mcp, @mastra/auth, @mastra/loggers |
| AI SDK | Vercel AI SDK v7 — ai, @ai-sdk/react (useChat) |
| Chat UI | AI Elements (vendored, shadcn-style) + shadcn/ui — Radix base |
| Models | Any provider — CHAT_MODEL is a provider/model string resolved by Mastra's model router (600+ models, 40+ providers). Gateway or direct keys both work. Image generation always uses OpenAI. |
| Storage + vectors | libSQL / Turso — @mastra/libsql (LibSQLStore + LibSQLVector); file: local, libsql:// prod. Postgres opt-in → |
| Embeddings | fastembed — local ONNX bge-small (384-d), no embedding API |
| Versioned data | Dolt via mysql2 (optional — the app boots without it) |
| API server | Hono (mounted via Mastra) |
| Frontend | Next.js 16, React 19, Tailwind 4 |
| Testing | Vitest, Playwright, AIMock |
| Linting | Biome |
| Monorepo | pnpm workspaces |
Memory, threads, and the vector index for semantic recall live in libSQL (native vector search — no separate vector service, no pgvector). Observability traces use DuckDB via a composite store, since Studio's Metrics/Logs need OLAP queries libSQL can't serve.
| Environment | TURSO_DATABASE_URL |
Needs |
|---|---|---|
| Local dev | file:./mastra.db (default) |
nothing — no server, no Docker |
| Production | libsql://<db>-<org>.turso.io + TURSO_AUTH_TOKEN |
a Turso database |
Prefer Postgres (Supabase / Neon / RDS)? It's a small, self-contained swap — see docs/postgres.md.
Separate from the app's own storage, the kit ships an optional integration with Dolt — a SQL database that versions data the way Git versions code. It speaks the MySQL wire protocol, so it's a normal mysql2 connection; the only special part is that every write ends in a DOLT_COMMIT.
It's data-agnostic — point it at whatever you'd want a history for:
| You put here… | …and versioning buys you |
|---|---|
| A product catalog / price list an agent edits | Undo a bad bulk change with one revert; diff exactly which rows moved |
| Customer / CRM records the agent updates | A full audit trail — who changed what, when — with author attribution |
| A knowledge base / FAQ the agent curates | Review its edits as a diff on a branch, merge only what you approve |
| App config / feature flags | Time-travel back to a known-good state after a regression |
| Labeled datasets / eval results | A branch per experiment, compared side by side |
Why version it: it gives an agent an auditable, reversible database. When an agent mutates data you don't just get the new value — you get a commit you can diff, view history on, time-travel to, branch, and roll back. That enables "branch-per-agent → human merges": the agent proposes changes on its own branch, a human reviews the diff and merges. The agent never knows it's a versioned DB — it just calls the doltQuery / doltWrite tools.
Off by default. With no DOLT_HOST / DOLT_PORT set, the tools and boot-time bootstrap don't activate and the app runs entirely on libSQL.
The server runs on Mastra, so you can open it in Mastra Studio — a browser dashboard for the agents without the web app.
pnpm --filter @mastra-chat-kit/server dev # server + Studio → http://localhost:4111- 💬 Chat with the agents directly (uses your configured
CHAT_MODEL) - ✏️ Edit & version system prompts (draft/publish) via
@mastra/editor - 🧰 Tools — browse every tool the agent can call and invoke it by hand
- 🧠 Memory & threads — working memory + every conversation, persisted to libSQL
- 🧬 Observational memory — the durable, cross-chat facts the background Observer distills
- 🔭 Traces — per-run agent / tool / LLM spans
- ✅ Scorers — score runs against the eval datasets
mastra build compiles the server into a self-contained Node app (.mastra/output) that runs on any Node/Bun/Deno host; packages/server/Dockerfile wraps it. The web app is a plain Next.js deploy. What decides the target is the agent's workspace: as shipped it uses local backends — LocalFilesystem + LocalSandbox (a real shell) + a Playwright browser — which want a persistent disk and a long-running process.
Always-on Node host / container — recommended, minimal changes.
Railway · Render · Fly.io · a VPS (Docker/Coolify) · AWS EC2 · DigitalOcean · Azure App Service. Point the build at packages/server/Dockerfile, set TURSO_DATABASE_URL + your keys.
- ✅ Server, memory/threads/vectors, the filesystem + shell workspace, DuckDB traces, and the headless browser all work with no app-code changes. The
Dockerfileprovisions Chromium — verified by launching it inside the built image as the non-root user.
Serverless / edge — a real port, not drop-in.
Vercel · Netlify · Cloudflare, via Mastra's deployers (added as deployer: on the Mastra instance — a code change). Serverless has no persistent disk or long-running process, so the local workspace + browser + DuckDB don't run there. Chat + memory + the model gateway work on the edge out of the box; to keep the workspace, swap its local backends for cloud ones in workspace.ts (E2BSandbox / VercelSandbox / RailwaySandbox, S3Filesystem / GCSFilesystem, a non-DuckDB observability store, Turso storage).
Mastra Cloud — managed.
mastra auth, then mastra deploy --org <id> --project <name> — gateway auto-seeded, managed libSQL provisioned. Deploy the Next.js web separately.
Building the image yourself. The build context is the repo root, not packages/server — the image installs from the workspace pnpm-lock.yaml so it can't drift onto versions dev and CI never used:
docker build -f packages/server/Dockerfile -t mastra-chat-kit-server .
pnpm verify:image mastra-chat-kit-server # asserts the image is clean and bootsBecause the context is the repo root, every nested rule in .dockerignore needs a **/ prefix. Docker is not gitignore — it matches with Go filepath.Match, so a bare .env or node_modules covers only the context root. Getting that wrong once baked packages/server/.env into an image layer and copied the host's node_modules over the container's own install. pnpm check:dockerignore catches a rule regression in milliseconds; the Container workflow builds the real image whenever anything it's made of changes, and asserts the result contains no secrets, no host files, and actually serves.
Does installing the registry give me the agent too?
Not the registry — but this repo has one, and that's the distinction worth getting straight.
Cloning the repo gives you the whole environment, agents included: packages/server is a working Mastra server with six agents, the workspace sandbox, and the full route contract registered. That's the "ready-to-run" path, and it's what Getting started sets up.
The registry is the other path — it's frontend-only by design: chat components, the transport/controller client, and same-origin Next route handlers that proxy to a Mastra server at MASTRA_SERVER_URL. No agent code ships with it, because a project installing the engine already has its own agents. You bring a Mastra server that speaks the contract; packages/server is the reference implementation. A stock mastra dev server won't match, since it serves Mastra's own /api/agents/* shape instead. The full 14-endpoint contract is in docs/registry.md.
Do I have to use Radix? Why can't I use Base UI?
Radix is required, but not because of this kit's own code — our components import zero Radix packages and the shadcn CLI happily rewrites asChild to Base UI's render on install. The blocker is the upstream Vercel AI Elements this kit depends on: they're authored against Radix and don't survive that transform, producing 14 type errors on a Base UI project.
Supporting both bases and tracking upstream are mutually exclusive. We track upstream.
Can I run it without spending anything on a model?
Yes — that's the default posture for tests and it works for dev too. Every test tier runs against AIMock fixtures, and dev:mock boots the same mock for interactive use. See the AIMock note under Getting started for the two gotchas.
Can I use a different chat UI?
Yes. The engine (chat-engine) is UI-free — it owns the SSE transport, the transcript reducer, and every /api/* call, and imports nothing but React. A skin is rendering over one hook, so chat and chat-minimal both drive the same session and neither depends on the other. Colors and fonts need no work at all: the registry ships no cssVars, so any skin inherits your project's shadcn theme. See docs/registry.md for how to author one.
Do I need Docker, Postgres, or a vector service?
None of them. Storage, threads, observability, and vector search all run on libSQL — a local file: database in dev. Embeddings run locally via fastembed, so there's no embedding API either. Postgres is a documented opt-in (docs/postgres.md), and Dolt is optional and off by default.
Issues and PRs welcome. Two things worth knowing before you start:
- This repo tracks work in beads, not GitHub issues.
bd readyshows what's actionable;bd show <id>has the design notes. SeeCLAUDE.md. - Don't hand-edit
registry.json. It's generated:pnpm --filter @mastra-chat-kit/web build:registryparses the real imports of the shipped files, and a validation gate fails the build if a shipped file imports a module or fetches an/api/*route that no registry item ships. Editscripts/gen-registry.mjsinstead.
Before opening a PR, run what CI runs:
pnpm lint && pnpm check:dockerignore && pnpm test && pnpm --filter @mastra-chat-kit/web buildIf you touched the Dockerfile or .dockerignore, also build and inspect the image — the Container workflow will do it on the PR, but it takes ~15 minutes to tell you:
docker build -f packages/server/Dockerfile -t mastra-chat-kit-server:ci .
pnpm verify:image mastra-chat-kit-server:ciUSE_AIMOCK=true has to be in .env — a shell variable won't take. The server loads packages/server/.env over the process environment, so USE_AIMOCK=true pnpm dev silently runs against the real provider. (Same trap for LOG_LEVEL.) That single setting is all you need: configureAIMock() points every provider base URL at the mock and defaults the API keys to mock, so you don't add a key and you don't change CHAT_MODEL — verified with openai/gpt-4.1-mini and no OPENAI_API_KEY present at all.
The fixtures match on turnIndex (assistant messages in the request), and Mastra's semantic recall pulls earlier threads into later ones, so anything turn-sensitive wants a freshly-wiped packages/server/mastra.db.
- 🎨 A visually distinct skin.
chatandchat-minimaldiffer in layout but still render messages through the same elements at default styling, so they look alike. The engine/skin split is done and tested; a skin with its own visual identity is the part that isn't. - 🧩 Upstream the AI Elements patches. Submitted to
vercel/ai-elementsand open at time of writing: #456 (agent) and #457 (code-block SSR), plus #458 and #459 for the two that change public types. Once merged we drop the local copies and depend entirely on upstream.
Recently shipped: the registry builds and installs from a local build (the Vercel deployment that hosted it was removed 2026-08-01 — see Install) · an install smoke test builds a throwaway consumer project from the registry · the chat engine is UI-free, so skins are swappable · the server image builds from the workspace lockfile and CI proves it ships no secrets.
Mastra
- Get started · Agent reference
- Model Router — 600+ models across 40+ providers via one
provider/modelstring · Environment variables — which key each provider needs - Agent Controller / AgentController — the session controller this kit runs on · announcement
- Memory · Signals — goals, task tracking, and observational memory all ride on the signal system
Vercel AI SDK
- AI SDK — the streaming layer · AI Elements — the UI components this kit wires up
This repo
docs/registry.md— what ships, the install prerequisites, the server contractdocs/agent-controller-events.md— every controller event → the element it drives (also live at/events)docs/coverage.md·docs/agent-controller.md·docs/ai-elements.md·docs/postgres.md
- Mastra — the agent framework: agents, AgentController, memory, evals, observability.
- Vercel — the AI SDK and AI Elements this chat layer is built from.
- Turso — libSQL, the zero-friction storage + vector backend.
- Hono, Next.js, and shadcn/ui — server, frontend, and components.
MIT. © 2026 Otaku Solutions. Part of the Mastra kit lineage (sibling to mastra-base / mastra-base-turso). Questions: hello@otakusolutions.io.



