MIGRATION GUIDE
Migrate an existing Next.js 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: 'nextjs',
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 '@fabrk/core'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 '@fabrk/core'
<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]
In Next.js App Router, 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 '@fabrk/core'
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>
)
}[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
fabrk lintto catch remaining design system 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 '@fabrk/core'
// ... 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 */
}
}