Arhitectura multi-tenant SaaS: pool, bridge sau silo
Pool, bridge sau silo? Ghid de decizie pentru tech leads: izolare, cost, RLS corect în Postgres și pragul de la care migrarea devine rearhitecturare.

Ai trecut de întrebarea „ce este un SaaS" și ești la prima decizie serioasă de arhitectură multi-tenant care vine imediat după: cum izolezi datele clienților unul de altul. Pare un detaliu de infrastructură. În realitate, modelul de tenancy pe care îl alegi acum dictează cât te costă fiecare client nou, cât de greu respecți o cerință de conformitate și cât durează să scalezi peste primul prag serios. Schimbat târziu, devine o rearhitecturare de luni, nu un refactor de o după-amiază.
Există trei modele clasice — pool, bridge și silo — și niciunul nu „câștigă". Fiecare e un compromis între izolare, cost și complexitate operațională. Ghidul ăsta îți dă criteriile concrete ca să alegi înainte să scrii cod. Dacă încă vrei imaginea de ansamblu a modelului, pornește de la ce înseamnă un SaaS.
Cele trei modele multi-tenant: pool, bridge, silo
Vocabularul vine din framework-ul AWS Well-Architected (SaaS Lens), care numește trei tipare de izolare: pool, silo și bridge. Tradus pe baza de date, arată așa:
- Pool — shared database. Toți tenanții stau în aceleași tabele, separați printr-o coloană
tenant_idși, ideal, Row-Level Security (RLS). Resurse partajate la maximum. - Bridge — schema-per-tenant. O singură instanță, dar fiecare tenant primește propria schemă (
tenant_acme.facturi,tenant_beta.facturi). Izolare logică, fără date amestecate în același tabel. - Silo — database-per-tenant. Fiecare client are propria bază de date (uneori propriul stack). Izolare fizică, maximă.
AWS descrie compromisul direct: pool-ul îți dă eficiență și agilitate, dar introduce riscul de „noisy neighbor" și face mai grea atribuirea costului pe tenant; silo-ul elimină vecinul zgomotos și ușurează cost tracking-ul și blast radius-ul, dar renunță la economiile de scară. Bridge-ul e hibridul dintre ele — partajezi unele resurse, izolezi altele.
Pe ce axe se decide, de fapt
Nu alegi un model „în general". Îl alegi pe câteva axe concrete:
- Izolare. Pool îți dă o graniță la nivel de rând prin RLS; bridge, separare logică pe schemă; silo, separare fizică. Atenție la o capcană de intuiție: schema-per-tenant nu îți oferă, de fapt, o izolare de securitate mai bună decât un RLS bine făcut. Senzația de separare e mai mare, dar garanția vine din enforcement, nu din locul fizic al datelor.
- Cost. Pool e cel mai ieftin (o instanță, resurse partajate). Silo e cel mai scump (un baseline de cost per client). Bridge stă la mijloc.
- Complexitate operațională. La migrări: în pool rulezi o singură dată pe un set de tabele; în bridge le rulezi pe fiecare schemă (fan-out); în silo, pe fiecare bază, cu orchestrator, valuri și risc de rollback parțial.
- Onboarding. Pool = un
INSERT, fără migrare. Bridge și silo = creezi schema sau baza și rulezi migrarea de setup la fiecare client nou. - Conformitate. Dacă un client cere date într-o anumită regiune sau într-o bază complet separată — frecvent în sectoare reglementate — silo-ul devine cel mai scurt drum spre „da".
Pus cap la cap, decizia arată cam așa:
Pool: shared database cu RLS (punctul de pornire pentru majoritatea)
Pentru cele mai multe SaaS-uri B2B noi, pool-ul e răspunsul corect de start. Shared schema cu RLS scalează la sute de mii de tenanți pe un singur cluster, iar migrările ating un singur set de tabele. Pierzi puțin la izolarea „pe hârtie", dar câștigi enorm la simplitate.
Cheia e să nu lași izolarea pe seama unui WHERE tenant_id = ? scris de mână în fiecare query. E suficient un endpoint în care ai uitat clauza și ai expus datele unui client altui client. RLS mută filtrul în motorul bazei de date: Postgres rescrie automat fiecare interogare, deci nu mai depinzi de disciplina perfectă a fiecărui dezvoltator în fiecare rută.
Cum arată RLS corect în Postgres
Tiparul sigur are câteva piese care trebuie toate la locul lor:
- Activezi RLS pe tabel și o forțezi și pentru owner:
ALTER TABLE facturi ENABLE ROW LEVEL SECURITY;plusFORCE ROW LEVEL SECURITY. FărăFORCE, owner-ul tabelului și superuserii ocolesc policy-ul — și exact ca ei îți rulezi de obicei testele, așa că totul pare în regulă când, de fapt, policy-ul nici nu se aplică. - Rolul aplicației nu are
BYPASSRLS. - Definești policy pentru ambele direcții:
USING(ce rânduri vezi) șiWITH CHECK(ce ai voie să scrii). Dacă uițiWITH CHECK, un client poate insera un rând cutenant_id-ul altcuiva. - Pui un index compus cu
tenant_idpe prima poziție. Cu el, overhead-ul RLS rămâne mic — în jur de 2–4% pe interogări indexate, potrivit benchmark-urilor publice. - Testezi izolarea cu un rol non-superuser, în CI, nu doar în dev. Policy-urile eșuează „închis": un rezultat greșit arată exact ca un rezultat gol corect, fără eroare.
Fluxul unei cereri, de la HTTP la rândurile filtrate, arată așa:
Capcana de connection pooling (și CVE-2024-10976)
Aici cad cele mai multe echipe. RLS citește contextul tenantului dintr-o variabilă de sesiune, de obicei prin current_setting('app.tenant_id'). Problema: în spatele aproape oricărui SaaS stă un pooler ca PgBouncer sau Supavisor, iar în transaction mode conexiunile sunt reutilizate între cereri diferite. Dacă setezi app.tenant_id la nivel de sesiune, următoarea cerere pe aceeași conexiune moștenește contextul tenantului anterior — adică o scurgere de date care apare exact în producție, când mai mulți clienți lovesc aplicația simultan.
Soluția e o singură literă de disciplină: setezi contextul transaction-scoped, cu SET LOCAL într-o tranzacție (sau set_config(..., true)). La COMMIT, contextul se resetează singur. Un singur SET de sesiune rătăcit e o breșă latentă — merită prins la code review și acoperit cu un test automat de izolare cross-tenant.
Și un detaliu de versiune care ține tot de aici: dacă folosești policy-uri legate de rol (SET ROLE per tenant) plus reutilizare de planuri de query, te privește CVE-2024-10976. Potrivit avizului oficial PostgreSQL, urmărirea incompletă a tabelelor cu RLS putea aplica policy-ul greșit când o interogare era planificată sub un rol și executată sub altul — prin subquery, CTE, security invoker view sau funcție SQL. E reparat în versiunile 17.1, 16.5, 15.9, 14.14, 13.17 și 12.21, deci, practic, stai pe o versiune minoră la zi.
Bridge: schema-per-tenant, și unde se oprește
Bridge-ul are sens când chiar ai nevoie de customizări de schemă per client sau ai puțini clienți mari care vor separare logică vizibilă. Câștigi izolare logică puternică — aceleași queries, doar cu alt search_path — și posibilitatea de a indexa sau migra diferit pe tenant.
Dar are un plafon practic, și e bine să-l știi înainte. Fiecare tabel, index, constrângere și secvență din toate schemele trăiește în cataloagele de sistem partajate. La sute de scheme, fiecare cu tabelele și indecșii ei, cataloagele cresc spre milioane de rânduri; planner-ul, care consultă catalogul la fiecare query, încetinește, și la fel migrările. În practică, ghidurile de specialitate pun pragul de degradare în jur de 1.000–2.000 de scheme, iar PlanetScale e și mai conservator, recomandând să nu treci de câteva sute. Heroku a ajuns să avertizeze explicit împotriva acestui model în documentația lor. Peste prag, fie treci pe pool, fie pe sharding — Citus a adăugat în versiunea 12 schema-based sharding cu integrare PgBouncer, viabil până la ordinul a câteva mii de tenanți.
Operațional, mai e o capcană: dacă deschizi câte un pool de conexiuni per schemă, rămâi rapid fără conexiuni. Și aici, SET LOCAL search_path în tranzacție e regula — altfel search_path-ul curge între cereri exact ca variabila de tenant.
Silo: database-per-tenant, pentru izolare și conformitate
Silo-ul e răspunsul când izolarea nu e negociabilă: cerințe legale, rezidența datelor pe regiuni sau un client enterprise care pur și simplu cere baza lui separată. Îți dă izolarea maximă, blast radius minim, cost tracking trivial per tenant și chei de criptare diferite per client.
Prețul e operațional. Pe un RDS clasic, database-per-tenant la scară devine nesustenabil: fiecare instanță are un cost de baseline chiar și inactivă, stocarea nu scade înapoi, iar provisioning-ul, patching-ul și backup-urile pentru sute de baze cer practic o echipă dedicată.
Aici s-a schimbat ceva real în ultimii ani. Platformele de Postgres serverless au făcut silo-ul accesibil. Neon, de pildă, separă compute de storage și scalează compute la zero când baza e inactivă, așa că o bază per tenant inactivă costă aproape nimic, iar provisioning-ul e programatic — un singur inginer poate administra mii de tenanți prin API. După achiziția de către Databricks în 2025, Neon a redus prețul de stocare și tarifele de compute, ceea ce schimbă direct economia modelului. Dacă mergi pe silo, asta e diferența între „prea scump" și „fezabil".
Pipeline-urile care alimentează datele per tenant — ETL și web scraping — și joburile de fundal care rulează per tenant se mapează curat peste un model silo, fiindcă fiecare rulează izolat pe baza clientului.
Build vs buy pe izolare
Întrebarea „construiesc izolarea sau o cumpăr" se reduce la cât control vrei față de cât operațional vrei să eviți.
- Build (RLS manual, shared DB). Control total, cost minim, dar îți asumi toate footgun-urile RLS:
FORCE,WITH CHECK, indexare, testare ca rol non-superuser, disciplinaSET LOCAL. Pe Postgres simplu (RDS, Cloud SQL, orice managed), e perfect fezabil — doar nu „gratis" ca efort. - Buy (platforme cu tenancy în prim-plan). Supabase îți dă RLS ca barieră de bază și auto-generează un API REST peste bază. E comod, dar înseamnă că RLS devine singura barieră, deci trebuie să fie corect pe fiecare tabel. (Într-un Next.js custom, unde singurul client al bazei e aplicația ta, RLS rămâne defense-in-depth, nu unica linie.) Neon împinge spre database-per-tenant ieftin prin branching și scale-to-zero. Pentru pool la scară foarte mare, Citus transformă Postgres într-o bază sharded.
Nu există un câștigător universal nici aici. Regula practică: cumperi când platforma îți rezolvă exact modelul pe care l-ai ales (silo ieftin → Neon; RLS cu tooling → Supabase) și construiești când vrei controlul și ai disciplina de testare ca să nu te muște footgun-urile.
Pornește pool, „promovează" enterprise la silo
Cel mai bun default practic nu e să alegi un singur model pe vecie, ci o secvență. Pornești pool cu RLS — ieftin, simplu, scalează. Când un client enterprise cere izolare fizică sau rezidență, îl „promovezi" individual la silo, păstrând restul flotei în pool. Asta e, de fapt, un model bridge la nivel de produs.
Cheia e să poți muta un tenant între nivele fără downtime mare. Postgres îți dă primitivele: logical replication cu row filters (din Postgres 15+), foreign data wrappers și DDL online. Tiparul de migrare arată așa: creezi baza nouă a tenantului, publici cu WHERE tenant_id = ..., faci snapshot-ul inițial, prinzi din urmă, apoi pauzezi scurt scrierile clientului, comuți traficul și ștergi rândurile lui din tabelele partajate.
De ce contează să pui asta la punct devreme? Pentru că, dacă amâni, schimbarea modelului pe un sistem cu trafic real nu mai e un refactor — devine o rearhitecturare care îți blochează feature-urile luni la rând. Echipele care au pornit cu schema-per-tenant fără plan de ieșire descriu exact asta: migrări care durează și care, dacă pică la jumătate, te lasă cu baze în stări diferite și fără un punct clar al rupturii. Decizia pe care o iei azi e mult mai ieftin de luat azi.
Vezi și restul articolelor despre arhitectură și multi-tenancy, sau cum arată tooling-ul intern și dashboards-urile B2B când un client cere panoul lui dedicat.
Întrebări frecvente
Pot porni cu pool și să trec la silo mai târziu?
Da, și e chiar strategia recomandată. Pornești pool cu RLS, apoi promovezi individual clienții care cer izolare fizică, folosind logical replication cu row filters (Postgres 15+) ca să muți un tenant cu downtime minim. Important e să construiești capacitatea de migrare devreme, nu după ce ai deja mii de tenanți.
Schema-per-tenant îmi dă o securitate mai bună decât RLS?
Nu neapărat. Senzația de separare e mai mare, dar garanția de securitate vine din enforcement, nu din locul fizic al datelor. Un RLS corect — cu FORCE, WITH CHECK, rol fără BYPASSRLS și teste de izolare — oferă o garanție comparabilă, fără costul operațional al sutelor de scheme.
Câți tenanți „duce" schema-per-tenant?
Practic, de la câteva sute până în jur de 1.000–2.000 de scheme, după care cataloagele de sistem se umflă și planner-ul plus migrările încetinesc. Peste atât, treci pe pool cu RLS sau pe sharding.
Ce e CVE-2024-10976 și mă afectează?
E o vulnerabilitate Postgres în care RLS putea aplica policy-ul greșit la reutilizarea planurilor de query cu schimbări de rol. Te privește dacă folosești policy-uri legate de rol cu SET ROLE per tenant. E reparată în versiunile minore 17.1, 16.5, 15.9, 14.14, 13.17 și 12.21 — soluția e să stai la zi.
RLS încetinește baza de date?
Marginal, dacă indexezi corect. Pe coloane indexate, overhead-ul e de ordinul a câteva procente (în jur de 2–4% în benchmark-uri publice). Costul real apare din policy-uri complexe, join-uri adânci sau funcții care nu sunt LEAKPROOF și blochează folosirea indexului.
Trebuie să folosesc neapărat o platformă ca Neon sau Supabase?
Nu. Postgres simplu pe orice managed service (RDS, Cloud SQL) acoperă toate cele trei modele. Platformele ajută la cazuri specifice: Neon face database-per-tenant ieftin prin scale-to-zero, Supabase îți dă RLS cu tooling. Alegi platforma după modelul pe care l-ai ales, nu invers.
Ce urmează
Decizia de tenancy e una dintre puținele pe care e scump să le schimbi târziu. Dacă vrei imaginea completă a stack-ului, pornește de la ghidul despre ce este un SaaS, iar pentru cum se construiește efectiv un SaaS, vezi serviciile de platforme SaaS.
În articolele care urmează din această serie ducem decizia mai departe: de la idee la un MVP real, conformitatea fiscală pentru SaaS în România (TVA și e-Factura) și cât costă, de fapt, un SaaS.
Andrei Badulescu
Fondator & Software ArchitectConstruiește sisteme B2B la BaseTech — ERP la comandă, platforme SaaS, agenți AI și arhitecturi programmatic SEO. Scrie despre deciziile tehnice din spatele lor: stack, trade-off-uri și ce ține la scară.
Vezi profilul autorului →Articole conexe

Modele de pricing SaaS: flat, tiered, per-seat, usage
Flat, tiered, per-seat sau usage-based? Cum alegi modelul de pricing și value metric potrivit pentru SaaS-ul tău B2B — fără să penalizezi adopția.

Stripe vs merchant of record pentru un SaaS: ghid 2026
Stripe (PSP) sau un merchant of record — Paddle, Lemon Squeezy, Polar — pentru SaaS-ul tău? Fee-uri 2026, cine duce TVA-ul, control și lock-in, pe axe.

Billing SaaS cu Stripe: abonamente, webhooks, facturare
Cum implementezi billing recurent într-un SaaS cu Stripe: abonamente, webhooks ca sursă de adevăr, proration, invoicing și recuperarea plăților eșuate.
Insights pentru companii
care construiesc
Articole noi despre ERP, AI, agenți și pSEO, direct pe email. Fără spam.