Back to Blog
Architecture
12 min read

One Codebase. 13 Platforms. Zero Compromises.

How three files become a web form, CLI command, MCP tool, native screen, and automated job - simultaneously.

definition.ts → web · cli · mcp · native · cron · 10 more

5,802 TypeScript files. ~2.1 million lines. Zero `any`. Zero runtime type errors. One pattern. Repeated 374 times.

That's the codebase behind unbottled.ai - and the framework that powers it, next-vibe. The same architecture runs a web app, a mobile app, a CLI, an AI agent interface, an MCP server, a cron system, a websocket event bus, and a live dataflow engine.

The pattern is called the Unified Surface. Here's what it is, how it works, and why - once you see it - you'll find it hard to go back.

A feature is a folder

Every feature in next-vibe lives in a folder. Three files are required. Everything else is optional.

bash
1explain-to-my-boss/
2  definition.ts    ← what it does
3  repository.ts    ← how it does it
4  route.ts         ← makes it exist everywhere
5  widget.tsx       ← custom React UI (optional)
6  widget.cli.tsx   ← custom terminal UI (optional)

That's it. One folder. Three required files. And from those three files, that feature exists on up to 13 platforms simultaneously.

13 platforms from 3 files

When you add a feature to next-vibe, it doesn't just become an API endpoint. It becomes everything at once.

Web API

REST endpoint, auto-validated, fully typed

React UI

Auto-generated from the definition - no JSX written

CLI

Every endpoint is a command with auto-generated flags

AI Tool Schema

Function calling schema generated automatically

MCP Server

Plug into Claude Desktop, Cursor, or any MCP client

React Native

iOS and Android screens from the same definition

Cron Job

Schedule any endpoint to run on a timer

WebSocket Events

Push updates to connected clients on completion

Electron Desktop

Native desktop app via the same endpoint contracts

Admin Panel

Auto-generated admin UI, no bespoke code needed

VibeFrame Widget

Embeddable iframe widget for any website

Agent Skill

Callable by AI agents as a structured skill

Vibe Sense Node

Node in a live dataflow graph - same endpoint

Delete the folder. The feature ceases to exist everywhere at once.

Platform access is one enum array

You don't write separate permission layers for each platform. Platform access is declared in the definition itself - one enum array that every platform reads natively at runtime.

typescript
1// This single array controls where the feature appears
2allowedRoles: [
3  CLI_OFF,         // blocks the CLI
4  MCP_VISIBLE,     // opts into the MCP tool list
5  REMOTE_SKILL,    // puts it in the agent skill file
6  PRODUCTION_OFF,  // disables it in prod
7]

Same definition. Same place. No config files to sync. No separate permission systems per platform.

There is no API for humans and API for AI. There's just the tool.

The live demo: Thea builds an endpoint

Instead of explaining the pattern abstractly, let me show you what it looks like in practice.

Who is Thea?

Thea is the AI admin for this platform. She runs on production 24/7, operating through the exact same endpoint contracts as every user - same validation, same permissions, no backdoor. And she can delegate work to a local machine.

I asked Thea to build a new endpoint - explain-to-my-boss - using Claude Code running on my PC. You give it a technical decision. It gives you a non-technical justification your manager will actually believe. Every developer has needed this.

The live demo: Thea builds an endpoint

1
You

Ask Thea

Type the task in the chat - two input fields, one AI-generated response, all platforms, MCP_VISIBLE, custom React and CLI widgets.

2
Thea

Creates task

Thea reasons out loud, creates a task with targetInstance="hermes" (your local machine), and goes dormant.

3
Local Hermes

Picks up the task

The local instance syncs every 60 seconds. No open ports. Your machine initiates the connection.

4
Claude Code

Builds the endpoint

Interactive session. Reads existing patterns first, creates five files, runs vibe check. Zero errors.

5
Claude Code

Reports completion

Calls complete-task with the task ID. Status: completed. Summary attached.

6
Thea

Wakes up

wakeUp fires. Thea resumes mid-conversation via websocket, streams her response, TTS speaks.

7
Result

Exists everywhere

Web form. CLI command. MCP tool. React Native screen. All live. From five files.

The proof

Once Claude Code called complete-task, three things existed that didn't five minutes before:

A custom React widget - dramatic heading, animated gradient on the AI output, a fake corporate alignment score.

