Next.js 15 -fullstack-kehitys (App Router + Server Actions) · Oppitunti

Tenanttikohtaiset teemat ja ominaisuusliput

Ladatkaa tenanttikohtainen brändäys ja käytön mukaan rajatut ominaisuudet pyynnön yhteydessä ilman uutta käyttöönottoa.

Oppitunti 3/413 vaihetta

Tenanttikohtaiset teemat ja ominaisuusliput on ilmainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.

Mitä tenant-kohtainen teemoitus tarkoittaa

Monen tenantin SaaS-sovelluksessa jokainen asiakas odottaa usein, että tuote tuntuu heidän omaltaan. Tenant-kohtainen teemoitus tarkoittaa eri väreistä, logosta, typografiasta tai asettelusta koostuvan teeman lataamista pyynnön aikana ilman uuden koontiversion toimittamista.

  • White labeling: tenantin brändi korvaa oman brändinne kokonaan.
  • Korostusten ylikirjoitukset: yhteinen käyttöliittymä, jossa ensisijaiset värit ja fontit määritetään tenant-kohtaisesti.
  • Ominaisuusliput: tietyt käyttöliittymäelementit tai API-reitit otetaan käyttöön vain tietyille tenanteille heidän tilauksensa tai määritystensä perusteella.

Next.js 15 App Router sopii tähän erinomaisesti, koska jokainen pyyntö kulkee Server Components -komponenttien ja middlewaren kautta, joten tenant-kontekstin ratkaisemiseen ennen minkään renderöintiä on luonteva paikka.

Tenantin ratkaiseminen reunalla

Ensimmäinen vaihe on tunnistaa, mikä tenant pyynnön tekee. Kaksi yleisintä strategiaa ovat:

  • Aliveritysreititys: acme.app.com → tenantin tunniste = acme
  • Muokatun verkkotunnuksen yhdistäminen: dashboard.acme.com → tenant haetaan host-otsakkeen perusteella

Next.js 15:n middleware suoritetaan reunalla ennen kaikkia Server Component -komponentteja, joten se on oikea paikka tenantin ratkaisemiseen ja kontekstin välittämiseen eteenpäin pyyntöotsakkeiden avulla.

// 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).*)'],
};

Tenant-määrityksen skeeman määrittäminen

Määritä ennen tenant-tietojen hakemista selkeä TypeScript-tyyppi, joka kuvaa kaiken tenantin muokattavissa olevan. Tämä muodostaa tietokannan ja käyttöliittymän välisen sopimuksen.

  • Brändäys: ensisijainen väri, logon URL-osoite ja fonttiperhe.
  • Ominaisuusliput: merkkijonoavainten ja totuusarvojen muodostama tietue, joten uusia lippuja voidaan lisätä ilman skeeman muutoksia.
  • Tilauspaketin taso: käytetään kokonaisien ominaisuusjoukkojen käyttöoikeuksien hallintaan.
// 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;
}

Tenant-määrityksen hakeminen palvelimella

Kun tunniste on välitetty otsakkeiden kautta, mikä tahansa Server Component tai Server Action voi ratkaista täyden TenantConfig-määrityksen tietokannasta tai nopeasta välimuistista (Redis, Vercel KV jne.).

Käyttäkää Reactin cache()-funktiota haun deduplikointiin yhden pyynnön sisällä — funktiota kutsutaan useita kertoja sisäkkäisten asettelujen ja sivujen kautta, mutta tietokantakysely suoritetaan vain kerran.

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

CSS-muuttujien lisääminen brändäystä varten

Selkein tapa käyttää tenant-kohtaisia värejä ja fontteja on määrittää CSS-muuttujat juurielementtiin. Tailwind- tai tavalliset CSS-luokat viittaavat näihin muuttujiin — arvot vaihtuvat tenantin mukaan, mutta luokkien nimet eivät muutu.

Hakekaa tenant-määritys juuriasettelussa ja tulostakaa rivinsisäinen <style>-tagi. Koska kyseessä on Server Component, asiakkaalla ei tarvita lainkaan JavaScriptiä.

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

Tenantin logon ja nimen renderöiminen

Kun getTenantConfig on muistioitu cache()-funktion avulla, voitte kutsua sitä vapaasti missä tahansa sisäkkäisessä Server Component -komponentissa. Tulos on sama objekti, jota käytetään uudelleen ensimmäisen kutsun jälkeen — uusia tietokantakierroksia ei tarvita.

Jaettu TenantHeader-Server Component lukee määrityksen ja renderöi tenantin logon ja yrityksen nimen ilman, että props-arvoja tarvitsee välittää ylemmistä asetteluista.

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

Ominaisuuslippujen käyttöoikeuksien hallinta Server Components -komponenteissa

Tenant-määritykseen tallennettujen ominaisuuslippujen avulla käyttöliittymän kokonaisia osioita voidaan renderöidä ehdollisesti palvelimella, joten käytöstä poistetut ominaisuudet eivät päädy lainkaan asiakkaan pakettiin.

  • Kutsukaa isFeatureEnabled(config, 'analytics_dashboard') Server Component -komponentin sisällä.
  • Jos lippu ei ole käytössä, palauttakaa null tai päivityskehote — komponentin koodi on edelleen paketissa, mutta tulos jätetään pois.
  • Tämä on turvallisempaa kuin käyttöoikeuksien hallinta asiakkaalla, koska päättäväinen käyttäjä voisi tarkastella JavaScriptiä.
// 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>
  );
}

API-reittien suojaaminen ominaisuuslipuilla

