Conventions (do / don't)
The patterns that keep the design system coherent — and the ones that quietly break it.
These rules come from the migration that consolidated three drifting palettes into one. They're enforced by review, not by lint — read them before you ship a UI change.
Tokens
Do — use semantic Tailwind classes
<div className="bg-card border border-border text-foreground">
<p className="text-muted-foreground">Helper copy</p>
<button className="bg-primary text-primary-foreground">Continue</button>Don't — hardcode colors
// All of these break theme-switching and drift over time.
<div className="bg-black border border-white/10 text-white">
<p className="text-zinc-400">Helper copy</p>
<button className="bg-white text-black">Continue</button>
<div style={{ background: 'oklch(0% 0 0)' }}>Even bg-white/[0.05] is a smell unless you're modelling an alpha overlay over a known surface (e.g. card hover). The token (hover:bg-accent) is preferred when one exists.
Fonts
Do — rely on the root layout
apps/client/app/layout.tsx already loads Geist Sans and Geist Mono via next/font/google and exposes them as --font-geist-sans / --font-geist-mono. Reach for font-sans and font-mono in components.
Don't — re-load fonts per component
// Don't.
import { Geist } from 'next/font/google';
const geist = Geist({ subsets: ['latin'] });
export function MyComponent() {
return <div className={geist.className}>...</div>;
}Per-component font loading ships duplicate <link>s, defeats the preload, and produces FOIT inconsistencies between routes.
Brand primitives
Do — import from @/components/brand
import { BrandMark, GridGlowBackground } from '@/components/brand';
<header>
<BrandMark>
<VersionChip />
</BrandMark>
</header>Don't — re-roll the wordmark, the grid, the chip
// Don't — three pages will end up with three different wordmarks.
<span className="font-semibold tracking-[-0.02em] text-lg">UseDeploy</span>
// Don't — the grid and glow are intentionally one component for a reason.
<div className="absolute inset-0 [background-image:linear-gradient(...)]" />If you need a variant the primitive doesn't expose, add a prop to the primitive — don't fork it.
Auth pages
Do — wrap form content in <AuthCard>
// app/(auth)/login/page.tsx
export default function LoginPage() {
return (
<AuthCard
title="Sign in"
subtitle="Welcome back."
footer={<Link href="/register">Create an account</Link>}
>
<LoginForm />
</AuthCard>
);
}The (auth)/layout.tsx mounts the page chrome (GridGlowBackground, centred container). The page only declares its form.
Don't — re-implement the chrome per page
// Don't.
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="rounded-lg border border-white/10 bg-white/5 p-8">
<h1>Sign in</h1>
<LoginForm />
</div>
</div>
);This is the bug pattern that produced three wrapper definitions before #145.
Auth client
Do — use cookies via apiClient with credentials
const { data, error } = await api.POST('/auth/login', { body: { email, password } });
// Cookie set by the server; no token to store.Don't — reach for localStorage / bearer tokens
BetterAuth issues an HTTP-only cookie that the browser sends back automatically because apiClient has credentials: 'include' (withCredentials: true for axios callsites). There is no token to capture.
Form schemas
Do — import from @app/contracts, extend for client-only fields
import { auth } from '@app/contracts';
export const registerFormSchema = auth.SignUpInput
.extend({ confirmPassword: z.string() })
.refine((d) => d.password === d.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});Don't — re-define server-known fields
// Don't — drifts from the server, ships a 400 to users, typecheck doesn't catch it.
export const registerFormSchema = z.object({
fullName: z.string().min(1),
emailAddress: z.string().email(),
password: z.string().min(8),
});See contracts for the bug history.
When in doubt
Open a primitive's source — they're tiny (the entire brand/ directory is under 400 lines). The tokens are 250 lines of globals.css. Read the code; it's the source of truth.