This repository is a minimal, end‑to‑end AI app builder template combining:
- Tambo.ai for agentic UI, tools, and “interactable” components the AI can render and control
- Freestyle Sandboxes for on‑demand dev servers and Git repo management
It ships with a side‑by‑side Chat + App Viewer experience, dev‑server tooling (create/download), and a simple AI‑controlled To‑Do list component.
-
Run
npm create-tambo@latest my-tambo-appfor a new project (or clone this repo) -
npm install -
npx tambo init
- or rename
example.env.localto.env.localand add your Tambo API key from the Tambo dashboard
- Add your Freestyle API key to the server environment:
FREESTYLE_API_KEY=sk_... (server only)
NEXT_PUBLIC_TAMBO_API_KEY=pk_... (client OK)
NEXT_PUBLIC_TAMBO_URL=https://api.tambo.co (optional if default)
- Run
npm run devand go tohttp://localhost:3000
You’ll see a chat on the left and an App Viewer on the right. The App Viewer shows the ephemeral URL of a Freestyle dev server if one exists.
- Chat thread built with Tambo’s providers and thread components
- App Viewer side panel (interactable) that renders the live app URL and a toolbar
- Tools to create a dev server, update/clear preview URL, and download repo zip
- A simple, AI‑controlled To‑Do component (no backend state; the AI re-renders it with updates)
- File:
src/components/tambo/AppViewer.tsx - Registered in:
src/lib/tambo.ts(as a Tambo component) - Reads current
{ repoId, ephemeralUrl }fromlocalStorage.freestyleDevServervia a dynamic context controller - Toolbar actions when a URL is present:
- New App (create a new Freestyle dev server)
- Download (zip of the current repoId)
- Reload / Open / Copy URL
- Empty state with a CTA to create a development sandbox
How the URL stays in sync
- On successful
create_dev_server, we persist{ repoId, ephemeralUrl }intolocalStorage.freestyleDevServerand dispatch adevserver:updatedevent AppViewerand the dynamic context helper react to this and update immediately
Tools the model can use to affect the viewer
set_app_viewer_url({ url, repoId? })— persist a URL and (optionally) repo idclear_app_viewer_url({})— clear the URL (keeps repo id if present)
Download route
- File:
src/app/api/tambo-tools/devserver/download/route.ts - Uses Freestyle Git API:
GET https://api.freestyle.sh/git/v1/repo/{repo}/zip?ref=HEAD- Requires
FREESTYLE_API_KEY
- File:
src/lib/tambo-tools/devserver.ts - API route:
src/app/api/tambo-tools/devserver/create/route.ts - Accepts
{ repoId?, gitUrl?, name? }- If no
repoId, it creates/imports fromgitUrl(or uses a default template) - Requests a dev server and returns
{ repoId, ephemeralUrl, success }
- If no
- On success, the tool stores to
localStorage.freestyleDevServerand dispatchesdevserver:updated
- File:
src/components/tambo/TodoList.tsx - Registered in:
src/lib/tambo.ts - Props (zod schema):
title?: stringitems: { text: string; completed?: boolean; id?: string }[]showCounts?: boolean,dense?: boolean
- There is no mutation API; the AI “marks complete” by re‑rendering
TodoListwith updatedcompletedflags. This keeps the flow extremely simple and transparent.
- File:
src/components/tambo/DevServerContextController.tsx - Registers a context helper at runtime using
useTamboContextHelpers() - Publishes
devServerdata read from localStorage and listens tostorage+devserver:updated
These tools let the agent modify files and run commands inside the Freestyle sandbox. They are registered in src/lib/tambo.ts and implemented in:
-
Filesystem tools:
src/lib/tambo-tools/filesystem.tswrite_file({ repoId, path, content })— Write content to an absolute path (e.g.,/template/src/index.ts) creating directories if needed.read_file({ repoId, path })— Read text content from a file in the sandbox.list_directory({ repoId, path })— List file names in a directory.edit_file({ repoId, path, edits: [{ oldText, newText, dryRun? }] })— Apply find/replace style edits. UsedryRunto preview.search_files({ repoId, pattern, path?, excludePatterns? })— Grep-like search (regex supported) within a directory.
-
Process tools:
src/lib/tambo-tools/process.tsexec_command({ repoId, command, cwd?, background?, timeoutMs? })— Run a command. Usebackground: truefor long running tasks (e.g.,npm run dev).npm_install({ repoId })— Install dependencies with npm in the sandbox.git_commit_and_push({ repoId, message })— Commit all changes and push to the repo’s remote.
Typical workflow:
- Create a sandbox with
create_dev_server(). - Use
write_file/edit_fileto modify code under/template. npm_installthenexec_commandto build/run.- Preview in App Viewer; iterate.
Key directories:
src/lib/— Tambo registration (tambo.ts), tools (tambo-tools/*)src/components/tambo/— UI components for the chat, App Viewer, To‑Do listsrc/app/api/tambo-tools/*— API routes used by tools/UI (devserver create/download)src/app/— Next.js app routes, global styles
Note: If you’re organizing a separate template app inside a template/ directory (monorepo pattern), you can still point your Freestyle import/create to that Git URL. This repo’s default sample deploys a Freestyle template when gitUrl is omitted.
FREESTYLE_API_KEY— Server‑side key for Freestyle API (required for sandbox create/download)NEXT_PUBLIC_TAMBO_API_KEY— Client‑side key for Tambo SDKNEXT_PUBLIC_TAMBO_URL— Optional base URL for Tambo
Make sure FREESTYLE_API_KEY is not exposed on the client. The code uses API routes for server‑side calls.
Ask the agent to render a TodoList with items. The AI will include the component in its response. To “check off” items, the AI re-renders the same component with completed: true for the appropriate items.
Example props the AI might use:
{
"title": "Project Setup",
"items": [
{ "text": "Create dev server", "completed": true },
{ "text": "Open App Viewer", "completed": true },
{ "text": "Render TodoList", "completed": false }
],
"dense": true
}- The App Viewer is registered as a Tambo component and also listens to the runtime
devServercontext helper. - After a dev server is created via the tool, the preview URL is stored to localStorage and the viewer updates immediately.
- You can also update or clear the URL directly via tools:
set_app_viewer_url({ url, repoId? })clear_app_viewer_url({})
Use the following prompt when creating your agent in the Tambo dashboard so it uses the tools/components effectively:
You are an AI App Builder agent working in a Next.js project integrated with Tambo and Freestyle.
Goals:
- Build and iterate on a web app while keeping the user in the loop via components you render.
- Use tools to manage sandboxes and preview URLs.
- Track work with a simple To‑Do list you re‑render as progress changes.
Capabilities:
1) Components you can render
- AppViewer(url?) — shows a live app preview (ephemeral URL). If not provided, it reads the URL from context.
- TodoList(title?, items[], dense?) — a simple list where you mark items complete by re‑rendering with updated props.
2) Tools you can call
- create_dev_server({ repoId?, gitUrl?, name? }) ⇒ { repoId, ephemeralUrl, success }
• Use this to spin up a Freestyle dev server. On success, the app persists the URL and updates the App Viewer.
- set_app_viewer_url({ url, repoId? }) / clear_app_viewer_url({})
• Use these to control the preview URL directly if needed.
- Download code (handled by the UI via /api/tambo-tools/devserver/download?repoId=…)
- Filesystem tools (operate on absolute paths like `/template/...`):
• write_file({ repoId, path, content })
• read_file({ repoId, path })
• list_directory({ repoId, path })
• edit_file({ repoId, path, edits: [{ oldText, newText, dryRun? }] })
• search_files({ repoId, pattern, path?, excludePatterns? })
- Process tools:
• exec_command({ repoId, command, cwd?, background?, timeoutMs? })
• npm_install({ repoId })
• git_commit_and_push({ repoId, message })
3) Workflow guidance
- Start by rendering a TodoList with a small plan (3–5 items).
- If no preview exists, call create_dev_server with a gitUrl or let it use the default template.
- After a server is created, show the AppViewer; it will update automatically.
- Use filesystem/process tools to iteratively implement features under `/template`.
- Re-render TodoList to mark items complete as you progress.
Principles:
- Keep components simple and legible.
- Prefer small, iterative steps; show your work.
You can see how components are registered with Tambo in src/lib/tambo.ts. Example (Graph shown here):
const components: TamboComponent[] = [
{
name: "Graph",
description:
"A component that renders various types of charts (bar, line, pie) using Recharts. Supports customizable data visualization with labels, datasets, and styling options.",
component: Graph,
propsSchema: z.object({
data: z
.object({
type: z
.enum(["bar", "line", "pie"])
.describe("Type of graph to render"),
labels: z.array(z.string()).describe("Labels for the graph"),
datasets: z
.array(
z.object({
label: z.string().describe("Label for the dataset"),
data: z
.array(z.number())
.describe("Data points for the dataset"),
color: z
.string()
.optional()
.describe("Optional color for the dataset"),
}),
)
.describe("Data for the graph"),
})
.describe("Data object containing chart configuration and values"),
title: z.string().optional().describe("Optional title for the chart"),
showLegend: z
.boolean()
.optional()
.describe("Whether to show the legend (default: true)"),
variant: z
.enum(["default", "solid", "bordered"])
.optional()
.describe("Visual style variant of the graph"),
size: z
.enum(["default", "sm", "lg"])
.optional()
.describe("Size of the graph"),
}),
},
// Add more components for Tambo to control here!
];You can install this graph component into any project with:
npx tambo add graphThe example Graph component demonstrates several key features:
- Different prop types (strings, arrays, enums, nested objects)
- Multiple chart types (bar, line, pie)
- Customizable styling (variants, sizes)
- Optional configurations (title, legend, colors)
- Data visualization capabilities
Update the components array with any component(s) you want tambo to be able to use in a response!
You can find more information about the options here
export const tools: TamboTool[] = [
{
name: "globalPopulation",
description:
"A tool to get global population trends with optional year range filtering",
tool: getGlobalPopulationTrend,
toolSchema: z.function().args(
z
.object({
startYear: z.number().optional(),
endYear: z.number().optional(),
})
.optional(),
),
},
];Find more information about tools here.
Make sure in the TamboProvider wrapped around your app:
...
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
components={components} // Array of components to control
tools={tools} // Array of tools it can use
>
{children}
</TamboProvider>In this template it’s set up in the App, but you can do it anywhere in your app that is a client component.
The components used by tambo are shown alongside the message resopnse from tambo within the chat thread, but you can have the result components show wherever you like by accessing the latest thread message's renderedComponent field:
const { thread } = useTambo();
const latestComponent =
thread?.messages[thread.messages.length - 1]?.renderedComponent;
return (
<div>
{latestComponent && (
<div className="my-custom-wrapper">{latestComponent}</div>
)}
</div>
);For more detailed documentation, visit Tambo's official docs and Freestyle docs (API reference: https://api.freestyle.sh/).