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.
typed endpoints
runtime type errors
platforms per endpoint
files required
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.
next-vibe builds all thirteen from one file.
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.
1const { POST } = createEndpoint({
2 scopedTranslation,
3 method: Methods.POST,
4 path: ["explain", "to-my-boss"],
5 title: "post.title" as const,
6 description: "post.description" as const,
7 icon: "sparkles",
8 category: "endpointCategories.ai",
9 allowedRoles: [UserRole.CUSTOMER, UserRole.ADMIN] as const,
10 fields: customWidgetObject({
11 render: ExplainContainer,
12 usage: { request: "data", response: true } as const,
13 children: {
14 decision: requestField(scopedTranslation, {
15 type: WidgetType.FORM_FIELD,
16 fieldType: FieldDataType.TEXTAREA,
17 label: "post.decision.label" as const,
18 columns: 12,
19 schema: z.string().min(10),
20 }),
21 justification: responseField(scopedTranslation, {
22 type: WidgetType.TEXT,
23 content: "post.justification.content" as const,
24 schema: z.string(),
25 }),
26 },
27 }),
28 errorTypes: {
29 [EndpointErrorTypes.UNAUTHORIZED]: { title: "post.errors.unauthorized.title" as const, description: "post.errors.unauthorized.description" as const },
30 [EndpointErrorTypes.SERVER_ERROR]: { title: "post.errors.serverError.title" as const, description: "post.errors.serverError.description" as const },
31 // ... other error types
32 },
33 successTypes: { title: "post.success.title" as const, description: "post.success.description" as const },
34 examples: { requests: { default: { decision: "Rewrite everything in TypeScript" } }, responses: { default: { justification: "..." } } },
35});
36
37export type ExplainResponseOutput = typeof POST.types.ResponseOutput;
38const definitions = { POST } as const;
39export 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.
1import "server-only";
2
3export class ExplainRepository {
4 static async explainToMyBoss(
5 data: ExplainRequestOutput,
6 user: JwtPayloadType,
7 logger: EndpointLogger,
8 t: ExplainT,
9 ): Promise<ResponseType<ExplainResponseOutput>> {
10 if (!user.id) {
11 return fail({ message: t("post.errors.unauthorized.title"), errorType: ErrorResponseTypes.UNAUTHORIZED });
12 }
13 try {
14 const [saved] = await db
15 .insert(explainResults)
16 .values({ userId: user.id, decision: data.decision })
17 .returning();
18 logger.info("Saved decision", { userId: user.id, id: saved.id });
19 return success({ justification: saved.justification ?? "" });
20 } catch (error) {
21 logger.error("Failed to save decision", parseError(error));
22 return fail({ message: t("post.errors.serverError.title"), errorType: ErrorResponseTypes.INTERNAL_ERROR });
23 }
24 }
25}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.
1"use client";
2
3interface CustomWidgetProps {
4 field: {
5 value: ExplainResponseOutput | null | undefined;
6 } & (typeof definition.POST)["fields"];
7}
8
9export function ExplainContainer({ field }: CustomWidgetProps): JSX.Element {
10 const children = field.children;
11 const emptyField = useMemo(() => ({}), []);
12
13 return (
14 <Div className="flex flex-col gap-4 p-4">
15 <TextareaFieldWidget fieldName="decision" field={children.decision} />
16 <SubmitButtonWidget<typeof definition.POST>
17 field={{
18 text: "Explain to my boss",
19 loadingText: "Explaining...",
20 icon: "sparkles",
21 variant: "primary",
22 }}
23 />
24 <FormAlertWidget field={emptyField} />
25 <AlertWidget
26 fieldName="justification"
27 field={withValue(children.justification, field.value?.justification, null)}
28 />
29 </Div>
30 );
31}Delete the folder. The feature disappears from every platform at once.
One definition. Thirteen platforms.
When you add a feature to next-vibe, it doesn't just become an API endpoint. It runs everywhere at once.
REST endpoint, fully validated and typed
Purpose-built interactive UI - no JSX required
Every endpoint is a command with auto-generated flags
Function-calling schema generated automatically
Connect Claude Desktop, Cursor, any MCP client
iOS and Android screens from the same definition
Schedule any endpoint on a cron expression
Push updates to connected clients on completion
Native desktop app via the same endpoint contracts
Auto-generated admin UI - no dedicated code
Embeddable iframe widget for any site
Callable by AI agents as a structured skill
Node in a live data-flow graph - same endpoint
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.
Replace with a real typed interface. If you reach for any, your architecture has a hole.
Same rule. unknown is just any with extra steps. Define the type.
Bare object is meaningless. Write the shape you actually expect.
Type assertions are lies to the compiler. Fix the architecture instead.
Use ResponseType<T> with success(data) or fail({message, errorType}). Errors are data.
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.
Fork, ask, ship.
You're at unbottled.ai's level from day one.
Fork on GitHub, then clone your fork locally.
1git clone https://github.com/YOUR_USERNAME/next-vibe
2cd next-vibe && bun installFor 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.
1# development
2vibe dev
3
4# production - first start
5vibe build && vibe start
6
7# production - after changes
8vibe rebuildOpen the app and click "Login as Admin" - the setup wizard walks you through API keys and admin password.
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.
1# In unbottled.ai chat or Claude Code:
2"Build me a feature that explains decisions to my boss"
3# AI writes all files and runs vibe check automaticallyBuilding something big?
We help teams with setup, custom integrations, architecture review, and ongoing development support. Same codebase, your deployment.