DESIGN PHILOSOPHY
The principles behind FABRK's architecture. Understand why things work the way they do.
[INTERNATIONALIZATION (I18N)]
FABRK components accept all user-facing text as props. There are no hardcoded English strings inside components. You bring your own i18n library and pass translated strings through props.
WHY PROPS-BASED TEXT
Baking a specific i18n library (next-intl, react-i18next, lingui) into a component library couples consumers to that solution. Instead, FABRK stays agnostic: every label, placeholder, and message is a prop.
import { KPICard, Button, DataTable } from '@fabrk/components'
import { useTranslation } from 'your-i18n-library'
function Dashboard() {
const { t } = useTranslation()
return (
<div>
<KPICard title={t('dashboard.revenue')} value="$48,290" />
<KPICard title={t('dashboard.users')} value="3,847" />
<Button>{t('actions.submit')}</Button>
<DataTable
columns={[
{ key: 'name', label: t('table.name') },
{ key: 'status', label: t('table.status') },
]}
data={rows}
/>
</div>
)
}[DATA FETCHING]
FABRK components use a callback/props pattern. They never fetch data internally. This decouples the view layer from the data layer and works with any fetching strategy: Server Components, SWR, React Query, tRPC, or plain fetch.
CALLBACK PROPS VS HARDCODED FETCHING
Components accept callbacks for actions and receive data through props. The component never knows where the data comes from or how mutations happen.
// The component has zero knowledge of your data layer
<DataTable
data={users} // You fetch however you want
columns={columns}
onSort={(key, dir) => refetch({ sort: key, order: dir })}
onRowClick={(row) => router.push(`/users/${row.id}`)}
/>
<NotificationCenter
notifications={notifications} // From SWR, React Query, RSC, etc.
onMarkRead={(id) => markRead(id)} // Your mutation function
onMarkAllRead={() => markAllRead()}
/>// This couples the component to a specific API and fetching strategy
function DataTable() {
const { data } = useSWR('/api/users') // Locked to SWR + this endpoint
// ...
}[NO CSS-IN-JS RUNTIME]
FABRK uses Tailwind CSS with CSS custom properties for theming. There is zero runtime CSS overhead. Theme switching happens via CSS variable reassignment, not JavaScript re-renders.
HOW THEMING WORKS
Design tokens like bg-primary and text-foreground map to CSS custom properties. Switching themes reassigns those variables on the root element. No component re-renders, no style recalculation in JavaScript.
/* Theme variables are set on :root */
:root {
--primary: 142 71% 45%;
--background: 0 0% 3%;
--foreground: 0 0% 98%;
--border: 0 0% 15%;
--radius: 0px;
}
/* Switching themes just swaps these values */
:root[data-theme="ocean"] {
--primary: 199 89% 48%;
--background: 222 47% 6%;
--radius: 8px;
}import { cn } from '@fabrk/core'
import { mode } from '@fabrk/design-system'
// Design tokens resolve to CSS variables at zero runtime cost
<Card className={cn("bg-card border border-border", mode.radius)}>
<h2 className="text-foreground">Title</h2>
<p className="text-muted-foreground">Description</p>
</Card>[ADAPTER PATTERN]
All external services (payments, email, storage, auth) sit behind provider-agnostic interfaces. You switch providers by changing your config, not your application code.
ONE INTERFACE, MANY PROVIDERS
// @fabrk/core defines the interface
interface PaymentAdapter {
createCheckout(options: CheckoutOptions): Promise<CheckoutSession>
handleWebhook(body: string, signature: string): Promise<WebhookEvent>
getSubscription(id: string): Promise<Subscription>
}
// @fabrk/payments provides implementations
import { StripePaymentAdapter } from '@fabrk/payments' // Stripe
import { PolarPaymentAdapter } from '@fabrk/payments' // Polar
import { LemonSqueezyAdapter } from '@fabrk/payments' // Lemon Squeezy// fabrk.config.ts — change one line
export default defineFabrkConfig({
payments: {
adapter: 'stripe', // Switch to 'polar' or 'lemonsqueezy'
config: {
secretKey: process.env.STRIPE_SECRET_KEY,
},
},
email: {
adapter: 'resend', // Switch to 'console' for dev
},
storage: {
adapter: 's3', // Switch to 'r2' or 'local'
},
})[STORE PATTERN]
Stores are injectable data access layers with in-memory defaults. Every store interface has a zero-config in-memory implementation for development and testing. In production, swap in Prisma-backed stores.
IN-MEMORY BY DEFAULT, PRISMA FOR PRODUCTION
// Development: zero setup, in-memory stores
import { InMemoryCostStore, AICostTracker } from '@fabrk/ai'
const tracker = new AICostTracker(new InMemoryCostStore())
// Production: swap in Prisma stores
import { PrismaCostStore } from '@fabrk/ai'
import { prisma } from '@/lib/prisma'
const tracker = new AICostTracker(new PrismaCostStore(prisma))import { autoWire } from '@fabrk/core'
import { PrismaTeamStore, PrismaAuditStore } from '@fabrk/store-prisma'
import { prisma } from '@/lib/prisma'
// autoWire reads fabrk.config.ts and creates everything
// Pass store overrides for production persistence
const app = await autoWire(config, undefined, {
teamStore: new PrismaTeamStore(prisma),
auditStore: new PrismaAuditStore(prisma),
})