Next.js 15 fullstack (App Router + Server Actions) · Lektion

Teman och feature flags per tenant

Läs in tenantspecifik profilering och begränsade funktioner vid begäran utan nya driftsättningar.

Lektion 3 av 413 steg

Teman och feature flags per tenant är en gratis lektion i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit. Detta är lektion 3 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Next.js 15 fullstack (App Router + Server Actions), och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Vad är tematisering per tenant?

I en SaaS-applikation med flera tenants förväntar sig varje kund (tenant) ofta att produkten ska kännas som deras egen. Tematisering per tenant innebär att en annan färgpalett, logotyp, typografi eller layout läses in vid varje begäran — utan att en ny build behöver distribueras.

  • White-labeling: tenantens varumärke ersätter ert helt och hållet.
  • Åsidosättning av accentfärger: ett gemensamt gränssnitt med primärfärger och typsnitt som varierar per tenant.
  • Featureflaggor: vissa gränssnittselement eller API-rutter aktiveras endast för specifika tenants utifrån deras abonnemang eller konfiguration.

Next.js 15 App Router passar utmärkt för detta eftersom varje begäran passerar genom Server Components och middleware, vilket ger en naturlig plats att fastställa tenantkontexten innan något renderas.

Fastställa tenant vid edge

Det första steget är att identifiera vilken tenant som gör begäran. De två vanligaste strategierna är:

  • Routing via subdomän: acme.app.com → tenant-slug = acme
  • Mappning av anpassad domän: dashboard.acme.com → slå upp tenant utifrån host-headern

Next.js 15 middleware körs vid edge innan någon Server Component, vilket gör den till rätt plats för att fastställa tenanten och skicka vidare kontexten via request-headers.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const host = req.headers.get('host') ?? '';
  // Extract subdomain: "acme.app.com" -> "acme"
  const subdomain = host.split('.')[0];
  const tenantSlug = subdomain !== 'www' && subdomain !== 'app' ? subdomain : 'default';

  const res = NextResponse.next();
  // Forward tenant slug to Server Components via a custom header
  res.headers.set('x-tenant-slug', tenantSlug);
  return res;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Definiera schemat för tenantkonfiguration

Innan ni hämtar tenantdata bör ni definiera en tydlig TypeScript-typ som beskriver allt en tenant kan anpassa. Den blir ert kontrakt mellan databasen och användargränssnittet.

  • Varumärkesprofil: primärfärg, URL till logotyp och typsnittsfamilj.
  • Featureflaggor: en post med strängnycklar och booleska värden, så att ni kan lägga till nya flaggor utan schemaändringar.
  • Abonnemangsnivå: används för att styra åtkomsten till hela funktionsuppsättningar.
// lib/tenant/types.ts
export type PlanTier = 'free' | 'pro' | 'enterprise';

export interface TenantBranding {
  primaryColor: string;   // e.g. "#6366F1"
  logoUrl: string;
  fontFamily: string;     // e.g. "Inter"
  companyName: string;
}

export interface TenantConfig {
  id: string;
  slug: string;
  plan: PlanTier;
  branding: TenantBranding;
  /** Map of feature-flag keys to enabled status */
  features: Record<string, boolean>;
}

// Helper to check a flag safely
export function isFeatureEnabled(
  config: TenantConfig,
  flag: string
): boolean {
  return config.features[flag] === true;
}

Hämta tenantkonfiguration på serversidan

När sluggen har skickats vidare via headers kan valfri Server Component eller Server Action hämta hela TenantConfig från databasen eller en snabb cache (Redis, Vercel KV med mera).

Använd React:s cache() för att deduplicera uppslagningen inom en enskild begäran — funktionen anropas många gånger i nästlade layouter och sidor, men databasfrågan körs bara en gång.

// lib/tenant/get-tenant-config.ts
import { cache } from 'react';
import { headers } from 'next/headers';
import { db } from '@/lib/db';          // your Drizzle / Prisma client
import type { TenantConfig } from './types';

