All writing

11 min readfirst published on LinkedIn

AI-Native, Not AI-Powered: Redesigning Our Product Around the API

We wrapped our existing REST API, 50+ endpoints, as tools in one chat route. Zero backend changes. What AI-native means, and the pattern behind it.

  • ai agents
  • architecture
  • vercel ai sdk
  • claude

Every AI product discussion eventually arrives at "we need an AI layer." A new service. New endpoints. New deployment. Another box on the architecture diagram.

We explored that path. It was getting complicated: more plumbing than product, more time on infrastructure than on the experience. So we tried the simplest thing we could think of: we wrapped our existing REST API (50+ endpoints, unchanged) as tools in a single chat route handler. The backend never knew an AI agent was calling it. Zero new backend code.

That turned our traditional SaaS into something architecturally different. Not AI-powered. AI-native.

AI-Powered vs AI-Native

Beforetraditional app

User
clicks
Pagesdashboard · artifacts · runs · scheduler
React Query hooksserver actions
API clientcalls endpoints, handles auth
REST backendfastapi on modal

AfterAI-native

User
one sentence
app/api/chat/route.tsa single chat route
Claude30+ tools
API clientthe same one
REST backendunchanged
fig 1 · before and after

The industry uses these interchangeably. They're not the same thing, and the difference has real architectural consequences.

AI-powered means AI is a feature inside an existing product. You can remove it and users still complete their workflows. Think of a CRM with an AI-written email draft, or a dashboard with an AI summary panel, or a code editor with autocomplete. Valuable, but the underlying interaction model is unchanged: pages, navigation, forms, buttons. The AI makes existing workflows faster or smarter. It doesn't create new ones.

The key characteristic: the product was designed without AI, and AI was added later. The information architecture, the navigation, the page structure, the data model all predate the AI. Remove the AI features and you still have a complete product. Users might miss the convenience, but they can do everything they did before.

AI-native means AI is the primary interface. The user expresses intent in natural language, and the AI decides how to fulfill it: which operations to run, in what order, with what parameters. There is no predetermined page for most workflows. The AI composes them on the fly based on what the user asks for.

The key characteristic: remove the AI and you don't have a slower product. You have a blank text input with nothing behind it. The product was designed around the AI from the start (or, in our case, redesigned around it). The AI isn't accelerating a workflow. It is the workflow.

Most products today sit in the AI-powered camp. That's fine. It's the safer bet. But there's a class of products where AI-native is the clearly better architecture: products where the user's workflows are varied, multi-step, and hard to predict. Where the "right" sequence of operations depends heavily on context. Where building a dedicated page for every workflow would mean building hundreds of pages.

Compliance is one of those domains. The question "check my SOC 2 posture and generate a report" involves discovering available scans, running one, waiting for results, analyzing findings, composing a document, rendering it to PDF. In a traditional app, that's twelve clicks across four pages. In an AI-native app, it's one sentence. Not because we made the twelve clicks faster, but because the AI composes the entire workflow from intent.

Here's a practical test: look at your most complex user workflow. Count the clicks, the page navigations, the decisions about where to go next. Now imagine the user describing the outcome in one sentence. If your system could execute that, choosing the right operations, filling the right parameters, handling edge cases, you've crossed from AI-powered to AI-native.

Why Building the "AI Layer" on the Backend Failed

The obvious move was putting the chat layer on our Python backend. It already ran Claude for scans, had direct data access, all the context.

Three problems, some specific to our stack, but the tensions are common:

Serverless latency hit us hard. Our backend runs on Modal. Cold starts take seconds, which is fine for REST responses but brutal for streaming chat. The AI layer needed to be closer to the user. (If your backend has consistent low latency, this matters less.)

Multi-layer streaming is non-trivial. LLM to Modal container to FastAPI to Next.js to browser. Four SSE layers, each with its own buffering and failure modes. Solvable, but significant complexity for something the frontend could handle directly.

We'd have been reimplementing frontend concerns in Python. Auth, caching, retries, session management: our frontend already had all of it. (If your backend owns these concerns, the calculus differs.)

The insight: our REST API already abstracted data access. The AI layer could consume it, not bypass it.

The Pattern

