Open Source · MIT + GPL v3

One definition. Thirteen platforms.

next-vibe turns a single TypeScript definition into thirteen platforms at once - interactive web UI, CLI command, MCP tool, mobile screen, cron job, WebSocket, admin panel, and more. Full type safety, zero drift, zero repetition.

669+
typed endpoints
0
runtime type errors
13
platforms per endpoint
2
files required
The problem

You've built the same thing thirteen times.

Every feature needs a custom web UI, CLI command, MCP tool, mobile screen, cron job, WebSocket handler, admin panel, and more. Same validation, same i18n, same error handling - just dressed differently. Every time.

Web API
React UI
CLI
AI Tool
MCP Server
React Native
Cron Job
WebSocket
Electron
Admin Panel
VibeFrame Widget
Agent Skill
Vibe Sense Node
next-vibe builds all thirteen from one file.
The Framework

Your AI builds new tools for itself.

Add an endpoint and it immediately becomes a custom interactive UI, CLI command, AI-callable tool, MCP server tool, and scheduled job. The stack is recursive - Claude Code builds next-vibe using next-vibe.

I need an endpoint to list active subscriptions with billing status. Three files, the usual pattern.
Claude Opus 5.5
Delegating to Claude Code. Three files: definition.ts, repository.ts, route.ts. It knows the patterns.
Done. Three files, 0 lint errors. The subscriptions endpoint is live as an interactive admin UI, CLI command, AI-callable tool, and MCP server tool - automatically. No extra wiring.
0.64 Credits
The pattern

Two files required. Every platform.

Each feature lives in a folder. Only definition.ts and route.ts are required - everything else is optional.

definition.ts - the contract

Declare your fields, Zod schemas, labels, error types, and examples once. This file is the single source of truth - the framework reads it at runtime on every platform.

definition.ts
const { POST } = createEndpoint({
  scopedTranslation,
  method: Methods.POST,
  path: ["explain", "to-my-boss"],
  title: "post.title" as const,
  description: "post.description" as const,
  icon: "sparkles",
  category: "endpointCategories.ai",
  allowedRoles: [UserRole.CUSTOMER, UserRole.ADMIN] as const,
  fields: customWidgetObject({
    render: ExplainContainer,
    usage: { request: "data", response: true } as const,
    children: {
      decision: requestField(scopedTranslation, {
        type: WidgetType.FORM_FIELD,
        fieldType: FieldDataType.TEXTAREA,
        label: "post.decision.label" as const,
        columns: 12,
        schema: z.string().min(10),
      }),
      justification: responseField(scopedTranslation, {
        type: WidgetType.TEXT,
        content: "post.justification.content" as const,
        schema: z.string(),
      }),
    },
  }),
  errorTypes: {
    [EndpointErrorTypes.UNAUTHORIZED]:      { title: "post.errors.unauthorized.title" as const,      description: "post.errors.unauthorized.description" as const },
    [EndpointErrorTypes.SERVER_ERROR]:      { title: "post.errors.serverError.title" as const,       description: "post.errors.serverError.description" as const },
    // ... other error types
  },
  successTypes: { title: "post.success.title" as const, description: "post.success.description" as const },
  examples: { requests: { default: { decision: "Rewrite everything in TypeScript" } }, responses: { default: { justification: "..." } } },
});

export type ExplainResponseOutput = typeof POST.types.ResponseOutput;
const definitions = { POST } as const;
export default definitions;

repository.ts - the logic

Business logic lives here - DB queries, auth checks, error handling with success()/fail(). The route.ts is just a thin delegator; validation, logging, and platform registration happen automatically.

repository.ts
import "server-only";

export class ExplainRepository {
  static async explainToMyBoss(
    data: ExplainRequestOutput,
    user: JwtPayloadType,
    logger: EndpointLogger,
    t: ExplainT,
  ): Promise<ResponseType<ExplainResponseOutput>> {
    if (!user.id) {
      return fail({ message: t("post.errors.unauthorized.title"), errorType: ErrorResponseTypes.UNAUTHORIZED });
    }
    try {
      const [saved] = await db
        .insert(explainResults)
        .values({ userId: user.id, decision: data.decision })
        .returning();
      logger.info("Saved decision", { userId: user.id, id: saved.id });
      return success({ justification: saved.justification ?? "" });
    } catch (error) {
      logger.error("Failed to save decision", parseError(error));
      return fail({ message: t("post.errors.serverError.title"), errorType: ErrorResponseTypes.INTERNAL_ERROR });
    }
  }
}

widget.tsx - the UI (optional)

Without a widget, the framework auto-renders your fields everywhere. Add widget.tsx to ship a fully custom interactive UI - the same component renders in admin panels, embedded widgets, and mobile screens.

widget.tsx
"use client";

interface CustomWidgetProps {
  field: {
    value: ExplainResponseOutput | null | undefined;
  } & (typeof definition.POST)["fields"];
}

