Web razvoj
Next.jsSaaSViše-tenanataApp RouterSigurnostArhitekturaPostgreSQLAuth

Next.js multitenant SaaS arhitektura: modeli tenancije, rutiranje, autentikacija i izolacija podataka (Vodič za 2026.)

AO
Adrijan Omićević
·15 min čitanja

# Š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:

OdlukaOpcijeUtječe na
Adresa tenantaSubdomena, prefiks putanje, custom domenaDNS, middleware razrješenje tenanta, opseg kolačića
Identitet tenantaKorisnik pripada jednom tenantu, više tenanata ili role po tenantuDizajn sesije, provjere autorizacije
Izolacija podatakaDatabase-per-tenant, schema-per-tenant, row-levelStrategija migracija, obrasci upita, blast radius
Izolacija deploymentaJedna aplikacija, deployment po tenantu, hibridnoTrošak, usklađenost, debugiranje, skaliranje
Granice cacheaCache ključevi po tenantu, dijeljeni cache, bez cacheaRizik 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 tenantaMiddleware razrješava tenant, server kod bira ispravnu bazu
Pristup podacimaDB klijent se mora kreirati po requestu ili uz per-tenant cache
MigracijePokretati po bazi, pratiti verzije po tenantu
DeploymentObič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_path na tenant schemu.
  • Morate validirati odabir scheme na svakom requestu.
AspektŠto se mijenja u Next.js-u
Razrješenje tenantaIsto kao i kod drugih modela
Pristup podacimaPostaviti schema kontekst po requestu prije upita
MigracijePrimijeniti migracije po schemi
AnalitikaZahtijeva 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 tenantaPotrebno na gotovo svakoj ruti
Pristup podacimaUpiti uvijek moraju uključiti tenant filter
SigurnostToplo preporučeno koristiti Postgres RLS
PerformanseDodati kompozitne indekse s tenantId

💡 Savjet: U row-level tenanciji dodajte kompozitne indekse poput tenant_id, created_at i tenant_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#

UzorakPrimjer URL-aPrednostiMane
Subdomenaacme.example.comČist UX, lako razdvajanje, izolacija kolačića po subdomeniTreba wildcard DNS, lokalni development je zahtjevniji
Prefiks putanjeexample.com/acmeJednostavan lokalni dev, bez DNS kompleksnostiTeže s custom domenama, cache ključevi moraju uključiti putanju
Custom domenaapp ide na customer.comNajbolji enterprise UXTreba verifikaciju domene i tablicu mapiranja
Hibridsubdomena plus custom domenePokriva sve segmenteViš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.tsx
  • app/[tenant]/(app)/settings/page.tsx
  • app/[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.tsx
  • app/(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:

  1. 1
    Mapiranje custom domene po hostu
  2. 2
    Mapiranje subdomene po hostu
  3. 3
    Mapiranje po segmentu putanje
  4. 4
    Eksplicitni 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.

TypeScript
// 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.

PoljePrimjerZašto postoji
userIduser_1Stabilan identitet
tenantIdtenant_123Aktivni tenant kontekst za request
rolesadminOdluke o autorizaciji
memberTenantstenant_123, tenant_456Sigurno prebacivanje tenanta
orgDomaincustomer.comOpcionalno 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:

  1. 1
    Middleware razriješi tenantId.
  2. 2
    Server kod pročita tenantId i sesiju.
  3. 3
    Server kod verificira članstvo za svaki request koji čita ili piše podatke.

Primjer guard-a za Route Handler:

TypeScript
// 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:

  1. 1
    Dodajte tenant_id u tablice.
  2. 2
    Uključite RLS.
  3. 3
    Postavite session varijablu poput app.tenant_id.
  4. 4
    Napravite policy koji uspoređuje tenant_id sa session varijablom.
SQL
-- 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_path po requestu na tenant schemu
  • Držati shared tablice u zasebnoj schemi poput public ili shared
  • 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 userId ili 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:

PoljePrimjerSvrha
tenantIdtenant_123Izolacija tenanta
jobTypeinvoice_runRutiranje i logiranje
idempotencyKeyinv_2026_04_14_tenant_123Sprječavanje dvostrukog izvršenja
actorUserIduser_1Audit trail
traceIdtrace_abcDebugging 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:

TablicaKljučna poljaNapomene
tenantsid, slug, plan, created_atStabilan ID, slug za prikaz
domainsdomain, tenant_id, verified_atMapira custom domene
usersid, emailGlobalni identitet
membershipsuser_id, tenant_id, roleMany-to-many
projectsid, tenant_id, nameTenant-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 tenantId u payload-u.
  • Cache ključevi uključuju tenantId i 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

Share
A
Adrijan OmićevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.