A CLI widget - ASCII banner, spinner while the AI thinks, the justification printed line by line in green.

An MCP tool - explain-to-my-boss_POST - because MCP_VISIBLE was in the definition. Claude Desktop can now explain your decisions to your boss on your behalf.

One definition. Five files total. Three completely different UIs. The endpoint contract didn't change. Only the presentation layer did.

Under the hood

definition.ts - the living contract

The definition is not a code generator. It's a living contract that every platform reads natively at runtime. Change it - everything updates. Delete the folder - nothing breaks downstream. There's no generated code to clean up.

typescript
1// definition.ts
2const { POST } = createEndpoint({
3  scopedTranslation,
4  aliases: [EXPLAIN_TO_MY_BOSS_ALIAS],
5  method: Methods.POST,
6  path: ["explain", "to-my-boss"],
7  title: "post.title",
8  description: "post.description",
9  icon: "sparkles",
10  category: "endpointCategories.ai",
11  allowedRoles: [UserRole.CUSTOMER, UserRole.ADMIN] as const,
12  fields: objectField(scopedTranslation, {
13    type: WidgetType.CONTAINER,
14    usage: { request: "data", response: true },
15    children: {
16      decision: requestField(scopedTranslation, {
17        type: WidgetType.FORM_FIELD,
18        fieldType: FieldDataType.TEXTAREA,
19        label: "post.fields.decision.label",
20        description: "post.fields.decision.description",
21        schema: z.string().min(1).max(2000),
22        columns: 12,
23      }),
24      justification: responseField(scopedTranslation, {
25        type: WidgetType.TEXT,
26        content: "post.fields.justification.content",
27        schema: z.string(),
28      }),
29    },
30  }),
31  errorTypes: { /* ... all 9 required error types ... */ },
32  successTypes: { title: "post.success.title", description: "post.success.description" },
33  examples: { requests: { default: { decision: "Migrate to Bun" } }, responses: { default: { justification: "..." } } },
34});

repository.ts - no throw, ever

Repository functions never throw. Errors propagate as data - typed, explicit, and catchable by the caller. The AI can reason about failure paths. No surprise exceptions.

typescript
1// repository.ts
2// Returns ResponseType<T> - never throws
3export async function explainToMyBoss(
4  data: { decision: string },
5  logger: Logger,
6): Promise<ResponseType<{ justification: string }>> {
7  const result = await ai.generateText({
8    prompt: buildPrompt(data.decision),
9  });
10  if (!result.text) {
11    return fail({ message: "AI returned empty response", errorType: EndpointErrorTypes.SERVER_ERROR });
12  }
13  return success({ justification: result.text });
14}

route.ts - the entire bridge

route.ts wires the definition to the handler. endpointsHandler takes care of validation, auth, logging, and exposure to all 13 platforms. The actual business logic is one line.

typescript
1// route.ts
2import { endpointsHandler } from "@/app/api/[locale]/system/unified-interface/shared/endpoints/route/multi";
3import { Methods } from "@/app/api/[locale]/system/unified-interface/shared/types/enums";
4import definitions from "./definition";
5import { explainToMyBoss } from "./repository";
6
7export const { POST, tools } = endpointsHandler({
8  endpoint: definitions,
9  [Methods.POST]: { handler: ({ data, logger }) => explainToMyBoss(data, logger) },
10});

The numbers

374 endpoints

One pattern, applied 374 times

Zero `any`

Enforced at build time, not convention

Three languages

Compile-time checked - t("typo.here") is a compiler error

That's not convention. It's enforced at build time.

One pattern. Repeated 374 times.
Up next
Vibe Sense: The pipeline is the platform

Every node in a Vibe Sense graph is a regular next-vibe endpoint. The same createEndpoint(). The same 3-file structure. An EMA indicator is an endpoint. A threshold evaluator is an endpoint. And because it's an endpoint - you can call it from the CLI, from the AI, from anywhere.

vibe analytics/indicators/ema --source=leads_created --period=7

The pipeline is the platform.

Vibe Sense is just... more endpoints. The same thing, applied to time series data. Lead funnels. Credit economy. User growth. Your platform watching itself.

Back to Blog

Define it once. It exists everywhere.

WordPress gave everyone the power to publish. next-vibe gives you the power to build platforms that work on web, mobile, CLI, AI agents, and automation - that watch themselves, reason about their own data, and act on what they find.

Star next-vibe on GitHub