0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Ders

Kiracı Başına Tema ve Özellik Bayrakları

Yeniden dağıtım yapmadan istek sırasında kiracıya özel markalamayı ve erişim kontrollü özellikleri yükleyin.

Kiracı Başına Tema ve Özellik Bayrakları, CoddyKit'te ücretsiz bir Next.js 15 Fullstack (App Router + Server Actions) dersidir. Bu, 4 dersinin 3. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Next.js 15 Fullstack (App Router + Server Actions) öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.

What Is Per-Tenant Theming?

In a multi-tenant SaaS application, every customer (tenant) often expects the product to feel like their own. Per-tenant theming means loading a different color palette, logo, typography, or layout at request time — without shipping a new build.

  • White-labeling: the tenant's brand replaces yours entirely.
  • Accent overrides: a shared UI with per-tenant primary colors and fonts.
  • Feature flags: certain UI elements or API routes are enabled only for specific tenants based on their plan or configuration.

Next.js 15 App Router is ideal for this because every request passes through Server Components and middleware, giving you a natural hook to resolve tenant context before anything renders.

Resolving the Tenant at the Edge

The first step is identifying which tenant is making the request. The two most common strategies are:

  • Subdomain routing: acme.app.com → tenant slug = acme
  • Custom domain mapping: dashboard.acme.com → look up tenant by host header

Next.js 15 middleware runs at the edge before any Server Component, making it the right place to resolve the tenant and forward that context downstream 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).*)'],
};

Defining the Tenant Config Schema

Before fetching tenant data, define a clear TypeScript type that describes everything a tenant can customize. This becomes your contract between the database and the UI.

  • Branding: primary color, logo URL, font family.
  • Feature flags: a record of string keys to booleans, so you can add new flags without schema changes.
  • Plan tier: used to gate entire feature sets.
// 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;
}

Fetching Tenant Config Server-Side

With the slug forwarded via headers, any Server Component or Server Action can resolve the full TenantConfig from your database or a fast cache (Redis, Vercel KV, etc.).

Use React's cache() to deduplicate the lookup within a single request — the function is called many times across nested layouts and pages, but the DB query runs only once.

// 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;
});

Injecting CSS Variables for Branding

The cleanest way to apply per-tenant colors and fonts is via CSS custom properties set on the root element. Your Tailwind or plain CSS classes reference these variables — the values change per tenant, but the class names never change.

In the root layout, fetch tenant config and emit an inline <style> tag. Because this is a Server Component, there is zero client JavaScript overhead.

// 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>
  );
}

Rendering the Tenant Logo and Name

With getTenantConfig memoized via cache(), you can call it freely inside any nested Server Component. The result is the same object reused from the first call — no extra DB round-trips.

A shared TenantHeader Server Component reads the config and renders the tenant's logo and company name without any props drilling from parent layouts.

// 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>
  );
}

Feature Flag Gating in Server Components

Feature flags stored in the tenant config let you conditionally render entire sections of UI on the server, so disabled features never reach the client bundle at all.

  • Call isFeatureEnabled(config, 'analytics_dashboard') inside a Server Component.
  • If the flag is off, return null or an upgrade prompt — the component code is still in your bundle but the output is omitted.
  • This is safer than client-side gating, where a determined user could inspect the JS.
// 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>
  );
}

Protecting API Routes with Feature Flags

UI gating is not enough — a determined user can call your API route directly. Always enforce feature flags inside the Route Handler or Server Action as well.

Create a reusable guard helper that resolves tenant config and throws a typed error if the requested feature is disabled. This keeps the enforcement logic in one place.

// 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 and Feature Gating

Server Actions are invoked directly from Client Components and can also be gated. Because Server Actions run on the server, requireFeature works identically — the tenant context is resolved from the request headers that Next.js forwards automatically.

// 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}` };
}

Caching Tenant Config Efficiently

Database lookups on every request would be too slow. Use a two-layer caching strategy:

  • Layer 1 — React cache(): deduplicates within a single request (already applied via getTenantConfig).
  • Layer 2 — External cache (Redis / Vercel KV): stores the resolved config for N minutes so subsequent requests skip the DB entirely.

When a tenant updates their branding in the admin panel, invalidate their cache key via a Server Action or webhook handler.

// 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}`);
}

Exposing Minimal Config to the Client

Sometimes a Client Component needs to know the tenant's primary color or whether a feature is enabled — for example, to style an interactive chart or conditionally render a button.

Never serialize the full TenantConfig (which may contain sensitive plan or pricing data) to the client. Instead, create a trimmed client-safe view and pass it as a prop from a Server Component, or expose it via a lightweight context.

// 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;
}

Knowledge Check: Where Should Feature Flags Be Enforced?

A colleague argues that checking feature flags only in Client Components is sufficient, since the UI will simply hide disabled features. What is the primary flaw in this approach?

Lesson Recap: Per-Tenant Theming and Feature Flags

You now have a complete request-time multi-tenancy system in Next.js 15. Here is what you built:

  • Middleware extracts the tenant slug from the subdomain or host header and forwards it via a custom request header.
  • getTenantConfig uses React cache() to resolve the full TenantConfig from the database exactly once per request, with an external Redis/KV layer for cross-request caching.
  • CSS variables injected in the root layout apply per-tenant colors and fonts with zero client JS.
  • Feature flags are checked on the server — in Server Components, Route Handlers, and Server Actions — so disabled features are invisible and unreachable.
  • Only a minimal client-safe config is serialized to the client, protecting sensitive plan and pricing data.

This architecture scales cleanly: adding a new feature flag requires only a database column change and a single isFeatureEnabled call — no redeploys, no per-tenant builds.

Sıkça Sorulan Sorular

“Kiracı Başına Tema ve Özellik Bayrakları” dersi ücretsiz mi?

Evet — “Kiracı Başına Tema ve Özellik Bayrakları” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Next.js 15 Fullstack (App Router + Server Actions) kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

“Kiracı Başına Tema ve Özellik Bayrakları” dersinde ne öğreneceğim?

Yeniden dağıtım yapmadan istek sırasında kiracıya özel markalamayı ve erişim kontrollü özellikleri yükleyin. Next.js 15 Fullstack (App Router + Server Actions) ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

Next.js 15 Fullstack (App Router + Server Actions) öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te Next.js 15 Fullstack (App Router + Server Actions), başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 3. dersidir.

“Kiracı Başına Tema ve Özellik Bayrakları” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu Next.js 15 Fullstack (App Router + Server Actions) dersinde kod yazıp çalıştırabilir miyim?

Evet. Her Next.js 15 Fullstack (App Router + Server Actions) dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. Alt Alan Adı ve Yol Tabanlı Kiracı Çözümleme
  2. Satır Düzeyinde Kiracı Verisi Yalıtımı Desenleri
  3. Kiracı Başına Tema ve Özellik Bayrakları
  4. Kullanım Ölçümü ve Abonelik Uygulaması
← Next.js 15 Fullstack (App Router + Server Actions) Sayfasına Dön