# Što ćete naučiti#
Ovaj vodič objašnjava Next.js multitenant SaaS arhitekturu od temelja: modele tenancije, strategije rutiranja, razrješenje tenanta, auth obrasce i tehnike učvršćivanja kako biste spriječili curenje podataka.
Vidjet ćete kako se izbor baze mapira na Next.js App Router, middleware i moderne deploymente na Vercelu ili kontejnerskim platformama, uz konkretne primjere koda i checkliste koje možete odmah primijeniti.
# Osnove multitenancyja: što prvo morate odlučiti#
Multi-tenancy nije samo “dodaj tenantId stupac”. Morate odabrati kako se tenant-i razdvajaju u rutiranju, autentikaciji i pohrani podataka, a zatim te granice učiniti nepreskočivima.
Ovo su odluke koje utječu na sve ostalo:
| Odluka | Opcije | Utječe na |
|---|---|---|
| Adresa tenanta | Subdomena, prefiks putanje, custom domena | DNS, middleware razrješenje tenanta, opseg kolačića |
| Identitet tenanta | Korisnik pripada jednom tenantu, više tenanata ili role po tenantu | Dizajn sesije, provjere autorizacije |
| Izolacija podataka | Database-per-tenant, schema-per-tenant, row-level | Strategija migracija, obrasci upita, blast radius |
| Izolacija deploymenta | Jedna aplikacija, deployment po tenantu, hibridno | Trošak, usklađenost, debugiranje, skaliranje |
| Granice cachea | Cache ključevi po tenantu, dijeljeni cache, bez cachea | Rizik curenja podataka, performanse |
🎯 Ključna poruka: Prvo odaberite svoj model izolacije podataka, a zatim dizajnirajte rutiranje i auth tako da se granica baze ne može zaobići.
# Modeli tenancije za izolaciju podataka (s prednostima, manama i kada ih koristiti)#
U produkcijskom SaaS-u najčešće ćete vidjeti tri pristupa: database-per-tenant, schema-per-tenant i row-level. Svaki radi s Next.js-om, ali kompromisi su vrlo različiti.
Model 1: Database-per-tenant#
Svaki tenant dobiva vlastitu bazu. Ovo je najjača granica izolacije i dobro se uklapa u zahtjeve usklađenosti (compliance).
Najbolje za
- Visoku usklađenost ili regulirane workloadove
- Enterprise tenante koji traže izolaciju
- Velike tenante s posebnim performansnim potrebama
Kompromisi
- Operativni overhead brzo raste kako se broj tenanata povećava
- Migracije se moraju pokretati kroz mnogo baza
- Connection pooling postaje teži, posebno na serverlessu
Mapiranje u Next.js
- Razrješenje tenanta odabire ispravan connection string za bazu.
- Morate izbjegavati globalne singleton-e koji skrivaju tenant kontekst.
| Aspekt | Što se mijenja u Next.js-u |
|---|---|
| Razrješenje tenanta | Middleware razrješava tenant, server kod bira ispravnu bazu |
| Pristup podacima | DB klijent se mora kreirati po requestu ili uz per-tenant cache |
| Migracije | Pokretati po bazi, pratiti verzije po tenantu |
| Deployment | Obično jedna aplikacija, ali može i uz per-tenant infrastrukturu |
⚠️ Upozorenje: Na serverlessu, otvaranje velikog broja konekcija po tenantu može iscrpiti limite. Koristite pooler (npr. PgBouncer) ili managed serverless driver, i cacheajte konekcije po tenantu in-process samo kada vaš runtime jamči reuse.
Model 2: Schema-per-tenant#
Svi tenant-i dijele jednu bazu, ali svaki tenant ima zasebnu schemu. Izolacija je jača nego kod row-level modela, a migracije se mogu automatizirati po schemi.
Najbolje za
- Srednji broj tenanata
- Timove koji žele jasniju granicu izolacije nego row-level
- Postgres-heavy stackove gdje je tooling za scheme zreo
Kompromisi
- I dalje operativno zahtjevnije od row-level modela
- Velik broj schema može usporiti introspekciju i alate
- Cross-tenant analitika postaje kompleksnija
Mapiranje u Next.js
- Middleware razrješava tenant, sloj podataka postavlja
search_pathna tenant schemu. - Morate validirati odabir scheme na svakom requestu.
| Aspekt | Što se mijenja u Next.js-u |
|---|---|
| Razrješenje tenanta | Isto kao i kod drugih modela |
| Pristup podacima | Postaviti schema kontekst po requestu prije upita |
| Migracije | Primijeniti migracije po schemi |
| Analitika | Zahtijeva union view-e ili ETL |
Model 3: Row-level multi-tenancy#
Svi tenant-i dijele iste tablice, a svaki redak ima tenant_id. Ovo je najčešći model za early-to-mid stage SaaS jer ga je najbrže isporučiti.
Najbolje za
- Puno malih do srednjih tenanata
- Brzu iteraciju i visoku brzinu razvoja featurea
- Potrebe za zajedničkom analitikom i izvještavanjem
Kompromisi
- Najveći rizik curenja podataka između tenanata ako se oslanjate samo na aplikacijsku logiku
- Zahtijeva strogu disciplinu upita ili enforcement na razini baze
Mapiranje u Next.js
- Razrješenje tenanta daje vam
tenantId. - Svi upiti moraju biti tenant-scoped, idealno uz enforcement pomoću RLS-a.
| Aspekt | Što se mijenja u Next.js-u |
|---|---|
| Razrješenje tenanta | Potrebno na gotovo svakoj ruti |
| Pristup podacima | Upiti uvijek moraju uključiti tenant filter |
| Sigurnost | Toplo preporučeno koristiti Postgres RLS |
| Performanse | Dodati kompozitne indekse s tenantId |
💡 Savjet: U row-level tenanciji dodajte kompozitne indekse poput
tenant_id, created_atitenant_id, id. To je mala promjena sheme koja sprječava degradaciju performansi kako dataset raste.
# Tenant adresiranje i rutiranje u Next.js App Routeru#
Strategija rutiranja je ono zbog čega SaaS djeluje “tenant-native”. Dva najčešća uzorka su subdomena i prefiks putanje, plus hibrid za custom domene.
Opcije rutiranja i što podrazumijevaju#
| Uzorak | Primjer URL-a | Prednosti | Mane |
|---|---|---|---|
| Subdomena | acme.example.com | Čist UX, lako razdvajanje, izolacija kolačića po subdomeni | Treba wildcard DNS, lokalni development je zahtjevniji |
| Prefiks putanje | example.com/acme | Jednostavan lokalni dev, bez DNS kompleksnosti | Teže s custom domenama, cache ključevi moraju uključiti putanju |
| Custom domena | app ide na customer.com | Najbolji enterprise UX | Treba verifikaciju domene i tablicu mapiranja |
| Hibrid | subdomena plus custom domene | Pokriva sve segmente | Više edge caseova u razrješenju tenanta |
Uzorci strukture u App Routeru#
Uzorak s prefiksom putanje najjednostavnije se modelira u App Routeru:
app/[tenant]/(app)/dashboard/page.tsxapp/[tenant]/(app)/settings/page.tsxapp/[tenant]/(auth)/login/page.tsx
Ključ je držati sve tenant stranice pod segmentom [tenant] kako bi params.tenant uvijek bio dostupan.
Uzorak sa subdomenom obično drži rute tenant-agnostičnima, a middleware injecta tenant kontekst:
app/(app)/dashboard/page.tsxapp/(app)/settings/page.tsx
Tenant se razrješava iz Host headera, a ne iz putanje.
ℹ️ Napomena: Ako migrirate s Pages Routera, App Router olakšava centraliziranje server-side tenant provjera u layout-ima i route handlerima. Iskoristite to kao dio kontroliranog plana migracije poput onoga u vodiču Checklist za migraciju na Next.js App Router.
# Razrješenje tenanta: konkretan middleware obrazac#
Razrješenje tenanta znači pretvoriti dolazni request u pouzdan tenantId. Napravite to u middlewareu kako bi svaki request rano bio normaliziran, a zatim ponovno provjerite članstvo u server kodu.
Iz čega biste trebali razrješavati#
Redoslijed je bitan. Čest prioritetni popis je:
- 1Mapiranje custom domene po hostu
- 2Mapiranje subdomene po hostu
- 3Mapiranje po segmentu putanje
- 4Eksplicitni header samo za interne pozive
Primjer middlewarea: razrješenje tenanta po hostu ili putanji#
Ovaj primjer postavlja cookie tenant za downstream server komponente i route handlere. Također podržava custom domene kroz mapping funkciju.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const APP_HOST = 'example.com';
function getSubdomain(host: string) {
const h = host.split(':')[0];
if (!h.endsWith(APP_HOST)) return null;
const parts = h.replace(`.${APP_HOST}`, '').split('.');
return parts.length >= 1 ? parts[0] : null;
}
export async function middleware(req: NextRequest) {
const host = req.headers.get('host') || '';
const url = req.nextUrl;
const sub = getSubdomain(host);
const pathTenant = url.pathname.split('/')[1] || null;
const tenantSlug = sub || pathTenant;
if (!tenantSlug) return NextResponse.next();
// Replace this with a cache-friendly lookup, for example KV or DB read.
const tenantId = await resolveTenantId(tenantSlug, host);
if (!tenantId) return NextResponse.redirect(new URL('/not-found', url));
const res = NextResponse.next();
res.cookies.set('tenant', tenantId, { path: '/', sameSite: 'lax' });
return res;
}
export const config = {
matcher: ['/((?!_next|api/public|favicon.ico).*)'],
};
// Placeholder
async function resolveTenantId(tenantSlug: string, host: string) {
return tenantSlug === 'acme' ? 'tenant_123' : null;
}Ovaj middleware radi dvije važne stvari:
- Centralizira parsiranje i normalizaciju tenanta.
- Sprema razriješeni tenant identitet kao stabilan ID, a ne kao slug.
Zašto su stabilni ID-jevi bitni: slugovi se mogu mijenjati, a custom domene se mogu mapirati na isti tenant. Ako slug tretirate kao identitet tenanta, prije ili kasnije će doći do curenja podataka tijekom preimenovanja ili aliasiranja.
# Auth u multitenant Next.js-u: dizajn sesije i provjere članstva#
Autentikacija dokazuje tko je korisnik. Multitenancy zahtijeva autorizaciju koja dokazuje kojem tenantu korisnik smije pristupiti.
Sigurna postavka nameće ove provjere:
- Je li korisnik autentificiran
- Je li korisnik član traženog tenanta
- Koje role i dozvole ima unutar tog tenanta
Za opcije implementacije i produkcijske postavke koristite naš namjenski vodič: Vodič za Next.js autentikaciju s NextAuth, Clerk i Supabase.
Obrazac payload-a sesije: korisnik plus aktivni tenant#
Praktičan obrazac je “aktivni tenant” na sesiji, odvojen od popisa članstava.
| Polje | Primjer | Zašto postoji |
|---|---|---|
| userId | user_1 | Stabilan identitet |
| tenantId | tenant_123 | Aktivni tenant kontekst za request |
| roles | admin | Odluke o autorizaciji |
| memberTenants | tenant_123, tenant_456 | Sigurno prebacivanje tenanta |
| orgDomain | customer.com | Opcionalno za enterprise pravila domene |
Nametnite članstvo u server-only gateu#
Nemojte se oslanjati samo na middleware za kontrolu pristupa. Middleware radi na edgeu i može se zaobići u internim pozivima ako niste oprezni.
Robustan obrazac je:
- 1Middleware razriješi tenantId.
- 2Server kod pročita tenantId i sesiju.
- 3Server kod verificira članstvo za svaki request koji čita ili piše podatke.
Primjer guard-a za Route Handler:
// app/api/projects/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET() {
const tenantId = cookies().get('tenant')?.value;
const session = await getSession();
if (!session?.userId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
if (!tenantId) return NextResponse.json({ error: 'tenant_missing' }, { status: 400 });
const member = await isMember(session.userId, tenantId);
if (!member) return NextResponse.json({ error: 'forbidden' }, { status: 403 });
const projects = await listProjects({ tenantId });
return NextResponse.json({ projects });
}
// Placeholder functions
async function getSession() { return { userId: 'user_1' }; }
async function isMember(userId: string, tenantId: string) { return true; }
async function listProjects(input: { tenantId: string }) { return [{ id: 1 }]; }Ovaj obrazac se dobro skalira jer gura tenant kontekst u svaki DB poziv. Ako developer zaboravi tenant filter, code review i testovi to mogu uhvatiti, ali izolaciju biste ipak trebali nametnuti na razini baze.
# Nametanje izolacije podataka: kako u praksi spriječiti curenja#
Većina tenant curenja dogodi se zbog jednog od ovoga:
- Upit bez tenant filtera
- Cache koji miješa tenante
- Background jobovi koji rade bez tenant konteksta
- Admin endpointi koji izlažu cross-tenant podatke
“Secure by construction” multitenant arhitektura otežava napraviti pogrešku.
Row-level tenancija s Postgres RLS-om#
Ako koristite row-level tenanciju, Row Level Security je najučinkovitiji guardrail jer vas štiti čak i kad aplikacijski kod pogriješi.
Minimalni RLS obrazac:
- 1Dodajte
tenant_idu tablice. - 2Uključite RLS.
- 3Postavite session varijablu poput
app.tenant_id. - 4Napravite policy koji uspoređuje
tenant_idsa session varijablom.
-- Example for Postgres
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::text);
-- In your DB session, set:
-- SET app.tenant_id = 'tenant_123';Da bi ovo radilo, vaš DB access layer mora postaviti app.tenant_id po requestu prije bilo kojeg upita. Ovo se dobro uparuje sa server-only kodom u Next.js route handlerima i server actions.
⚠️ Upozorenje: Nemojte koristiti jednu dijeljenu DB konekciju kroz više requestova ako se oslanjate na session varijable. Tenant kontekst može procuriti između requestova ako je pooling krivo konfiguriran. Osigurajte da se tenant kontekst postavlja unutar transakcije ili po checked-out konekciji.
Schema-per-tenant enforcement#
U schema-per-tenant modelu izolacija se nameće granicama schema, ali morate spriječiti slučajne cross-schema upite.
Tipične zaštite:
- Postaviti
search_pathpo requestu na tenant schemu - Držati shared tablice u zasebnoj schemi poput
publicilishared - Ograničiti permissions tako da app role ne može čitati druge tenant scheme
Praktičan pristup je generirati ime scheme iz stabilnog tenant ID-ja poput t_tenant_123. Izbjegavajte direktan user input za imena schema.
Database-per-tenant enforcement#
Database-per-tenant konceptualno je jednostavniji:
- Razriješite tenant
- Koristite tenantov connection string
- Pokrećite upite normalno
Gdje timovi najčešće “nastradaju” je operativa:
- automatizirane migracije
- rotacija credentials-a
- observability kroz mnogo baza
- connection limiti i dimenzioniranje poola
Ako idete ovim putem, od prvog dana implementirajte tenant provisioning kao automatizaciju. Ručni provisioning ne preživi nakon šačice tenanata.
# Next.js deployment i runtime razmatranja za multitenancy#
Next.js multitenancy često pukne na rubovima: ponašanje middlewarea, cacheanje i background rad.
Edge middleware i gdje tenant logika treba živjeti#
Middleware je odličan za:
- Redirectanje na tenant-specifične rute
- Normalizaciju tenant konteksta
- Rano blokiranje očito nevažećih tenanata
Middleware nije dovoljan za:
- Odluke o autorizaciji
- Pristup bazi
- Role-based dozvole
Držite middleware laganim i determinističkim. Provjere članstva stavite u server kod.
Cacheanje i dohvat podataka: tenant-aware po defaultu#
Curenja se često dogode kad se cache ključevi vežu samo za URL ili samo za naziv upita.
Koristite ova pravila:
- Ako se odgovor razlikuje po tenantu, cache ključ mora uključiti
tenantId. - Ako se odgovor razlikuje po korisniku, cache ključ mora uključiti
userIdili odgovor ne smije biti cacheiran. - Nikad nemojte spremati tenant-specifične podatke u globalni in-memory cache bez tenant ključa.
Za fetch pozive u server komponentama:
- Preferirajte server-side funkcije za pristup podacima koje zahtijevaju
tenantId. - Izbjegavajte implicitne globale poput “currentTenant” spremljene u module scope.
Background jobovi i automatizacije#
Ako pokrećete cron jobove, queueove ili n8n workflowe, tenant kontekst mora biti eksplicitan. Payload joba uvijek bi trebao uključiti tenantId.
Primjer polja payload-a:
| Polje | Primjer | Svrha |
|---|---|---|
| tenantId | tenant_123 | Izolacija tenanta |
| jobType | invoice_run | Rutiranje i logiranje |
| idempotencyKey | inv_2026_04_14_tenant_123 | Sprječavanje dvostrukog izvršenja |
| actorUserId | user_1 | Audit trail |
| traceId | trace_abc | Debugging kroz servise |
Ako automatizirate interne operacije, gradite workflowe koji su tenant-aware od prvog čvora. Missing tenantId tretirajte kao hard failure.
# Praktičan arhitekturni blueprint#
Ovaj blueprint odgovara onome što implementiramo za većinu SaaS timova kojima trebaju brzina i sigurnost.
Preporučena osnova za većinu proizvoda#
Za early i growth-stage SaaS:
- Row-level tenancija plus Postgres RLS
- Subdomain rutiranje plus opcionalne custom domene
- Middleware za razrješenje tenanta
- Server-only provjere članstva za autorizaciju
- Tenant-aware pravila cacheanja
Za enterprise-heavy SaaS:
- Schema-per-tenant ili database-per-tenant za specifične tenante
- Hibridni model za “VIP” tenante može se isplatiti ako app-layer API ostane stabilan
Minimalni multitenant data model#
Neka osnovni entiteti budu eksplicitni:
| Tablica | Ključna polja | Napomene |
|---|---|---|
| tenants | id, slug, plan, created_at | Stabilan ID, slug za prikaz |
| domains | domain, tenant_id, verified_at | Mapira custom domene |
| users | id, email | Globalni identitet |
| memberships | user_id, tenant_id, role | Many-to-many |
| projects | id, tenant_id, name | Tenant-scoped podaci |
Ova struktura podržava:
- korisnike koji pripadaju više tenanata
- prebacivanje tenanta
- sigurne provjere autorizacije
# Checkliste: što provjeriti prije isporuke#
Koristite ove checkliste u PR reviewima i pred-release hardeningu.
Checklist za rutiranje i razrješenje tenanta#
- Tenant se deterministički razrješava iz Host i putanje.
- Tenant se sprema kao stabilan ID, ne samo kao slug.
- Requestovi bez tenant konteksta failaju rano za tenant-only stranice.
- Custom domene mapiraju se kroz zasebnu tablicu i flow verifikacije.
- Tenant je uključen u logove i error reporte kao polje.
Checklist za auth i autorizaciju#
- Svaki tenant-scoped request provjerava članstvo server-side.
- Provjere rola rade se nakon potvrde članstva.
- Prebacivanje tenanta je eksplicitno i auditirano.
- Sesija uključuje aktivni tenant kontekst ili se tenant izvodi i validira na svakom requestu.
- Public rute su eksplicitno whitelisted.
Za dublji security review prođite širi popis hardeninga poput Checklist za sigurnost web aplikacija.
Checklist za izolaciju podataka i sprječavanje curenja#
- Row-level model ima izolaciju nametnutu bazom, idealno RLS.
- Upiti se ne mogu izvršiti bez tenant konteksta.
- Background jobovi zahtijevaju
tenantIdu payload-u. - Cache ključevi uključuju
tenantIdi user kontekst gdje je potrebno. - Admin endpointi su odvojeni i zaštićeni, ne “samo skriveni”.
💡 Savjet: Dodajte automatizirani test koji kreira dva tenanta i potvrđuje da svaki endpoint vraća podatke samo za aktivni tenant. To hvata propuštene tenant filtere brže od code reviewa.
# Česte zamke (i kako ih izbjeći)#
Zamka 1: “Middleware je razriješio tenant, sigurni smo”#
Middleware može normalizirati, ali ne smije biti jedini sloj enforcementa. Uvijek verificirajte članstvo u tenantu u server kodu prije pristupa podacima.
Zamka 2: Tenant kontekst spremljen u module scope#
U Next.js-u, module scope može preživjeti između requestova u nekim runtimeovima. Ako “trenutnog tenanta” spremite u globalnu varijablu, može se preliti u druge requestove.
Zamka 3: Cacheanje odgovora bez tenant-aware ključeva#
Cacheanje tenant-specifičnih odgovora pod dijeljenim ključevima je klasično curenje. Ako morate cacheati, uključite tenant i user faktore u cache ključ ili nemojte cacheati.
Zamka 4: Background jobovi bez tenantId#
Job bez tenant konteksta postaje slučajno cross-tenant. Failajte odmah kad tenantId nedostaje i učinite ga obaveznim u shemama queuea.
# Ključne poruke#
- Odaberite model tenancije rano i nametnite ga kao čvrstu granicu, idealno na razini baze za row-level sustave.
- Koristite Next.js middleware za razrješenje tenanta, ali provjere članstva i rola namećite u server komponentama i route handlerima.
- Tenant identitet tretirajte kao stabilan ID i razrješavajte ga iz hosta, mapiranja custom domena ili putanje determinističkim prioritetnim redoslijedom.
- Spriječite curenja tako da tenant kontekst bude obavezan u svakom upitu, cache ključu i payload-u background jobova.
- Dodajte automatizirane boundary testove s najmanje dva tenanta kako biste uhvatili propuštene filtere i cache greške prije releasea.
# Zaključak#
Produkcijski spremna Next.js multitenant SaaS arhitektura uglavnom se svodi na uklanjanje “opcijskog” tenant konteksta. Razrješenje tenanta, auth i pristup podacima moraju svi zahtijevati isti tenant identitet, a baza bi trebala nametnuti izolaciju kad god je to moguće.
Ako želite review vašeg trenutnog multitenant dizajna ili pomoć oko implementacije RLS-a, rutiranja custom domena i tenant-safe auth-a u App Routeru, javite se Samiodi. Pomoći ćemo vam isporučiti brže bez rizika curenja podataka između tenanata.
FAQ
Više iz kategorije Web razvoj
Sve →Observabilnost web aplikacija: praktični vodič za logove, metrike i tracing za React i Next.js
End-to-end, produkcijski spremna observability postava za React i Next.js: praćenje grešaka, nadzor performansi, strukturirani logovi, tracing, nadzorne ploče i alerti koji hvataju stvarne probleme.
Arhitektura React komponenti za skaliranje: obrasci za održiv dizajnerski sustav
Pragmatična arhitektura React komponenti za velike React i Next.js codebaseove: kompozicija, složene (compound) i polimorfne komponente, tematiziranje, konvencije mapa, anti-uzorci i plan refaktoriranja koji vaš tim može pratiti.
Next.js autentikacija u 2026.: NextAuth vs Clerk vs Supabase (što koristimo na klijentskim projektima)
Praktična usporedba opcija za Next.js autentikaciju u 2026. — NextAuth, Clerk i Supabase — kroz UX, sigurnost, trošak, vrijeme postavljanja i enterprise zahtjeve, uz matrice odluke za SaaS, interne alate i B2B portale.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Next.js autentikacija u 2026.: NextAuth vs Clerk vs Supabase (što koristimo na klijentskim projektima)
Praktična usporedba opcija za Next.js autentikaciju u 2026. — NextAuth, Clerk i Supabase — kroz UX, sigurnost, trošak, vrijeme postavljanja i enterprise zahtjeve, uz matrice odluke za SaaS, interne alate i B2B portale.
Arhitektura React komponenti za skaliranje: obrasci za održiv dizajnerski sustav
Pragmatična arhitektura React komponenti za velike React i Next.js codebaseove: kompozicija, složene (compound) i polimorfne komponente, tematiziranje, konvencije mapa, anti-uzorci i plan refaktoriranja koji vaš tim može pratiti.
Kontrolni popis za migraciju na Next.js App Router (s Pages Routera) + česte zamke
Praktičan, korak-po-korak plan migracije na Next.js App Router s Pages Routera, uključujući kontrolni popis za routing, dohvat podataka, SEO metadata, deployment i vodič za rješavanje čestih zamki.