export const getTenantConfig = cache(async (): Promise<TenantConfig> => {
  const headerList = await headers();
  const slug = headerList.get('x-tenant-slug') ?? 'default';

  const row = await db.query.tenants.findFirst({
    where: (t, { eq }) => eq(t.slug, slug),
    columns: { id: true, slug: true, plan: true, branding: true, features: true },
  });

  if (!row) {
    throw new Error(`Tenant not found: ${slug}`);
  }

  return row as TenantConfig;
});

Infoga CSS-variabler för varumärkesprofilen

Det renaste sättet att tillämpa färger och typsnitt per tenant är att använda CSS custom properties på rotelementet. Era Tailwind- eller vanliga CSS-klasser refererar till dessa variabler — värdena ändras per tenant, men klassnamnen ändras aldrig.

Hämta tenantkonfigurationen i rotlayouten och generera en inline-<style>-tagg. Eftersom detta är en Server Component medför det ingen extra JavaScript-belastning på klienten.

// app/layout.tsx
import { getTenantConfig } from '@/lib/tenant/get-tenant-config';
import type { ReactNode } from 'react';

export default async function RootLayout({ children }: { children: ReactNode }) {
  const tenant = await getTenantConfig();
  const { primaryColor, fontFamily } = tenant.branding;

  const cssVars = [
    `--color-primary: ${primaryColor};`,
    `--font-sans: '${fontFamily}', sans-serif;`,
  ].join('\n');

  return (
    <html lang="en">
      <head>
        <style>{`:root { ${cssVars} }`}</style>
      </head>
      <body style={{ fontFamily: 'var(--font-sans)' }}>
        {children}
      </body>
    </html>
  );
}

Rendera tenantens logotyp och namn

När getTenantConfig har memoisierats via cache() kan ni anropa den fritt i valfri nästlad Server Component. Resultatet är samma objekt som återanvänds från det första anropet — inga extra turer till databasen.

En gemensam Server Component, TenantHeader, läser konfigurationen och renderar tenantens logotyp och företagsnamn utan att props behöver skickas genom flera föräldralayouter.

// components/tenant-header.tsx
import Image from 'next/image';
import { getTenantConfig } from '@/lib/tenant/get-tenant-config';

export default async function TenantHeader() {
  const { branding } = await getTenantConfig();

  return (
    <header className="flex items-center gap-3 px-6 py-4 border-b">
      <Image
        src={branding.logoUrl}
        alt={branding.companyName}
        width={120}
        height={32}
        priority
      />
      <span className="text-lg font-semibold text-[var(--color-primary)]">
        {branding.companyName}
      </span>
    </header>
  );
}

Styra åtkomst med featureflaggor i Server Components

Featureflaggor som lagras i tenantkonfigurationen gör det möjligt att villkorligt rendera hela delar av användargränssnittet på servern, så att inaktiverade funktioner inte når klientens bundle över huvud taget.

  • Anropa isFeatureEnabled(config, 'analytics_dashboard') i en Server Component.
  • Om flaggan är avstängd returnerar ni null eller en uppmaning att uppgradera — komponentkoden finns fortfarande i er bundle, men resultatet utelämnas.
  • Detta är säkrare än styrning på klientsidan, där en målmedveten användare kan granska JavaScript-koden.
// app/dashboard/page.tsx
import { getTenantConfig, isFeatureEnabled } from '@/lib/tenant';
import AnalyticsDashboard from '@/components/analytics-dashboard';
import UpgradeBanner from '@/components/upgrade-banner';

export default async function DashboardPage() {
  const config = await getTenantConfig();
  const hasAnalytics = isFeatureEnabled(config, 'analytics_dashboard');

  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
      {hasAnalytics ? (
        <AnalyticsDashboard />
      ) : (
        <UpgradeBanner
          message="Upgrade to Pro to unlock Analytics."
          plan={config.plan}
        />
      )}
    </main>
  );
}

