MIGRATION GUIDE
Migrate an existing application to the FABRK framework. Transform imports, extract components, set up config, and wire store adapters.
[OVERVIEW]
Migrating to FABRK involves four main steps:
- Install packages — Add @fabrk/* packages to your project
- Create fabrk.config.ts — Define your configuration
- Transform imports — Replace @/ aliases with @fabrk/* imports
- Wire adapters — Replace custom implementations with FABRK adapters
[STEP 1: INSTALL PACKAGES]
Start by installing the core packages. Add feature packages as you migrate each area.
# Core (required)
pnpm add @fabrk/core @fabrk/config @fabrk/design-system
# Components (UI library)
pnpm add @fabrk/components
# Theming (optional — adds ThemeProvider, chart colors, formatters)
pnpm add @fabrk/design-system
# Feature packages (install as needed)
pnpm add @fabrk/auth # NextAuth, API keys, MFA
pnpm add @fabrk/payments # Stripe, Polar, Lemon Squeezy
pnpm add @fabrk/ai # LLM providers, cost tracking
pnpm add @fabrk/email # Resend, console adapter
pnpm add @fabrk/storage # S3, R2, local filesystem
pnpm add @fabrk/security # CSRF, CSP, rate limiting, audit
pnpm add @fabrk/store-prisma # Prisma store adapters[STEP 2: CREATE CONFIGURATION]
Create a fabrk.config.ts at your project root. Start with just the sections you need and add more as you migrate.
import { defineFabrkConfig } from '@fabrk/config'
export default defineFabrkConfig({
framework: {
runtime: 'vite',
typescript: true,
srcDir: 'src', // or 'app' if no src directory
database: 'prisma', // or 'drizzle' or 'none'
},
theme: {
system: 'terminal', // or 'swiss' or 'custom'
colorScheme: 'green',
radius: 'sharp', // 'sharp' | 'rounded' | 'pill'
},
// Add sections as you migrate:
// auth: { ... },
// payments: { ... },
// ai: { ... },
})[STEP 3: IMPORT TRANSFORMATIONS]
The most common migration task is transforming imports from path aliases to FABRK package imports. Here is a comprehensive mapping.
UTILITY IMPORTS
// BEFORE: path alias imports
import { cn } from '@/lib/utils'
import { cn } from '@/utils'
// AFTER: FABRK package imports
import { cn } from '@/lib/utils'DESIGN SYSTEM IMPORTS
// BEFORE: local design system
import { mode } from '@/design-system'
import { mode } from '@/lib/design-system'
import { themes } from '@/config/themes'
// AFTER: FABRK design system
import { mode } from '@fabrk/design-system'
// OR (if using the themes package with ThemeProvider):
import { mode } from '@fabrk/design-system'COMPONENT IMPORTS
// BEFORE: local component imports
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { DataTable } from '@/components/data-table'
import { BarChart } from '@/components/charts/bar-chart'
import { KPICard } from '@/components/dashboard/kpi-card'
// AFTER: single FABRK import
import {
Button, Card, Input, Badge,
DataTable, BarChart, KPICard
} from '@fabrk/components'AUTH IMPORTS
// BEFORE: custom auth utilities
import { generateApiKey } from '@/lib/api-keys'
import { verifyTOTP } from '@/lib/mfa'
import { hashPassword } from '@/lib/auth'
// AFTER: FABRK auth package
import { generateApiKey, hashApiKey, validateApiKey } from '@fabrk/auth'
import { generateTOTP, verifyTOTP, generateBackupCodes } from '@fabrk/auth'PAYMENT IMPORTS
// BEFORE: direct Stripe SDK usage
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// AFTER: FABRK payment adapter
import { StripePaymentAdapter } from '@fabrk/payments'
const payments = new StripePaymentAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
})EMAIL IMPORTS
// BEFORE: direct Resend SDK
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
// AFTER: FABRK email adapter
import { ResendEmailAdapter } from '@fabrk/email'
const email = new ResendEmailAdapter({
apiKey: process.env.RESEND_API_KEY!,
})AI IMPORTS
// BEFORE: direct OpenAI/Anthropic SDK
import OpenAI from 'openai'
const openai = new OpenAI()
// AFTER: FABRK unified client
import { getLLMClient } from '@fabrk/ai'
const client = getLLMClient({
provider: 'openai',
model: 'gpt-4',
})# Find all @/ imports
@/components/ui/ → @fabrk/components (consolidate into barrel import)
@/lib/utils → @fabrk/core
@/design-system → @fabrk/design-system
@/lib/auth → @fabrk/auth
@/lib/api-keys → @fabrk/auth[STEP 4: COMPONENT EXTRACTION PATTERN]
When migrating custom components to use FABRK, follow these patterns:
REPLACE API-FETCHING WITH CALLBACKS
FABRK components accept callback props instead of making API calls directly. This keeps them portable and testable.
// BEFORE: Component makes its own API calls
function MemberList() {
const [members, setMembers] = useState([])
useEffect(() => {
fetch('/api/team/members').then(r => r.json()).then(setMembers)
}, [])
const handleRemove = async (id: string) => {
await fetch(`/api/team/members/${id}`, { method: 'DELETE' })
setMembers(prev => prev.filter(m => m.id !== id))
}
return (
<div>
{members.map(m => (
<div key={m.id}>
{m.name}
<button onClick={() => handleRemove(m.id)}>Remove</button>
</div>
))}
</div>
)
}// AFTER: Component accepts data and callbacks as props
import { MemberCard } from '@fabrk/components'
interface MemberListProps {
members: Array<{ id: string; name: string; email: string; role: string }>
onRemove: (id: string) => void
}
function MemberList({ members, onRemove }: MemberListProps) {
return (
<div className="space-y-3">
{members.map(m => (
<MemberCard
key={m.id}
name={m.name}
email={m.email}
role={m.role}
onRemove={() => onRemove(m.id)}
/>
))}
</div>
)
}REMOVE NEXT.JS DEPENDENCIES
Components should not import from next/link or next/navigation directly. Use the linkComponent prop pattern or callbacks instead.
// BEFORE: next/link dependency baked in
import Link from 'next/link'
function NavItem({ href, label }) {
return <Link href={href}>{label}</Link>
}
// AFTER: linkComponent prop pattern
interface NavItemProps {
href: string
label: string
linkComponent?: React.ComponentType<{ href: string; children: React.ReactNode }>
}
function NavItem({ href, label, linkComponent: LinkComp = 'a' as any }: NavItemProps) {
return <LinkComp href={href}>{label}</LinkComp>
}
// Usage with Next.js:
import Link from 'next/link'
<NavItem href="/dashboard" label="DASHBOARD" linkComponent={Link} />USE RENDER PROPS FOR OPTIONAL DEPS
// BEFORE: hard dependency on qrcode library
import QRCode from 'qrcode.react'
function MfaSetup({ uri }) {
return <QRCode value={uri} />
}
// AFTER: render prop for optional dependency
interface MfaSetupProps {
uri: string
renderQrCode?: (uri: string) => React.ReactNode
}
function MfaSetup({ uri, renderQrCode }: MfaSetupProps) {
if (renderQrCode) return <>{renderQrCode(uri)}</>
return <code className="text-xs break-all">{uri}</code>
}
// Usage:
import QRCode from 'qrcode.react'
<MfaSetup uri={totpUri} renderQrCode={(uri) => <QRCode value={uri} />} />[STEP 5: APPLY DESIGN SYSTEM]
Replace hardcoded colors and styles with FABRK design tokens.
COLOR TOKENS
// BEFORE: hardcoded Tailwind colors
<div className="bg-gray-100 text-gray-900 border-gray-200">
<button className="bg-blue-500 text-white hover:bg-blue-600">
<span className="text-red-500">Error</span>
<div className="bg-green-100 text-green-800">Success</div>
// AFTER: semantic design tokens
<div className="bg-muted text-foreground border-border">
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
<span className="text-destructive">Error</span>
<div className="bg-success/10 text-success">Success</div>BORDER RADIUS
// BEFORE: hardcoded border radius
<Card className="rounded-lg border border-gray-200 p-4">
<Button className="rounded-md px-4 py-2">Click</Button>
// AFTER: mode.radius from design system
import { mode } from '@fabrk/design-system'
import { cn } from '@/lib/utils'
<Card className={cn("border border-border p-4", mode.radius)}>
<Button className={cn("px-4 py-2", mode.radius)}>{'>'} CLICK</Button>TYPOGRAPHY
// BEFORE: generic text styles
<h1 className="text-2xl font-bold">Dashboard</h1>
<span className="text-sm text-gray-500">Active</span>
<button>Submit</button>
// AFTER: terminal aesthetic with mode.font
<h1 className={cn("text-2xl font-bold uppercase", mode.font)}>DASHBOARD</h1>
<Badge>[ACTIVE]</Badge>
<Button>{'>'} SUBMIT</Button>[STEP 6: WIRE STORE ADAPTERS]
Replace custom database queries with FABRK store adapters. This gives you a consistent interface and the ability to swap stores (in-memory for testing, Prisma for production).
BEFORE: DIRECT DATABASE CALLS
// BEFORE: scattered Prisma calls
import { prisma } from '@/lib/prisma'
// Team management — custom queries everywhere
async function getTeam(id: string) {
return prisma.organization.findUnique({ where: { id }, include: { members: true } })
}
async function addMember(teamId: string, userId: string, role: string) {
return prisma.orgMember.create({ data: { organizationId: teamId, userId, role } })
}
// Audit logging — custom implementation
async function logAction(action: string, userId: string) {
return prisma.auditLog.create({ data: { action, userId, timestamp: new Date() } })
}AFTER: FABRK STORE ADAPTERS
// AFTER: FABRK store pattern
import { autoWire } from '@fabrk/core'
import { PrismaTeamStore, PrismaAuditStore } from '@fabrk/store-prisma'
import { PrismaClient } from '@prisma/client'
import config from '../fabrk.config'
const prisma = new PrismaClient()
const fabrk = autoWire(config, undefined, {
teamStore: new PrismaTeamStore(prisma),
auditStore: new PrismaAuditStore(prisma),
})
// Now use FABRK managers — consistent API, swappable stores
const { manager: teamManager } = useTeam()
await teamManager.createTeam({ name: 'Engineering' })
await teamManager.addMember(teamId, { userId, role: 'member' })
// Audit logging via FABRK
const audit = new AuditLogger(new PrismaAuditStore(prisma))
await audit.log({ action: 'user.login', userId })// test setup
import { InMemoryTeamStore, InMemoryAuditStore } from '@fabrk/core'
const fabrk = autoWire(config, undefined, {
teamStore: new InMemoryTeamStore(),
auditStore: new InMemoryAuditStore(),
})
// Tests run instantly — no database required[STEP 7: ADD USE CLIENT DIRECTIVES]
Components that use cn() from @fabrk/coreor any interactive features need the 'use client' directive. Server components (static pages, layouts without interactivity) do not need it.
// NEEDS 'use client' — uses cn(), useState, onClick, etc.
'use client'
import { cn } from '@/lib/utils'
import { mode } from '@fabrk/design-system'
import { Button, Card } from '@fabrk/components'
function InteractiveComponent() {
const [open, setOpen] = useState(false)
return (
<Card className={cn("p-4", mode.radius)}>
<Button onClick={() => setOpen(true)}>{'>'} OPEN</Button>
</Card>
)
}
// DOES NOT NEED 'use client' — static content, no cn() or interactivity
import { DocLayout, Section } from '@/components/doc-layout'
export default function StaticPage() {
return (
<DocLayout title="ABOUT">
<Section title="INFO">
<p className="text-sm text-muted-foreground">
Static content here.
</p>
</Section>
</DocLayout>
)
}[MIGRATE FROM VERCEL AI SDK]
If you are using the Vercel AI SDK (ai package), fabrk replaces the ad-hoc generateText / streamText call sites with file-system agent definitions. Each agent lives in agents/<name>/agent.ts and is discovered automatically at startup — no registration boilerplate required.
BASIC GENERATION
// BEFORE: Vercel AI SDK
import { generateText, streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Hello',
})
// Streaming with tools
const stream = await streamText({
model: openai('gpt-4'),
messages: [...],
tools: { ... },
})
// AFTER: fabrk agent definition
// agents/assistant/agent.ts
import { defineAgent } from '@fabrk/framework'
export default defineAgent({
model: 'gpt-4',
tools: ['my-tool'],
systemPrompt: 'You are a helpful assistant.',
stream: true,
})CALLING AGENTS FROM THE CLIENT
// BEFORE: call generateText / streamText in your own API route,
// then wire up a custom fetch loop on the client.
// AFTER: use the built-in useAgent hook
import { useAgent } from '@fabrk/framework/client'
function Chat() {
const { messages, send, isStreaming } = useAgent('assistant')
return (
<div>
{messages.map((m, i) => (
<p key={i}>{m.content}</p>
))}
<button onClick={() => send('Hello')}>{'>'} SEND</button>
</div>
)
}agents/. The route is derived from the directory name: agents/assistant/agent.ts is served at/__ai/agents/assistant. No manual registration needed.[MIGRATE AI TOOLS]
Vercel AI SDK tool definitions map directly to fabrk's defineTool. The key difference is the schema format (JSON Schema object instead of a Zod schema) and the return shape (MCP-compatible content array).
// BEFORE: Vercel AI SDK tool
import { tool } from 'ai'
import { z } from 'zod'
const weatherTool = tool({
description: 'Get weather',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temp: 72 }),
})
// AFTER: fabrk defineTool
// tools/weather/tool.ts
import { defineTool } from '@fabrk/framework'
export default defineTool({
name: 'weather',
description: 'Get weather',
schema: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
handler: async ({ city }) => ({
content: [{ type: 'text', text: JSON.stringify({ temp: 72 }) }],
}),
})REFERENCING TOOLS IN AGENTS
// tools/weather/tool.ts — defines the tool (above)
// agents/assistant/agent.ts — references the tool by name
import { defineAgent } from '@fabrk/framework'
export default defineAgent({
model: 'gpt-4',
tools: ['weather'], // matched by tool name, auto-discovered from tools/
systemPrompt: 'You can look up weather.',
stream: true,
})defineTool are automatically exposed via the built-in MCP server at /__ai/mcp — no additional adapter code required.[MIGRATE COST TRACKING]
The Vercel AI SDK has no built-in cost tracking. You either omit it entirely or wire up a custom solution. fabrk tracks every LLM call automatically and enforces daily budgets and per-agent spending limits.
BEFORE: NO BUILT-IN TRACKING
// BEFORE: nothing — or a hand-rolled counter
import { generateText } from 'ai'
const result = await generateText({ model: openai('gpt-4'), prompt })
// Token usage is buried in result.usage — you must manually record,
// store, and alert on cost. No enforcement, no per-agent isolation.
console.log(result.usage.totalTokens)AFTER: AUTOMATIC COST TRACKING
// AFTER: configure budgets in fabrk.config.ts
import { defineFabrkConfig } from '@fabrk/config'
export default defineFabrkConfig({
ai: {
defaultModel: 'gpt-4',
costTracking: {
enabled: true,
dailyBudget: 10.00, // hard cap across all agents ($)
alertThreshold: 0.8, // warn at 80% of budget
perAgentLimit: 2.00, // per-agent daily cap ($)
},
},
})// Query live cost data at runtime
import { getCostTracker } from '@fabrk/ai'
const tracker = getCostTracker()
// Total spend today across all agents
const daily = await tracker.getDailyTotal()
// Spend broken down by agent name
const byAgent = await tracker.getDailyByAgent()
// Check remaining budget before running an expensive prompt
const remaining = await tracker.getRemainingBudget()
if (remaining < 0.10) {
throw new Error('Daily budget nearly exhausted')
}dailyBudget, fabrk rejects it before the LLM request is made and returns a 402 response to the client. The per-agent cap is enforced independently so one runaway agent cannot consume the entire shared budget.[MIGRATE FROM NEXT.JS ROUTING]
fabrk uses the same file-system routing conventions as the Next.js App Router. In most cases, your existing app/ directory moves over unchanged. The primary differences are the build toolchain (Vite instead of webpack) and a small set of Next.js-specific imports that need replacing.
ROUTING CONVENTIONS (IDENTICAL)
// Next.js App Router → fabrk (identical)
app/page.tsx → app/page.tsx
app/layout.tsx → app/layout.tsx
app/loading.tsx → app/loading.tsx
app/error.tsx → app/error.tsx
app/not-found.tsx → app/not-found.tsx
app/[id]/page.tsx → app/[id]/page.tsx
app/api/route.ts → app/api/route.ts
app/(group)/page.tsx → app/(group)/page.tsx
// No changes needed for any of these files.IMPORTS THAT NEED REPLACING
// BEFORE: Next.js imports
import Link from 'next/link'
import { useRouter, usePathname } from 'next/navigation'
import { headers, cookies } from 'next/headers'
import Image from 'next/image'
// AFTER: fabrk equivalents
import { Link } from '@fabrk/framework/client' // or standard <a>
import { useRouter, usePathname } from '@fabrk/framework/client'
import { headers, cookies } from '@fabrk/framework/server'
import { Image } from '@fabrk/framework/client' // or standard <img>SERVER ACTIONS
// Server actions work the same way — same directive, same signature
'use server'
export async function createItem(formData: FormData) {
const name = formData.get('name') as string
// ... database call
return { success: true }
}
// Client usage is identical too
'use client'
import { createItem } from './actions'
<form action={createItem}>
<input name="name" />
<button type="submit">{'>'} CREATE</button>
</form>METADATA API
// Metadata export works identically
import type { Metadata } from '@fabrk/framework'
export const metadata: Metadata = {
title: 'My App',
description: 'Built with fabrk',
}
export default function Page() {
return <main>...</main>
}- Build toolchain: Vite 7 instead of webpack/Turbopack — faster cold starts and HMR
- No
next.config.ts— usefabrk.config.tsinstead - No
next/font— import fonts directly in CSS or via Vite plugins - No Vercel-specific deployment primitives — fabrk targets any Node.js or edge host
[MIGRATION CHECKLIST]
- Install core packages: @fabrk/core, @fabrk/config, @fabrk/components, @fabrk/design-system
- Create fabrk.config.ts at project root
- Replace
cn()imports:@/lib/utilsto@fabrk/core - Replace design system imports:
@/design-systemto@fabrk/design-system - Replace UI component imports:
@/components/ui/*to@fabrk/components - Replace hardcoded colors with design tokens (bg-primary, text-foreground, etc.)
- Replace hardcoded border-radius with
mode.radius - Add
'use client'to components usingcn()or interactivity - Convert API-fetching components to callback props
- Replace direct SDK usage with FABRK adapters (payments, email, storage)
- Wire store adapters via
autoWire()withStoreOverrides - Run the design system validation to catch remaining violations
- Run
pnpm buildto verify everything compiles
[COMMON ISSUES]
SERVER COMPONENT ERRORS
// Error: cn() called in server component
// Solution: Add 'use client' directive at the top of the file
'use client' // ← Add this
import { cn } from '@/lib/utils'
// ... component codeTYPE NAMING CONFLICTS
// Error: Duplicate identifier 'Notification'
// When barrel-exporting, avoid conflicts with DOM types
// BEFORE:
export type { Notification } from './notification'
// AFTER: Use a specific name
export type { NotificationCenterItem } from './notification'ZOD DEFAULT VALUES
// When using fabrk.config.ts types in function parameters:
// Use FabrkConfigInput (keeps defaulted fields optional)
function setup(config: FabrkConfigInput) { ... }
// NOT FabrkConfig (all fields required — breaks callers)
function setup(config: FabrkConfig) { ... } // ← Don't use for paramsMISSING CSS VARIABLES
/* If design tokens don't resolve, add CSS variables to globals.css */
/* The FABRK templates include these automatically */
@layer base {
:root {
--background: 0 0% 7%;
--foreground: 0 0% 93%;
--card: 0 0% 10%;
--primary: 142 76% 45%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 15%;
--muted: 0 0% 15%;
--muted-foreground: 0 0% 60%;
--border: 0 0% 20%;
--destructive: 0 84% 60%;
--success: 142 76% 45%;
--radius: 0rem;
/* ... more tokens */
}
}