BrandparserInvite Only

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:

  1. Parse brand on customer signup
  2. Store a slim set of brand tokens in your database
  3. Serve brand-specific CSS variables via Next.js middleware
  4. 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/fonts

Tips

  • 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

On this page