Skydda API-rutter med featureflaggor

Det räcker inte att styra användargränssnittet — en målmedveten användare kan anropa API-rutten direkt. Tillämpa alltid featureflaggor även i Route Handler eller Server Action.

Skapa en återanvändbar guard-hjälpfunktion som hämtar tenantkonfigurationen och kastar ett typat fel om den efterfrågade funktionen är inaktiverad. Då hålls kontrollogiken samlad på ett ställe.

// lib/tenant/require-feature.ts
import { getTenantConfig, isFeatureEnabled } from './get-tenant-config';

export class FeatureDisabledError extends Error {
  constructor(flag: string) {
    super(`Feature "${flag}" is not enabled for this tenant.`);
    this.name = 'FeatureDisabledError';
  }
}

export async function requireFeature(flag: string): Promise<void> {
  const config = await getTenantConfig();
  if (!isFeatureEnabled(config, flag)) {
    throw new FeatureDisabledError(flag);
  }
}

// Usage inside a Route Handler:
// app/api/analytics/route.ts
import { requireFeature, FeatureDisabledError } from '@/lib/tenant/require-feature';
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    await requireFeature('analytics_dashboard');
  } catch (e) {
    if (e instanceof FeatureDisabledError) {
      return NextResponse.json({ error: e.message }, { status: 403 });
    }
    throw e;
  }
  // ... return analytics data
  return NextResponse.json({ data: [] });
}

Server Actions och styrning med featureflaggor

Server Actions anropas direkt från Client Components och kan också styras med featureflaggor. Eftersom Server Actions körs på servern fungerar requireFeature på samma sätt — tenantkontexten hämtas från de request-headers som Next.js vidarebefordrar automatiskt.

// app/actions/export-report.ts
'use server';

import { requireFeature, FeatureDisabledError } from '@/lib/tenant/require-feature';
import { getTenantConfig } from '@/lib/tenant/get-tenant-config';

export async function exportReport(format: 'csv' | 'pdf') {
  // Enforce feature flag before any expensive work
  await requireFeature('report_export');

  const config = await getTenantConfig();

  // PDF export only available on enterprise plan
  if (format === 'pdf' && config.plan !== 'enterprise') {
    throw new Error('PDF export requires an Enterprise plan.');
  }

  // ... generate and return the report
  return { url: `https://cdn.example.com/reports/${config.id}/report.${format}` };
}

Cacha tenantkonfiguration effektivt

Det skulle vara för långsamt att göra databassökningar vid varje begäran. Använd en cachningsstrategi i två lager:

  • Lager 1 — React cache(): deduplicerar inom en enskild begäran (redan tillämpat via getTenantConfig).
  • Lager 2 — Extern cache (Redis / Vercel KV): lagrar den hämtade konfigurationen i N minuter, så att efterföljande begäranden helt kan hoppa över databasen.

När en tenant uppdaterar sin varumärkesprofil i administrationspanelen ogiltigförklarar ni dess cache-nyckel via en Server Action eller webhook-hanterare.

// lib/tenant/tenant-cache.ts
import { kv } from '@vercel/kv';           // or ioredis
import { db } from '@/lib/db';
import type { TenantConfig } from './types';

const TTL_SECONDS = 300; // 5 minutes

export async function getOrFetchTenantConfig(slug: string): Promise<TenantConfig> {
  const cacheKey = `tenant:${slug}`;

  const cached = await kv.get<TenantConfig>(cacheKey);
  if (cached) return cached;

  const row = await db.query.tenants.findFirst({
    where: (t, { eq }) => eq(t.slug, slug),
  });
  if (!row) throw new Error(`Tenant not found: ${slug}`);

  await kv.set(cacheKey, row, { ex: TTL_SECONDS });
  return row as TenantConfig;
}

/** Call after admin updates tenant config */
export async function invalidateTenantCache(slug: string): Promise<void> {
  await kv.del(`tenant:${slug}`);
}