export function ExplainContainer({ field }: CustomWidgetProps): JSX.Element {
  const children = field.children;
  const emptyField = useMemo(() => ({}), []);

  return (
    <Div className="flex flex-col gap-4 p-4">
      <TextareaFieldWidget fieldName="decision" field={children.decision} />
      <SubmitButtonWidget<typeof definition.POST>
        field={{
          text: "Explain to my boss",
          loadingText: "Explaining...",
          icon: "sparkles",
          variant: "primary",
        }}
      />
      <FormAlertWidget field={emptyField} />
      <AlertWidget
        fieldName="justification"
        field={withValue(children.justification, field.value?.justification, null)}
      />
    </Div>
  );
}
Delete the folder. The feature disappears from every platform at once.
One definition

Write it once. Run it everywhere.

This is a real endpoint from this repo: read the focused window on a Linux desktop. Three files. No glue code. It shows up as a web page, a CLI command, an MCP tool, an AI tool and a cron job.

definition.ts

Path, alias, roles, Zod schema, widget, errors, examples. Everything every surface needs, in one place.

definition.ts
// definition.ts - the ONE source of truth
const { POST } = createEndpoint({
  scopedTranslation,
  method: Methods.POST,
  path: ["desktop", "get-focused-window"],
  aliases: ["desktop-get-focused-window"] as const,
  title: "get-focused-window.title",
  icon: "eye",
  allowedRoles: [UserRole.ADMIN, UserRole.PRODUCTION_OFF],

  fields: customWidgetObject({
    render: GetFocusedWindowWidget, // one widget, every platform
    usage: { request: "data", response: true } as const,
    children: {
      windowTitle: responseField(scopedTranslation, {
        type: WidgetType.TEXT,
        label: "get-focused-window.response.windowTitle",
        schema: z.string().optional(),
      }),
      pid: responseField(scopedTranslation, {
        type: WidgetType.TEXT,
        label: "get-focused-window.response.pid",
        schema: z.number().optional(),
      }),
    },
  }),
  errorTypes: { /* all 9 EndpointErrorTypes - required */ },
  successTypes: { /* title + description */ },
  examples: { /* request + response samples */ },
});

route.ts

Wires the definition to a repository function. That handler is what every surface calls.

route.ts
// route.ts - wire definition to repository
export const { POST, tools } = endpointsHandler({
  endpoint: getFocusedWindowEndpoints,
  [Methods.POST]: {
    email: undefined,
    handler: ({ t, logger }) =>
      DesktopWindowRepository.getFocusedWindow(t, logger),
  },
});

What you get without writing more code

CLI command

The alias becomes a command. Output is rendered by the same widget, laid out for the terminal.
terminal
$ vibe desktop-get-focused-window

  ◆ Focused Window
    Mozilla Firefox
    pid: 2820  2293×916  monitor: DP-2

MCP tool

Point any MCP client at vibe mcp. Schemas come straight from the Zod fields.
.mcp.json
// .mcp.json - any MCP client (Claude, Cursor, ...)
{
  "mcpServers": {
    "my-app": { "command": "vibe", "args": ["mcp"] }
  }
}

// the client now lists: desktop-get-focused-window
// input and output schemas come from the Zod fields above

AI-callable tool

Models get a function-calling schema generated from the definition. Roles decide who may call it.
tool-call
// Same definition, exposed as a function-calling tool.
// The model reads the Zod schema + description. No wrapper.
//
//   user:  "What window am I looking at?"
//   model: desktop-get-focused-window({})
//   tool:  { windowTitle: "Mozilla Firefox", pid: 2820 }
//
// Hide it from the model: add UserRole.AI_TOOL_OFF

Cron job

Same handler, now on a schedule. This is the real task file of our credit-expiry job.
task.ts
// task.ts - the same handler on a schedule
const task = createCronTask(
  definition.POST,
  tools.POST, // <- the route handler from route.ts
  {
    id: "credits-expire",
    schedule: CRON_SCHEDULES.DAILY_MIDNIGHT,
    category: TaskCategory.MAINTENANCE,
    priority: CronTaskPriority.MEDIUM,
    timeout: TASK_TIMEOUTS.SHORT,
    enabled: true,
    taskInput: undefined,
  },
);
export const tasks: Task[] = [task];
Plus a web page at /tools/<alias>, a typed REST route and a native screen. The widget renders on every one of them. Delete the endpoint folder and every surface disappears with it.

Add your own in five lines of shell

Create the folder, write the three files, regenerate, check. If vibe check says 0 errors, it works everywhere.

terminal
mkdir -p src/desktop/get-focused-window
# write definition.ts, repository.ts, route.ts (+ widget.tsx)

vibe gen    # regenerates route, CLI, MCP and tool registries
vibe check src/desktop/get-focused-window   # 0 errors or it's not done

