Key Concepts
How agent-native apps work under the hood — and why they're built this way.
Why agent-native
Teams today have four options for AI-powered work, and none of them are ideal:
- Chat apps (Claude Projects, ChatGPT) — accessible but not built for structured workflows. No persistent UI, no dashboards, no team collaboration.
- Raw agent interfaces (Claude Code, Cursor) — powerful but inaccessible to non-devs. No guardrails, no onboarding, no structured UI.
- Custom AI apps — limited. The AI can't see what you see, can't react to what you click, and can't update the app itself. No conversation history, no rollback, no skills.
- Existing SaaS (Amplitude, HubSpot, Google Slides) — bolting AI onto architectures that weren't designed for it. You can feel the seams.
Agent-native apps solve this by making the agent and the UI equal citizens of the same system. Think of it as Claude Code, but with buttons and visual interfaces. The agent can do anything the UI can do (via natural language), and the UI can trigger anything the agent can do (via buttons).
This is the same shift we saw with mobile-native. Instagram didn't shrink a desktop app — they built mobile-first with extreme discipline. Agent-native means every feature is tested against one question: will AI be able to work with this reliably? If yes, ship it. If not, don't.
The architecture
Every agent-native app is three things working together:
Autonomous AI that reads, writes, and executes code. Customizable with skills and instructions.
Full React UI with dashboards, flows, and visualizations. Guided experiences your team can use.
File system, browser, code execution. Agents work directly with files and tools — no MCPs needed.
The app runs inside a harness — a host environment that provides the agent alongside the app UI. The simplest harness is a terminal on the left (running Claude Code) and your app iframe on the right. Cloud harnesses add collaboration, visual editing, and managed infrastructure for teams.
Five rules govern the architecture:
- Files are the database — all app state lives in files
- All AI goes through the agent — no inline LLM calls
- Scripts for agent operations — complex work runs as scripts
- SSE keeps the UI in sync — file changes stream to the browser in real-time
- The agent can modify code — the app evolves as you use it
Files as database
This is the core insight that makes the architecture work. All application state — content, data, configuration — lives in files (JSON, Markdown, YAML) in the data/ directory. There is no traditional database.
Why files? Because agents are excellent at reading, writing, grepping, and navigating file trees. When state is files:
- The agent can read and modify any state directly — no API wrappers needed
- The UI reads state via API routes that serve files
- Both sides operate on the same source of truth
- State is versionable with git
- State is inspectable —
cat data/projects/my-project.json
# The agent reads files directly
cat data/dashboards/main.json
# The UI reads via API routes that serve the same files
GET /api/dashboards/main → reads data/dashboards/main.json
# Both sides see the same stateYour app becomes a function of files — like React is a function of state, an agent-native app is a function of files. When the agent writes a file, the UI updates. When the UI saves data, the agent can see it.
Agent chat bridge
The UI never calls an LLM directly. When a user clicks "Generate chart" or "Write summary", the UI sends a message to the agent via postMessage. The agent does the work — with full conversation history, skills, instructions, and the ability to iterate.
// In a React component — delegate AI work to the agent
import { sendToAgentChat } from "@agent-native/core";
sendToAgentChat({
message: "Generate a chart showing signups by source",
context: "Data source: data/analytics/signups.json",
submit: true,
});Why not call an LLM inline?
- AI is non-deterministic. You need conversation flow to give feedback and iterate — not one-shot buttons.
- Context matters. The agent has your full codebase, instructions, skills, and history. An inline call has none of that.
- The agent can do more. It can run scripts, browse the web, modify code, and chain multiple steps together.
- Headless execution. Because everything goes through the agent, any app can be driven entirely from Slack, Telegram, or another agent.
The transport is simple: window.parent.postMessage() in the browser. The harness (Claude Code wrapper or Builder) receives the message and types it into the agent. From scripts, the same bridge works via stdout (BUILDER_PARENT_MESSAGE: prefix).
Scripts system
When the agent needs to do something complex — call an API, process data, generate images — it runs a script. Scripts are TypeScript files in scripts/ that export a default async function:
// scripts/fetch-data.ts
import { parseArgs } from "@agent-native/core";
export default async function fetchData(args: string[]) {
const { source } = parseArgs(args);
const res = await fetch(`https://api.example.com/${source}`);
const data = await res.json();
// Write results to data/ — the UI will see the change via SSE
const fs = await import("node:fs/promises");
await fs.writeFile(`data/${source}.json`, JSON.stringify(data, null, 2));
console.log(`Fetched ${data.length} records`);
}# Agent runs scripts via CLI
pnpm script fetch-data --source=signupsThis means anything the UI can do, the agent can do — and vice versa. The UI calls POST /api/fetch-data, the agent calls pnpm script fetch-data. Same code, same results, different entry points.
Real-time SSE sync
When the agent writes a file, the UI needs to know immediately. A chokidar file watcher monitors data/ and streams changes to the browser via Server-Sent Events:
// Server: set up file watching and SSE
import { createFileWatcher, createSSEHandler } from "@agent-native/core";
const watcher = createFileWatcher("./data");
app.get("/api/events", createSSEHandler(watcher));
// Client: invalidate react-query caches on file changes
import { useFileWatcher } from "@agent-native/core";
useFileWatcher({ queryClient, queryKeys: ["dashboards", "projects"] });The flow is:
- Agent writes to
data/dashboards/main.json - Chokidar detects the change
- SSE pushes
{ "type": "change", "path": "data/dashboards/main.json" }to the browser useFileWatcherinvalidates matching react-query caches- Components re-fetch and render the new data
No polling, no refresh — the UI updates instantly when the agent acts.
Harnesses
Agent-native apps don't run standalone — they run inside a harness that provides the AI agent alongside the app UI. This is what makes the architecture work: the agent needs a computer (file system, browser, code execution), and the app needs the agent for AI work.
Terminal on the left running your choice of AI CLI (Claude Code, Codex, Gemini, OpenCode), your app iframe on the right. Runs locally. Free. Best for solo development.
Deploy to any cloud with real-time collaboration, visual editing, roles and permissions. Best for teams.
Both harnesses support the same protocol: postMessage bridge for chat, SSE for file sync, and the script system. Your app code is identical regardless of harness.
Database adapters
Files are great for single-user and local development. But when multiple people need to collaborate in real-time across different agent instances, you need a sync layer.
Agent-native provides a pluggable adapter system that syncs files to a remote database in real-time. Three adapters ship out of the box:
Real-time listener via onSnapshot. Best for apps already on Google Cloud or Firebase.
Real-time via Supabase Realtime channels. Best for teams using Supabase for auth, storage, or edge functions.
Polling-based sync via serverless SQL. Best for serverless-first architectures and Vercel deployments.
All adapters work the same way under the hood:
- A chokidar file watcher detects local changes and pushes them to the database
- A remote listener (real-time or polling) detects remote changes and writes them to disk
- Three-way merge with LCS-based conflict resolution handles concurrent edits
- Unresolvable conflicts create
.conflictsidecar files for manual or LLM-assisted resolution
The app doesn't know about the database — it just reads and writes files. The adapter handles sync behind the scenes. You configure which files sync via glob patterns:
// data/sync-config.json
{
"syncFilePatterns": ["data/projects/**/*.json", "data/**/*.md"],
"privateSyncFilePatterns": ["data/users/**/*.json"]
}This is important: the database is never the source of truth. Files are. The database is just a sync mechanism for collaboration. Git-ignore the synced files, and pull requests update the application code — not the data files.
APIs & CLIs, not MCPs
Agent-native apps can work with MCP servers, but the architecture leans heavily on something more standard: regular APIs and CLIs accessed through code execution.
Why? Because APIs and CLIs are universal. Every service already has them. They're documented, versioned, and battle-tested. Agents are great at writing code that calls fetch() or runs a CLI command — no special protocol needed.
// scripts/sync-stripe.ts — agent calls Stripe's REST API directly
import { parseArgs } from "@agent-native/core";
export default async function(args: string[]) {
const { customerId } = parseArgs(args);
const res = await fetch(`https://api.stripe.com/v1/customers/${customerId}`, {
headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
});
const customer = await res.json();
const fs = await import("node:fs/promises");
await fs.writeFile("data/customer.json", JSON.stringify(customer, null, 2));
}The scripts system is the key enabler. When the agent needs to interact with an external service, it writes (or runs) a script that uses the service's standard API or CLI. This means:
- No wrapper layer. Call APIs directly with
fetch()or use official SDKs. - Any CLI works.
ffmpeg,gh,aws,gcloud— if it runs in a terminal, the agent can use it. - Code is the protocol. TypeScript scripts are more expressive than any tool schema. The agent can chain calls, handle errors, transform data — all in regular code.
- MCP is additive. If you want to use MCP servers too, they work fine alongside scripts. But they're not required.
Agent modifies code
This is a feature, not a bug. The agent can edit the app's own source code — components, routes, styles, scripts. This enables a "fork and evolve" pattern:
- Fork a template (e.g. the analytics template)
- Customize it to your needs by asking the agent
- "Add a new chart type for cohort analysis" — the agent builds it
- "Connect to our Stripe account" — the agent writes the integration
- Your app gets better over time without manual development
This works because the agent has your full codebase. It can read your components, understand your patterns, and make changes that fit. Combined with git-based workflows, roles, and ACLs, you get the power of custom development with the safety of code review.