Käyttöliittymän käyttöoikeuksien hallinta ei riitä — päättäväinen käyttäjä voi kutsua API-reittiä suoraan. Valvokaa ominaisuusliput aina myös Route Handlerissa tai Server Actionissa.

Luokaa uudelleenkäytettävä guard-apufunktio, joka ratkaisee tenant-määrityksen ja heittää tyypitetyn virheen, jos pyydetty ominaisuus ei ole käytössä. Näin valvontalogiikka pysyy yhdessä paikassa.

// 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 ja ominaisuuksien käyttöoikeuksien hallinta

Server Actions -toimintoja kutsutaan suoraan Client Components -komponenteista, ja myös niiden käyttöä voidaan rajoittaa. Koska Server Actions suoritetaan palvelimella, requireFeature toimii samalla tavalla — tenant-konteksti ratkaistaan pyyntöotsakkeista, jotka Next.js välittää automaattisesti.

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

Tenant-määrityksen tehokas välimuistitus

Tietokantahaut jokaisella pyynnöllä olisivat liian hitaita. Käyttäkää kaksikerroksista välimuististrategiaa:

  • 1. kerros — Reactin cache(): deduplikoi haut yhden pyynnön sisällä (käytössä jo getTenantConfig-funktion kautta).
  • 2. kerros — ulkoinen välimuisti (Redis / Vercel KV): tallentaa ratkaistun määrityksen N minuutiksi, jolloin seuraavat pyynnöt ohittavat tietokannan kokonaan.

Kun tenant päivittää brändäystään ylläpitopaneelissa, mitätöikää hänen välimuistiavaimensa Server Actionin tai webhook-käsittelijän avulla.

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

Vain vähimmäismäärän määrityksiä asiakkaalle

Joskus Client Component tarvitsee tenantin ensisijaisen värin tai tiedon siitä, onko jokin ominaisuus käytössä — esimerkiksi vuorovaikutteisen kaavion tyylittelyä tai painikkeen ehdollista renderöintiä varten.

Älkää koskaan sarjallistako koko TenantConfig-määritystä asiakkaalle, sillä se voi sisältää arkaluonteisia tilauspaketti- tai hintatietoja. Luokaa sen sijaan rajattu, asiakkaalle turvallinen näkymä ja välittäkää se props-arvona Server Component -komponentista tai tarjotkaa se kevyen kontekstin kautta.

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

Tietotesti: missä ominaisuusliput pitäisi valvoa

Kollega väittää, että ominaisuuslippujen tarkistaminen vain Client Components -komponenteissa riittää, koska käyttöliittymä yksinkertaisesti piilottaa käytöstä poistetut ominaisuudet. Mikä on tämän lähestymistavan keskeinen puute?

Oppitunnin yhteenveto: tenant-kohtainen teemoitus ja ominaisuusliput

Teillä on nyt valmis pyyntöhetkellä toimiva monen tenantin järjestelmä Next.js 15:lle. Rakensitte seuraavat osat:

  • Middleware poimii tenantin tunnisteen aliverityksestä tai host-otsakkeesta ja välittää sen mukautetun pyyntöotsakkeen kautta.
  • getTenantConfig käyttää Reactin cache()-funktiota täyden TenantConfig-määrityksen ratkaisemiseen tietokannasta täsmälleen kerran pyyntöä kohti, ja ulkoinen Redis/KV-kerros huolehtii pyyntöjen välisestä välimuistista.
  • Juuriasetteluun lisätyt CSS-muuttujat määrittävät tenant-kohtaiset värit ja fontit ilman asiakkaan JavaScriptiä.
  • Ominaisuusliput tarkistetaan palvelimella — Server Components -komponenteissa, Route Handlereissa ja Server Action -toiminnoissa — joten käytöstä poistetut ominaisuudet ovat sekä näkymättömiä että saavuttamattomia.
  • Asiakkaalle sarjallistetaan vain vähimmäismäärä asiakkaalle turvallisia määrityksiä, mikä suojaa arkaluonteisia tilauspaketti- ja hintatietoja.

Tämä arkkitehtuuri skaalautuu siististi: uuden ominaisuuslipun lisääminen edellyttää vain tietokantasarakkeen muutosta ja yhtä isFeatureEnabled-kutsua — ei uudelleenjulkaisuja eikä tenant-kohtaisia koontiversioita.

Aloita maksutta

Opi TypeScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
22
Oppitunnit
88

Usein kysytyt kysymykset

Onko oppitunti ”Tenanttikohtaiset teemat ja ominaisuusliput” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolun 3 oppituntia, myös oppitunnin “Tenanttikohtaiset teemat ja ominaisuusliput”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Tenanttikohtaiset teemat ja ominaisuusliput”?

Ladatkaa tenanttikohtainen brändäys ja käytön mukaan rajatut ominaisuudet pyynnön yhteydessä ilman uutta käyttöönottoa. Harjoittelet Next.js 15 -fullstack-kehitys (App Router + Server Actions)-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Next.js 15 -fullstack-kehitys (App Router + Server Actions)-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Tenanttikohtaiset teemat ja ominaisuusliput”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunnilla?

Kyllä. Jokainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Tenantin ratkaiseminen aliverkkotunnuksen ja polun perusteella
  2. Tenanttien rivitason tietojen eristämismallit
  3. Tenanttikohtaiset teemat ja ominaisuusliput
  4. Käytön mittaus ja tilausten valvonta
← Takaisin: Next.js 15 -fullstack-kehitys (App Router + Server Actions)