Project Structure
How a Fabrik UI project is organized
Directory layout
A typical Fabrik project follows standard Next.js conventions:
my-app/
├── app/
│ ├── api/chat/
│ │ └── route.ts # Server route — LLM runs here
│ ├── layout.tsx
│ └── page.tsx # Client page — UI renders here
├── components/
│ └── weather-card.tsx # Your generative UI components
├── .env.local # API keys (never committed)
├── package.json
└── tsconfig.jsonSDK entry points
Fabrik UI ships multiple entry points so you only import what you need:
| Import path | Purpose | Environment |
|---|---|---|
@fabrik-sdk/ui | defineComponent, defineTool, types | Both |
@fabrik-sdk/ui/react | Fabrik, Chat, Fab, useChat, Message | Client |
@fabrik-sdk/ui/server | handler, server adapter | Server + Client |
@fabrik-sdk/ui/ai-sdk | AI SDK adapter (recommended) — use with @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google, etc. | Server |
@fabrik-sdk/ui/openai | Native OpenAI provider (lightweight alternative) | Server |
@fabrik-sdk/ui/anthropic | Native Anthropic provider (lightweight alternative) | Server |
@fabrik-sdk/ui/google | Native Google Gemini provider (lightweight alternative) | Server |
@fabrik-sdk/ui/custom | Custom provider adapter | Server |
@fabrik-sdk/ui/chat | Chat UI primitives | Client |
@fabrik-sdk/ui/pages | Multi-page routing | Client |
@fabrik-sdk/ui/testing | Mock provider for tests | Both |
@fabrik-sdk/ui/styles.css | Base styles | Client |
Server / client split
Fabrik enforces a strict separation:
- Server route (
app/api/chat/route.ts) — holds the LLM provider, API keys, and system prompt. Runs server-side only. - Client page — uses the
server()adapter to POST messages to the route. Receives SSE events and renders components.
This means API keys never reach the browser. The client only sees streamed events (text deltas, component props, elicitations).
Component registration
Components are defined with defineComponent() and passed to <Fabrik>:
import { defineComponent } from "@fabrik-sdk/ui"
import { z } from "zod"
export const weatherCard = defineComponent({
name: "weather_card",
description: "Shows current weather for a city",
schema: z.object({
city: z.string(),
temp: z.number(),
condition: z.string(),
}),
component: ({ city, temp, condition }) => (
<div className="rounded-xl border p-4">
<h3 className="font-bold">{city}</h3>
<p className="text-3xl">{temp}°F</p>
<p>{condition}</p>
</div>
),
})The schema is converted to JSON Schema and sent to the LLM as a tool definition. When the LLM calls the tool, the props are validated against your schema and your component renders.