Exponera minimal konfiguration för klienten

Ibland behöver en Client Component känna till tenantens primärfärg eller om en funktion är aktiverad — till exempel för att utforma ett interaktivt diagram eller villkorligt rendera en knapp.

Serialisera aldrig hela TenantConfig (som kan innehålla känsliga uppgifter om abonnemang eller priser) till klienten. Skapa i stället en nedbantad, klientsäker vy och skicka den som en prop från en Server Component, eller exponera den via en lättviktig kontext.

// lib/tenant/client-config.ts
export interface TenantClientConfig {
  primaryColor: string;
  logoUrl: string;
  companyName: string;
  enabledFeatures: string[];   // only the keys that ARE enabled
}

// app/providers.tsx  (Client Component wrapping the app shell)
'use client';
import { createContext, useContext } from 'react';
import type { TenantClientConfig } from '@/lib/tenant/client-config';

const TenantContext = createContext<TenantClientConfig | null>(null);

export function TenantProvider({
  config,
  children,
}: {
  config: TenantClientConfig;
  children: React.ReactNode;
}) {
  return <TenantContext.Provider value={config}>{children}</TenantContext.Provider>;
}

export function useTenant(): TenantClientConfig {
  const ctx = useContext(TenantContext);
  if (!ctx) throw new Error('useTenant must be used inside TenantProvider');
  return ctx;
}

Kunskapskontroll: Var ska featureflaggor tillämpas?

En kollega hävdar att det räcker att kontrollera featureflaggor i Client Components, eftersom användargränssnittet helt enkelt döljer inaktiverade funktioner. Vilken är den främsta bristen i detta tillvägagångssätt?

Lektionssammanfattning: Tematisering per tenant och featureflaggor

Ni har nu ett komplett system för multi-tenancy vid begäran i Next.js 15. Det här har ni byggt:

  • Middleware hämtar tenant-sluggen från subdomänen eller host-headern och skickar den vidare via en anpassad request-header.
  • getTenantConfig använder React cache() för att hämta hela TenantConfig från databasen exakt en gång per begäran, med ett externt Redis/KV-lager för cachning mellan begäranden.
  • CSS-variabler som infogas i rotlayouten tillämpar färger och typsnitt per tenant utan någon klient-JavaScript.
  • Featureflaggor kontrolleras på servern — i Server Components, Route Handlers och Server Actions — så att inaktiverade funktioner är både osynliga och oåtkomliga.
  • Endast en minimal, klientsäker konfiguration serialiseras till klienten, vilket skyddar känsliga uppgifter om abonnemang och priser.

Den här arkitekturen skalar på ett smidigt sätt: att lägga till en ny featureflagga kräver bara en ändring av en databaskolumn och ett enda anrop till isFeatureEnabled — inga omdistributioner och inga separata byggen per tenant.

Gratis att börja

Lär dig TypeScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
88

Vanliga frågor

Är lektionen ”Teman och feature flags per tenant” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Next.js 15 fullstack (App Router + Server Actions), inklusive ”Teman och feature flags per tenant”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Vad lär jag mig i ”Teman och feature flags per tenant”?

Läs in tenantspecifik profilering och begränsade funktioner vid begäran utan nya driftsättningar. Ni övar på Next.js 15 fullstack (App Router + Server Actions) med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Next.js 15 fullstack (App Router + Server Actions)?

Du behöver inga förkunskaper. Utbildningen i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.

Hur lång tid tar lektionen ”Teman och feature flags per tenant”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Next.js 15 fullstack (App Router + Server Actions)-lektionen?

Ja. Varje Next.js 15 fullstack (App Router + Server Actions)-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Tenantidentifiering baserad på subdomän och sökväg
  2. Mönster för isolering av tenantdata på radnivå
  3. Teman och feature flags per tenant
  4. Användningsmätning och prenumerationsbegränsningar
← Tillbaka till Next.js 15 fullstack (App Router + Server Actions)