White-Label Web Application
White-label allows clients to sell your product under their brand: own domain, logo, colors, email templates. Users of client don't know about your platform.
White-Label Architecture
Two isolation approaches:
By subdomain — acme.yourplatform.com or app.acmecorp.com (custom domain)
By DNS records — client adds CNAME: app.acmecorp.com → yourplatform.com
// middleware.ts: determine tenant by domain
export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host')!;
const domain = hostname.replace(':3000', '');
const tenant = await getTenantByDomain(domain);
if (!tenant) {
return NextResponse.rewrite(new URL('/404', request.url));
}
const response = NextResponse.next();
response.headers.set('x-tenant-id', tenant.id);
response.headers.set('x-tenant-slug', tenant.slug);
return response;
}
Custom Domain: Verification
export async function verifyCustomDomain(tenantId: string, domain: string) {
const dns = await import('dns/promises');
let cnameTarget: string;
try {
const records = await dns.resolveCname(domain);
cnameTarget = records[0];
} catch {
return { verified: false, error: 'CNAME not found' };
}
const expectedCname = `${process.env.PLATFORM_DOMAIN}`;
if (cnameTarget !== expectedCname) {
return {
verified: false,
error: `CNAME must point to ${expectedCname}, found: ${cnameTarget}`
};
}
await db.tenant.update({
where: { id: tenantId },
data: {
customDomain: domain,
customDomainVerifiedAt: new Date(),
}
});
await addCloudflareCustomHostname(domain, tenantId);
return { verified: true };
}
Branding: Tenant Configuration
model TenantBranding {
id String @id @default(cuid())
tenantId String @unique
logoUrl String?
faviconUrl String?
appName String
primaryColor String @default("#6366f1")
secondaryColor String @default("#f1f5f9")
fontFamily String @default("Inter")
supportEmail String?
supportUrl String?
privacyUrl String?
termsUrl String?
footerText String?
hideWatermark Boolean @default(false)
customCss String? @db.Text
emailFromName String?
emailFromAddress String?
emailLogoUrl String?
}
export function TenantStylesheet({ branding }: { branding: TenantBranding }) {
const css = `
:root {
--color-primary: ${branding.primaryColor};
--color-secondary: ${branding.secondaryColor};
--font-family: '${branding.fontFamily}', sans-serif;
}
${branding.customCss ?? ''}
`;
return <style dangerouslySetInnerHTML={{ __html: css }} />;
}
// app/layout.tsx: dynamic metadata
export async function generateMetadata(): Promise<Metadata> {
const tenantId = headers().get('x-tenant-id');
const branding = await db.tenantBranding.findUnique({
where: { tenantId: tenantId! }
});
return {
title: branding?.appName ?? 'App',
icons: { icon: branding?.faviconUrl ?? '/favicon.ico' },
};
}
Email Templates with Branding
export async function sendBrandedEmail(
tenantId: string,
to: string,
template: string,
variables: Record<string, string>
) {
const branding = await db.tenantBranding.findUnique({
where: { tenantId }
});
await resend.emails.send({
from: branding?.emailFromAddress
? `${branding.emailFromName} <${branding.emailFromAddress}>`
: `[email protected]`,
to,
subject: variables.subject,
react: EmailTemplate({
...variables,
logoUrl: branding?.emailLogoUrl,
brandColor: branding?.primaryColor ?? '#6366f1',
appName: branding?.appName ?? 'App',
footerText: branding?.footerText,
}),
});
}
Development of white-label system with custom domains, branding and email templates — 5–10 working days.







