MIGRATION GUIDE

Migrate an existing Next.js application to the FABRK framework. Transform imports, extract components, set up config, and wire store adapters.

[WHO IS THIS FOR]
This guide is for developers who have an existing Next.js application and want to adopt FABRK's component library, design system, and adapter pattern. It covers the import transformations, component extraction process, and configuration setup.

[OVERVIEW]

Migrating to FABRK involves four main steps:

  1. Install packages — Add @fabrk/* packages to your project
  2. Create fabrk.config.ts — Define your configuration
  3. Transform imports — Replace @/ aliases with @fabrk/* imports
  4. 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.

install core packages
# 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.

fabrk.config.ts
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 and after
// 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 and after
// 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 and after
// 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 and after
// 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 and after
// 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 and after
// 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 and after
// 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 AND REPLACE]
Use your editor's find-and-replace to batch transform imports:
regex patterns
# 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 fetches data
// 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 callbacks
// 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 and after
// 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

render props pattern
// 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

replace hardcoded colors
// 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

replace hardcoded 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

apply terminal aesthetic
// 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 })
[TESTING BENEFIT]
With store adapters, switch to in-memory stores for unit tests:
// 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.

when to add use client
// 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]

[STEP BY STEP]
  1. Install core packages: @fabrk/core, @fabrk/config, @fabrk/components, @fabrk/design-system
  2. Create fabrk.config.ts at project root
  3. Replace cn() imports: @/lib/utils to @fabrk/core
  4. Replace design system imports: @/design-system to @fabrk/design-system
  5. Replace UI component imports: @/components/ui/* to @fabrk/components
  6. Replace hardcoded colors with design tokens (bg-primary, text-foreground, etc.)
  7. Replace hardcoded border-radius with mode.radius
  8. Add 'use client' to components using cn() or interactivity
  9. Convert API-fetching components to callback props
  10. Replace direct SDK usage with FABRK adapters (payments, email, storage)
  11. Wire store adapters via autoWire() with StoreOverrides
  12. Run fabrk lint to catch remaining design system violations
  13. Run pnpm build to verify everything compiles

[COMMON ISSUES]

SERVER COMPONENT ERRORS

fix
// 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 code

TYPE NAMING CONFLICTS

fix
// 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

fix
// 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 params

MISSING CSS VARIABLES

fix
/* 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 */
  }
}