Your frontend API client (functions that call endpoints, handle auth, parse responses) is already a tool library. Each function has a name, typed input, structured output, side effects. That's the exact shape of an AI tool.

text
[User message] > [LLM with tools] > [Existing API client functions] > [Unchanged backend]

The LLM decides which calls to make and in what order. The backend sees identical authenticated requests whether they come from a button click or Claude.

Here's the core, a single streamText call wrapping our existing API client:

ts
// app/api/chat/route.ts: the entire AI layer

const result = streamText({
  model: anthropic("claude-sonnet-4-20250514"),
  providerOptions: {
    anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } },
  },
  system: systemPrompt + sessionContext,
  messages: modelMessages,
  tools: {
    listApps: tool({
      description: "List available scan apps the user can run.",
      inputSchema: z.object({}),
      execute: async () => {
        // getProjects(): the SAME function our traditional UI calls
        const projects = await withRetry(() =>
          getProjects("main", undefined, cookieHeader, orgId ?? undefined)
        );
        return { apps: projects.map((p) => ({ id: p.id, name: p.name })) };
      },
    }),
    runApp: tool({ /* wraps executeProjectQuery() */ }),
    getSessionDetail: tool({ /* wraps getProjectFiles() */ }),
    // ... 30 more tools, each wrapping an existing API client function
  },
  stopWhen: stepCountIs(12),
});

return result.toUIMessageStreamResponse({ sendReasoning: true });

For us: 30+ tools across eight areas (scans, files, artifacts, dashboard, conversations, groups, scheduling, document generation). No new backend service. No schema duplication, since tool schemas map directly to API parameters. Auth passes through transparently using the same Clerk cookies and org headers the UI uses.

The System Prompt Is Your Router

In a traditional app, the router maps URLs to code. In an AI-native app, the system prompt maps intent to tool sequences.

Ours handles three things:

Routing. "When the user says 'rerun,' call rerunSession. When they say 'new scan,' call listApps then runApp." Natural-language pattern matching.

Constraints. "Confirm before destructive operations." "If you hit the tool limit, tell the user what's done and what remains." Middleware, expressed as instructions.

Context injection. We serialize recent sessions into the prompt on every request. The AI already knows the user's environment. "What's my most critical finding?" resolves from context without a tool call.

The system prompt is the most-edited file in our codebase. Every confusing user interaction traces back to it: a routing rule that needed refinement, an edge case that wasn't covered. Treat it as architecture, not a personality description.

The Hard Problem: Two Cache Worlds

When you have both a chat interface and a traditional UI operating on shared state, you get a novel problem: the AI mutates state server-side, the traditional UI caches client-side.

Chat creates an artifact folder. The artifacts page in another tab has a stale cache.

We solve this in two layers. Server-side: a per-request Map deduplicates reads within a single AI response, and mutation tools invalidate relevant keys. Client-side: when the response finishes, we scan for mutation tool calls and invalidate the corresponding React Query keys. The traditional UI refetches automatically.

This is an underappreciated concern. Most AI products today are chat-only or treat AI as a side feature. The moment both interfaces mutate shared state, you need a coherence strategy.

When REST Wrappers Aren't Enough: Two-Tier AI Delegation

Chat agentorchestrator, not executor

tier 1

REST API tools

90% of tasks: reads, mutations, CRUD

  • scans
  • files
  • artifacts
  • dashboard
  • conversations
  • groups
  • scheduling
  • documents
REST backendunchanged

tier 2

Anthropic Skills

real code in a sandboxed Python container

  • pdf reportlab
  • docx python-docx
  • xlsx openpyxl
  • pptx python-pptx
File back to the chata downloadable artifact
fig 2 · two-tier delegation

Wrapping REST endpoints covers 90% of tasks: reads, mutations, CRUD orchestration. But some tasks need more. Real computation, specialized libraries, multi-turn reasoning with higher token budgets. Document generation, data transformation, complex analysis. There's no REST endpoint to wrap because the task doesn't exist yet. It's being composed on the fly from user intent.

For these, we use a second tier: Anthropic Skills. The chat agent delegates to a sandboxed Python container with real libraries like reportlab for PDFs, python-docx for Word documents, openpyxl for spreadsheets, and python-pptx for slide decks. The container runs multi-turn: Claude writes code, executes it, inspects results, iterates. The finished file comes back to the chat agent as a downloadable artifact.

