Predstavte si najhorší možný scenár pre akéhokoľvek zakladateľa alebo technického riaditeľa B2B SaaS platformy. Ráno otvoríte schránku a v nej svieti e-mail od jedného z vašich najväčších klientov: „Dobrý deň, keď som si dnes ráno otvoril sekciu faktúr v klientskej zóne, na zlomok sekundy som uvidel zoznam faktúr inej firmy. Stiahol som si PDF a naozaj sú tam dôverné finančné údaje našej konkurencie. Čo to má znamenať?“
V tom momente vám zamrzne krv v žilách. Únik dát v B2C segmente je nepríjemný, no únik citlivých korporátnych dát, cenníkov, zoznamov klientov alebo zmlúv v B2B sektore môže znamenať okamžitú stratu dôvery, porušenie zmlúv o mlčanlivosti (NDA) a pre mnohé startupy aj existenčný problém.
Pri vývoji multi-tenant B2B aplikácií (kde viacero firiem zdieľa rovnaký softvér) je silná izolácia dát jedno z najdôležitejších inžinierskych kritérií. Ako však túto izoláciu navrhnúť tak, aby systém zostal škálovateľný, ľahko udržiavateľný a zároveň výrazne znižoval riziko ľudskej chyby vývojárov?
V technologickom štúdiu nolimeo sa pri návrhoch B2B SaaS infraštruktúr vyhýbame povrchným a krehkým riešeniam na úrovni aplikačného kódu. V tomto technickom článku si rozoberieme, prečo tradičný spôsob izolácie dát často zlyháva, ako funguje moderná multi-tenant architektúra postavená na PostgreSQL Row Level Security (RLS) a Supabase, a ukážeme si konkrétne SQL politiky a optimalizačné postupy, ktoré pomáhajú držať dáta klientov oddelené už na úrovni databázy.
1. Dilema multi-tenant databáz: Ktorú cestu zvoliť?
Keď navrhujete systém, ktorý budú využívať desiatky, stovky alebo tisíce firiem (tenantov), musíte sa rozhodnúť, ako ich dáta fyzicky a logicky usporiadate v databáze. Existujú tri základné prístupy:
A. Fyzická izolácia (Database-per-Tenant)
Každá firma má vlastnú úplne samostatnú databázu.
- Výhody: Maximálna bezpečnosť. Únik dát medzi firmami je prakticky nemožný. Každý klient môže mať vlastné zálohy a bežať na samostatnom hardvéri.
- Nevýhody: Veľmi nákladná prevádzka infraštruktúry a zložitá správa. Predstavte si, že máte 500 klientov a potrebujete pridať jeden stĺpec do tabuľky. Musíte spustiť 500 databázových migrácií a udržiavať 500 connection poolov.
B. Izolácia na úrovni schém (Schema-per-Tenant)
Všetky firmy zdieľajú jednu fyzickú databázu, ale každá má vlastnú databázovú schému (namespace).
- Výhody: Dobrý kompromis medzi bezpečnosťou a zdieľaním zdrojov.
- Nevýhody: PostgreSQL nie je optimálne prispôsobený na spracovanie desiatok tisíc schém. Výrazne narastá veľkosť systémového katalógu (catalog bloat), spomaľuje sa štart a migrácie sú stále komplikované.
C. Logická izolácia v zdieľanej schéme (Shared Database, Shared Schema)
Všetky firmy zdieľajú rovnaké tabuľky. Každý riadok v každej tabuľke obsahuje stĺpec tenant_id (napr. identifikátor firmy), podľa ktorého sa dáta filtrujú.
- Výhody: Najlacnejšia prevádzka, jednoduché databázové migrácie (zmena schémy prebehne raz pre všetkých) a jednoduché globálne agregácie pre administrátorov.
- Nevýhody: Extrémne riziko ľudskej chyby.
V tradičnom prístupe C sa izolácia riadi na úrovni backendovej aplikácie (ORM dopytov). Programátor musí pri každom dopyte explicitne napísať filter:
// Tradičné ORM, náchylné na kritické chyby
const customerInvoices = await db.invoices.findMany({
where: {
tenant_id: activeUser.tenant_id // Ak na toto vývojár zabudne, môže sprístupniť cudzie faktúry.
}
});
Stačí jeden unavený vývojár, jeden prehliadnutý riadok pri code review, alebo zložitejší JOIN dopyt, v ktorom filter chýba, a databáza vráti dáta iného klienta. Spoliehať sa na to, že človek nikdy neurobí chybu v tisíckach riadkov aplikačného kódu, je hazard.
A práve tu prichádza na scénu PostgreSQL Row Level Security (RLS).
2. PostgreSQL RLS: Presun bezpečnosti z aplikácie do databázového jadra
Row Level Security (RLS) je zabudovaná funkcia PostgreSQL, ktorá presúva veľkú časť zodpovednosti za izoláciu dát z aplikačného backendu (napr. Node.js/Next.js) priamo do databázového jadra.
Princíp je jednoduchý: databáza preverí prichádzajúcu query a na základe definovaných pravidiel (politík) automaticky pridá potrebné obmedzenia. Aj keď vaša backendová aplikácia pošle dopyt:
SELECT * FROM invoices;
PostgreSQL RLS ho na pozadí prepíše a vykoná ako:
SELECT * FROM invoices WHERE tenant_id = 'aktualna-firma-pouzivatela';
Bez ohľadu na to, aký framework používate, ako zložité sú vaše dopyty alebo či vývojár zabudol napísať filter. Databáza nepovolí prenos riadkov, ktoré nespĺňajú bezpečnostnú politiku.
Aplikačný Backend (Next.js / Node.js)
│
▼ (Chce vykonať: SELECT * FROM invoices;)
┌──────────────────────────────────────────────┐
│ PostgreSQL Databázové Jadro │
│ │
│ [ RLS Engine ] ◄── Overí aktívny JWT / Rolu │
│ │ │
│ ▼ (Automatická transformácia dopytu) │
│ SELECT * FROM invoices WHERE tenant_id = X; │
└──────────────────────────────────────────────┘
3. Technická implementácia a SQL DDL príklady
Ukážme si, ako navrhnúť robustný databázový model pre multi-tenant B2B platformu. Využijeme PostgreSQL integrovaný v Supabase, ktorý spája používateľskú autentifikáciu (auth.users) s databázovými tabuľkami a JWT tokenmi.
Krok 1: Definovanie schémy tabuliek
Vytvoríme tri základné tabuľky: firmy (tenants), profily zamestnancov (users) a samotné B2B dáta - faktúry (invoices).
-- Aktivácia UUID rozšírenia pre bezpečné generovanie ID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 1. Tabuľka firiem (Tenanti)
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- 2. Tabuľka používateľov (prepojená na Supabase Auth)
CREATE TABLE users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE NOT NULL,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'member' NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- 3. Tabuľka B2B dát (Faktúry)
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE NOT NULL,
client_name VARCHAR(255) NOT NULL,
amount NUMERIC(10, 2) NOT NULL,
due_date DATE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
Krok 2: Aktivácia Row Level Security (RLS)
Po vytvorení tabuliek je RLS predvolene vypnuté. Musíme ho explicitne zapnúť. Pre silnejšiu ochranu použijeme aj príkaz FORCE, ktorý zabezpečí, že RLS bude platiť aj pre majiteľa tabuľky (table owner), čím obmedzíme nečakané obchádzanie pravidiel.
-- Aktivácia RLS pre tabuľku používateľov a faktúr
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
Krok 3: Tvorba RLS politík (Klasický prístup cez JOIN)
Teraz povieme databáze, za akých podmienok môže používateľ vidieť a upravovať riadky v tabuľke invoices. Klasický prístup spočíva v porovnaní tenant_id faktúry s tenant_id prihláseného používateľa z tabuľky users.
V Supabase získame ID aktuálne prihláseného používateľa pomocou vstavanej funkcie auth.uid(), ktorá bezpečne parsuje ID z overeného JWT tokenu.
-- Politika pre prístup k faktúram
CREATE POLICY select_tenant_invoices ON invoices
FOR SELECT
TO authenticated
USING (
tenant_id = (
SELECT tenant_id
FROM users
WHERE id = auth.uid()
)
);
-- Politika pre zápis nových faktúr
CREATE POLICY insert_tenant_invoices ON invoices
FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = (
SELECT tenant_id
FROM users
WHERE id = auth.uid()
)
);
4. Výkonnostná optimalizácia: Pasca menom "RLS Join Trap" a jej riešenie
Vyššie uvedená politika funguje spoľahlivo a bezpečne, no skrýva v sebe obrovské výkonnostné úskalie. Pri každom dopyte na tabuľku invoices musí PostgreSQL vykonať poddopyt (subquery) SELECT tenant_id FROM users WHERE id = auth.uid().
Ak vaša aplikácia načíta zoznam 100 faktúr alebo vykonáva zložité agregácie, databáza môže spúšťať tento poddopyt opakovane pre každý posudzovaný riadok. Výsledkom sú zbytočné JOIN operácie a výrazné spomalenie dopytov pri raste databázy na státisíce riadkov.
Riešenie: Využitie vlastných JWT Claims v Supabase
Supabase nám umožňuje pridať vlastné metadáta priamo do JWT tokenu používateľa, keď sa prihlasuje. Namiesto dopytovania tabuľky users v SQL môžeme uložiť tenant_id priamo do tokenu a v RLS politike ho prečítať bez dotyku s inou tabuľkou.
Keď používateľ úspešne prejde autentifikáciou, priradíme mu tenant_id do app_metadata v JWT.
Následne prepíšeme našu RLS politiku na časovú zložitosť O(1):
-- Rýchla RLS politika čítajúca tenant_id priamo z JWT claims
CREATE POLICY optimized_select_invoices ON invoices
FOR SELECT
TO authenticated
USING (
tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid
);
Tento prístup odstraňuje potrebu pomocných databázových dopytov v RLS politike a robí z PostgreSQL RLS použiteľné riešenie aj pre výkonné enterprise systémy s vysokým objemom transakcií.
Indexovanie zahraničných kľúčov
Rovnako dôležité je zabezpečiť, aby bol na stĺpci tenant_id v každej tabuľke vytvorený B-Tree index. Bez indexu bude musieť databáza pri každom RLS filtre prehľadávať celú tabuľku sekvenčne (Full Table Scan).
-- Nevyhnutný index pre rýchle filtrovanie cez RLS
CREATE INDEX IF NOT EXISTS invoices_tenant_id_idx ON invoices(tenant_id);
5. Produkčné riziká a ich ošetrenie: service_role a testovanie únikov
Nasadenie RLS v ostrej prevádzke vyžaduje vyriešenie dvoch kritických výziev: spracovanie systémových procesov na pozadí a eliminácia rizika, že niekto zabudne zapnúť RLS na novej tabuľke.
5.1. Bezpečné obchádzanie RLS pre systémové úlohy (service_role)
Niekedy potrebujete spustiť skript, ktorý zrátava faktúry všetkých firiem na vygenerovanie celkových štatistík, alebo nočnú cron úlohu, ktorá agreguje mesačné predplatné. Tieto procesy nemajú „prihláseného používateľa“ s tenant_id v JWT.
Supabase na tento účel poskytuje špeciálny kľúč service_role. Tento kľúč má administrátorské oprávnenia a obchádza RLS politiky.
Striktné inžinierske pravidlo: Kľúč service_role sa nesmie nikdy dostať do klientskeho prehliadača. Musí byť uložený výhradne v bezpečnom serverovom prostredí (napr. Next.js Server Actions, Node.js mikroslužba) chránený premennými prostredia.
Ukážka bezpečného vytvorenia Supabase klienta s právom obchádzať RLS pre interný backendový skript v TypeScript:
// src/lib/supabase-admin.ts
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; // Iba na serveri!
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error("Chýbajú Supabase konfiguračné premenné prostredia.");
}
/**
* Inicializuje administrátorského klienta, ktorý obchádza PostgreSQL RLS.
* Používajte výhradne pre backendové cron úlohy, migrácie a interné analýzy.
*/
export const getSupabaseAdmin = () => {
return createClient(supabaseUrl, supabaseServiceKey, {
auth: {
persistSession: false, // Na backendových skriptoch neukladáme session do pamäte
autoRefreshToken: false,
},
});
};
5.2. Ochrana pred "tichými únikmi" (Silent Leaks) cez automatizované testy
Najväčším rizikom pri raste aplikácie je ľudská nepozornosť. Vývojár vytvorí novú tabuľku contracts (zmluvy), pridá do nej stĺpec tenant_id, ale zabudne spustiť ALTER TABLE contracts ENABLE ROW LEVEL SECURITY;. V PostgreSQL môže byť nová tabuľka pri zlom nastavení rolí a oprávnení dostupná širšie, než chcete. Vzniká vážne bezpečnostné riziko.
Ako to eliminujeme? Automatizovaným testom na úrovni CI/CD pipeline.
Do našej testovacej suity v TypeScript (napr. Vitest / Jest) pridáme test, ktorý sa pomocou administrátorského klienta opýta systémového katalógu PostgreSQL na zoznam všetkých tabuliek, ktoré nemajú zapnuté RLS. Ak takú tabuľku nájde, test zlyhá a nepovolí nasadenie kódu do produkcie.
// src/__tests__/db-security.test.ts
import { getSupabaseAdmin } from "../lib/supabase-admin";
describe("Database RLS Enforcement Audit", () => {
it("všetky verejné tabuľky musia mať striktne aktivované Row Level Security", async () => {
const supabase = getSupabaseAdmin();
// Dopyt na systémový katalóg pg_tables vyhľadávajúci nezabezpečené tabuľky
const { data, error } = await supabase.rpc("audit_rls_status") as {
data: { tablename: string; rls_enabled: boolean }[] | null;
error: any
};
// Ak RPC funkcia neexistuje, môžeme spustiť priamy SQL dopyt
const fallbackQuery = `
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND rowsecurity = false
AND tablename NOT IN ('schema_migrations', 'spatial_ref_sys');
`;
const { data: unsafeTables, error: queryError } = await supabase
.from("_raw_sql" as any) // Vlastný endpoint pre admin SQL dopyty
.select("*")
.csv(); // Prípadne spustiť cez priamy pg node ovládač
// Alternatívne riešenie cez priamy dopyt do postgres klienta:
// const res = await pgClient.query(fallbackQuery);
// expect(res.rows.length).toBe(0);
// Pre demonštráciu testu overujeme, že nemáme žiadne nezabezpečené tabuľky
const unprotectedTables: string[] = []; // tu spracujeme výsledok SQL dopytu
expect(unprotectedTables).toEqual([]);
});
});
6. Prečo byť opatrný pri no-code "lepení" a zvoliť čistý vývoj na mieru?
V súčasnosti zažíva trh veľkú vlnu popularity vizuálnych no-code platforiem (ako Make.com, Zapier, Retool), ktoré sľubujú rýchle vybudovanie B2B systémov.
Pri budovaní multi-tenant SaaS aplikácií však tieto nástroje často prinášajú bezpečnostné riziká. No-code platformy sa zvyknú pripájať k databáze cez široké oprávnenia a spracúvajú dáta v pamäti externých cloudových služieb. Stačí zlé nastavenie webhooku, únik prístupového kľúča alebo chyba v externom JavaScripte a dáta vašich B2B klientov môžu byť vystavené nesprávnym ľuďom.
Bezpečnosť multi-tenancy musí byť postavená na troch pilieroch:
- Engine level: Vynucovaná priamo v databáze (PostgreSQL RLS).
- Type safety: Striktne typovaná v TypeScripte a validovaná za behu cez Zod schémy.
- Code ownership: Bežiaca na infraštruktúre, ktorú máte pod kontrolou alebo zmluvne zabezpečenú, bez zbytočných prostredníkov a krehkých integrácií tretích strán.
Záver: Investujte do bezpečnosti skôr, než bude neskoro
PostgreSQL Row Level Security v kombinácii so Supabase patrí medzi najsilnejšie praktické prístupy pre návrh multi-tenant B2B SaaS aplikácií. Presunom bezpečnostnej logiky z aplikačného kódu priamo do databázového jadra výrazne znižujete riziko únikov dát spôsobených ľudským faktorom.
Návrh robustnej, bezpečnej a vysoko výkonnej multi-tenant architektúry však vyžaduje skúsených inžinierov, ktorí rozumejú správnemu indexovaniu, riadeniu JWT tokenov, optimalizácii dopytov a nasadzovaniu CI/CD kontrolných mechanizmov.
Sme technologické štúdio nolimeo, špecializované butikové vývojárske jadro. Našim klientom nepredávame predražené licencie ani krehké "klikacie" integrácie. Aplikácie staviame na typovo čistom kóde, premyslených oprávneniach a dobre navrhnutých PostgreSQL databázach.
Napíšte nám a prejdeme si multi-tenant model, databázové politiky aj bezpečný technický návrh pre váš B2B SaaS produkt.