vibe desktop-get-focused-window             # CLI, right now
# web:   /en-US/tools/desktop-get-focused-window
# MCP:   already listed - restart your client
The Framework

One definition. 13 platforms. Zero drift.

Write one endpoint definition. Get an interactive web UI, CLI command, AI tool, MCP server, cron job, WebSocket, mobile screen, desktop app, admin panel, agent skill, and more - automatically. Type-safe end-to-end. Role-controlled access. Zero boilerplate.

const { POST } = createEndpoint({
  method: Methods.POST,
  path: ["greet"],
  allowedRoles: [UserRole.PUBLIC, UserRole.CUSTOMER, UserRole.ADMIN],
  title: "Greet user",
  description: "Returns a personalised greeting",
  icon: "hand-wave",
  tags: ["greetings"],
  fields: objectField({
    type: WidgetType.CONTAINER,
    title: "Greeting form",
    usage: { request: "data", response: true },
    children: {
      name: requestField({
        schema: z.string().min(1).max(100),
        type: WidgetType.FORM_FIELD,
        fieldType: FieldDataType.TEXT,
        label: "Your name",
        placeholder: "e.g. Alice",
      }),
      message: responseField({
        schema: z.string(),
        type: WidgetType.ALERT,
      }),
      submitButton: widgetField({
        type: WidgetType.SUBMIT_BUTTON,
        text: "Say hello",
       }),
    },
  }),
  examples: {
    requests:  { simpleGreeting: { name: "Alice" } },
    responses: { simpleGreeting: { message: "Hello, Alice!" } },
  },
});
automatically becomes
🌍fetch()

Add a path. The REST endpoint exists.

No routing config. No controllers. The moment you define a path and schema, the endpoint is live at /api/[locale]/[path]. Role checks, validation, and error shapes happen before your code runs.

const res = await fetch("/api/en-GLOBAL/greet", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice" }),
});
const { message } = await res.json();
// message → "Hello, Alice!"

Your AI builds new tools for itself

Add an endpoint → it immediately becomes an AI-callable tool, CLI command, live web UI, and scheduled job. The stack is recursive. Claude Code builds next-vibe using next-vibe.

✓Type-safe end to end✓Role-controlled access✓Zod-validated✓Zero boilerplate
Built with next-vibe

From a car dashboard to an AI platform.

Same codebase, any surface. These are real products running on next-vibe - a car PC, online shops, booking sites, and unbottled.ai itself. One framework, wildly different products.

In-car dashboard

A full car headunit

Navigation, climate, radio, Spotify, Bluetooth calls, and live vehicle diagnostics - plus Android Auto - on a touchscreen car PC. Built as a next-vibe fork.
E-commerce

Online shops that sell

Storefront, cart, checkout, inventory, and point of sale from one install. A real shop runs on it today.
Bookings

Practitioner & booking sites

Appointments, client records, invoicing, and a public site - the backbone of a practitioner business. Same stack as the shop.
AI platform

unbottled.ai

189 models, chat, persistent memory, agents, billing, and MCP - the flagship. This exact codebase, in production.
TypeScript supremacy

No any. No unknown. No throw.

Types must align completely - no exceptions. This isn't a style preference. It's a structural rule enforced at build time by vibe check.

no any

Replace with a real typed interface. If you reach for any, your architecture has a hole.

no unknown

Same rule. unknown is just any with extra steps. Define the type.

no bare object

Bare object is meaningless. Write the shape you actually expect.

no as X

Type assertions are lies to the compiler. Fix the architecture instead.

no throw

Use ResponseType<T> with success(data) or fail({message, errorType}). Errors are data.

no hardcoded strings

Every string needs a translation key. The checker catches untranslated literals.

vibe check runs Oxlint (Rust), ESLint, and TypeScript type checking in parallel. Zero errors required before ship.
Get started

Fork, ask, ship.

You're at unbottled.ai's level from day one.

1

Fork & clone

Fork on GitHub, then clone your fork locally.

terminal
git clone https://github.com/YOUR_USERNAME/next-vibe
cd next-vibe && bun install
2

Start the server

For local development, vibe dev starts PostgreSQL in Docker, runs migrations, seeds data, and gives you hot reload. For production, vibe build && vibe start does the initial deploy. vibe rebuild is what you use after that to update production with zero downtime.

terminal
# development
vibe dev

# production - first start
vibe build && vibe start

# production - after changes
vibe rebuild
3

Login as Admin

Open the app and click "Login as Admin" - the setup wizard walks you through API keys and admin password.

4

Ask the AI

Open unbottled.ai chat or Claude Code and describe the feature you want. The AI writes all the files - definition, route, widget, i18n - and runs vibe check automatically.

prompt
# In unbottled.ai chat or Claude Code:
"Build me a feature that explains decisions to my boss"
# AI writes all files and runs vibe check automatically
Enterprise

Building something big?

We help teams with setup, custom integrations, architecture review, and ongoing development support. Same codebase, your deployment.