Tenanttikohtaiset teemat ja ominaisuusliput
Ladatkaa tenanttikohtainen brändäys ja käytön mukaan rajatut ominaisuudet pyynnön yhteydessä ilman uutta käyttöönottoa.
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
nulltai 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ä jogetTenantConfig-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.
getTenantConfigkäyttää Reactincache()-funktiota täydenTenantConfig-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.
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
- Tenantin ratkaiseminen aliverkkotunnuksen ja polun perusteella
- Tenanttien rivitason tietojen eristämismallit
- Tenanttikohtaiset teemat ja ominaisuusliput
- Käytön mittaus ja tilausten valvonta