Here's the core idea in code. The chat tool composes content, delegates rendering to a skill, and falls back to local generation if the container fails:

ts
generateDocument: tool({
  execute: async ({ format, title, content }) => {
    try {
      // Tier 2: spin up sandboxed Python container via Anthropic Skills
      const { buffer } = await executeSkill(format, `Generate a ${format} file...\n${content}`);
      return { url: await upload(buffer) };
    } catch {
      // Fallback: local jsPDF/docx if skill container fails
      const { buffer } = await generateLocal(format, title, content);
      return { url: await upload(buffer) };
    }
  },
}),

Inside executeSkill, the Anthropic SDK spins up a container with the requested skill (pdf, docx, xlsx, pptx), Claude writes and executes Python code inside it, and if the task needs multiple rounds (complex charts, multi-sheet spreadsheets), the container pauses and resumes automatically up to 5 times. The finished file downloads via the Files API.

The pattern generalizes beyond document generation:

  • Report generation. "Generate a SOC 2 readiness report from last week's findings." The chat agent composes the content from session data, then delegates rendering to a skill with reportlab. No template builder. No configuration page. The user's intent is the template.
  • Data transformation. "Turn these scan results into an Excel breakdown by severity." The agent structures the data, delegates to a skill with openpyxl that produces a real spreadsheet with formulas, conditional formatting, multiple sheets.
  • Presentation creation. "Build a board deck summarizing our Q1 compliance posture." Same pattern: the agent composes narrative from live data, a skill renders polished slides.
  • Complex analysis. Any task where the agent needs to write and execute code against data (statistical analysis, chart generation, format conversion) fits this delegation model.

The architectural principle: the chat agent is the orchestrator, not the executor. It decides what needs to happen and composes the inputs. For lightweight operations (CRUD, queries, state changes), it calls REST tools directly. For heavy computation that needs a runtime environment, it delegates to a skill. Two tiers of AI, each doing what it's good at.

This runs entirely from the frontend. The chat route handler calls the Anthropic SDK directly to spin up a skill container with no backend involvement. The container executes, returns a file, the tool uploads it and returns a URL. One more tool in the same route file, same pattern, just a different execution backend.

You could build each of these as dedicated AI-powered features: a report builder, a data export wizard, a presentation tool. But that's four features with four UIs. The two-tier pattern gives you all of them through a single chat interface, with the quality and accuracy that comes from real code execution in a sandboxed environment.

Advice for Architects

Your API client is your tool library. Import the same functions your UI uses. Clean function signatures become tools with minimal glue.

Treat the system prompt like routing config. Version it. Review changes. Test against ambiguous inputs. It will change more than any other file.

Start read-only, earn mutations. Ship read tools first. They're safe and immediately useful. Add mutations with confirmation guards. Destructive operations last.

Design for cache coherence from day one. If you keep traditional UI (you should), decide how AI-side mutations propagate to UI-side caches before you ship.

AI-native doesn't mean chat-only. Chat for intent-driven workflows. Traditional UI for browsing, comparing, at-a-glance status. They complement each other.

Consider putting the AI near the user, not the data. If you have a clean API layer, the AI orchestration can live in the frontend, consuming your API like the UI does. Not always right (data-heavy agents or RAG may need to be near storage) but for orchestrating CRUD operations, frontend placement was simpler for us.

For us, AI-native was less work than AI-powered. No new service, no new auth, no dedicated UIs per feature. Import the API client, write descriptions, write a system prompt, ship one route. Your mileage will vary, but the asymmetry surprised us.

The Bottom Line

If your REST API is well-structured, you already have most of what an AI agent needs. The endpoints are tools. Someone already wrote, tested, and deployed them.

The AI layer is just the decision-making part: choosing which tools to call and in what order. Which is exactly what your users do manually, clicking through your UI, one page at a time.

For us, it was one route file and zero backend changes. If you have a clean API layer, the starting line might be closer than you think.

This post covers the architecture at a high level. If you'd like a full walkthrough of the implementation (tool anatomy, system prompt design, cache invalidation patterns, the two-tier delegation pipeline with Anthropic Skills, and how it all fits together with real code), let me know. I'll put together a deep-dive in the next post.