4. White-Label Your SaaS with Customer Branding
Multi-tenant brand theming using Brandparser with Next.js middleware
Automatically theme your SaaS product with each customer's brand colors and fonts. Parse once on signup, cache the tokens, and serve a branded experience per tenant.
What you'll build
A multi-tenant theming system where each customer sees your product styled with their brand. The pattern:
- Parse brand on customer signup
- Store a slim set of brand tokens in your database
- Serve brand-specific CSS variables via Next.js middleware
- Every page renders with the customer's brand -zero client-side fetching
Step 1: Parse on signup and store tokens
When a customer signs up and provides their website URL, parse their brand and store a minimal token set:
// lib/brand-tokens.ts
const API_KEY = process.env.BRANDPARSER_API_KEY!;
const BASE_URL = "https://api.brandparser.com";
export type BrandTokens = {
primaryColor: string;
primaryHoverColor: string;
buttonTextColor: string;
backgroundColor: string;
fontFamily: string;
fontCssUrl: string | null;
logoUrl: string | null;
};
export async function parseAndStoreBrandTokens(
tenantId: string,
websiteUrl: string
): Promise<BrandTokens> {
// 1. Submit for parsing
const parseRes = await fetch(`${BASE_URL}/v1/api/parse`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url: websiteUrl }),
});
const { data: parseData } = await parseRes.json();
// 2. Poll for completion
let brand;
while (true) {
const res = await fetch(
`${BASE_URL}/v1/api/brands/${parseData.brand_id}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { data } = await res.json();
if (data.analysis_status === "complete") {
brand = data;
break;
}
if (data.analysis_status === "failed") {
throw new Error("Brand analysis failed");
}
await new Promise((r) => setTimeout(r, 5000));
}
// 3. Extract tokens
const colors = brand.analysis.colors;
const fonts = brand.analysis.typography.font_families;
const logos = brand.analysis.logos.slots;
const buttons = colors.button_colors?.[0];
const mainFont = fonts[0];
const logoSlot = Object.values(logos).find(
(slot): slot is { source_url: string } =>
slot !== null && "source_url" in slot
);
const tokens: BrandTokens = {
primaryColor: colors.primary_palette[0].hex,
primaryHoverColor: buttons?.hover_background_hex ?? colors.primary_palette[0].hex,
buttonTextColor: buttons?.text_hex ?? "#FFFFFF",
backgroundColor: colors.supporting_colors?.[0]?.hex ?? "#FFFFFF",
fontFamily: mainFont?.name ?? "Inter",
fontCssUrl: mainFont?.availability?.google_fonts_css_url ?? null,
logoUrl: logoSlot?.source_url ?? null,
};
// 4. Store in your database
await db.tenants.update({
where: { id: tenantId },
data: {
brandTokens: JSON.stringify(tokens),
brandparserId: brand.id,
},
});
return tokens;
}Step 2: Serve brand CSS via middleware
Use Next.js middleware to inject brand tokens as CSS variables into every response. This avoids client-side fetching and prevents a flash of unstyled content.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
// In-memory cache -use Redis in production
const tokenCache = new Map<string, { tokens: string; expires: number }>();
async function getTenantTokens(tenantId: string): Promise<string | null> {
const cached = tokenCache.get(tenantId);
if (cached && cached.expires > Date.now()) {
return cached.tokens;
}
// Fetch from your database
const tenant = await db.tenants.findUnique({
where: { id: tenantId },
select: { brandTokens: true },
});
if (!tenant?.brandTokens) return null;
// Cache for 1 hour
tokenCache.set(tenantId, {
tokens: tenant.brandTokens,
expires: Date.now() + 3600_000,
});
return tenant.brandTokens;
}
export async function middleware(request: NextRequest) {
// Resolve tenant from subdomain: acme.yourapp.com → "acme"
const hostname = request.headers.get("host") ?? "";
const tenantId = hostname.split(".")[0];
const tokensJson = await getTenantTokens(tenantId);
if (!tokensJson) return NextResponse.next();
const response = NextResponse.next();
// Pass tokens to the layout via a header
response.headers.set("x-brand-tokens", tokensJson);
return response;
}
export const config = {
matcher: ["/((?!api|_next|favicon.ico).*)"],
};Step 3: Apply tokens in your layout
Read the brand tokens from the middleware header and apply them as CSS variables on the root element:
// app/layout.tsx
import { headers } from "next/headers";
import type { BrandTokens } from "@/lib/brand-tokens";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const tokensJson = headersList.get("x-brand-tokens");
let brandStyle: Record<string, string> = {};
let fontUrl: string | null = null;
if (tokensJson) {
const tokens: BrandTokens = JSON.parse(tokensJson);
fontUrl = tokens.fontCssUrl;
brandStyle = {
"--brand-primary": tokens.primaryColor,
"--brand-primary-hover": tokens.primaryHoverColor,
"--brand-button-text": tokens.buttonTextColor,
"--brand-background": tokens.backgroundColor,
"--brand-font-family": `"${tokens.fontFamily}", system-ui, sans-serif`,
};
}
return (
<html lang="en">
<head>
{fontUrl && <link rel="stylesheet" href={fontUrl} />}
</head>
<body style={brandStyle}>
{children}
</body>
</html>
);
}Step 4: Use brand variables in your components
Every component in the app can now reference the brand variables:
/* globals.css */
body {
font-family: var(--brand-font-family, system-ui, sans-serif);
background-color: var(--brand-background, #ffffff);
}
.btn-primary {
background-color: var(--brand-primary, #6366f1);
color: var(--brand-button-text, #ffffff);
}
.btn-primary:hover {
background-color: var(--brand-primary-hover, #4f46e5);
}The fallback values (e.g., #6366f1) are your default theme -used when no brand tokens are present.
Architecture overview
Customer visits acme.yourapp.com
→ Middleware resolves "acme" tenant
→ Fetches brand tokens from DB (cached)
→ Passes tokens via response header
→ Layout reads header, injects CSS variables
→ Every component renders with brand colors/fontsTips
- Cache aggressively -brand data rarely changes. Cache tokens in memory or Redis with a 1-hour TTL. Don't call the Brandparser API on every request.
- Fallback gracefully -always provide CSS variable fallbacks so the app works without brand tokens (e.g., for new tenants or internal pages).
- Re-parse periodically -if a customer rebrands, re-parse their URL to pick up the changes.
- Store the
brandparserId-keep the Brandparser brand ID alongside your tenant record. This lets you fetch the full brand data later (logos, tone, etc.) without re-parsing.
Next steps
- Let users edit their brand -build a brand editing UI for your customers
- Branded emails -extend the same tokens to email templates
- Colors schema -full color palette reference