Dlaczego każdy tenant dostaje własną bazę danych
Schema-per-tenant, shared tables czy database-per-tenant? W Avenit wybraliśmy najbardziej rygorystyczne podejście. Oto dlaczego — i jakie są tego konsekwencje.
Multi-tenancy to pytanie, które w każdym SaaSie zadajesz sobie raz — i odpowiadasz na nie na kolejne pięć lat. W Avenit mamy jedną bazę danych PostgreSQL per tenant. Nie schema-per-tenant. Nie shared tables z kolumną tenant_id. Osobną, pełnoprawną bazę.
To nie jest popularny wybór. Zobaczmy dlaczego go zrobiliśmy.
Trzy opcje, które miałem do wyboru
1. Shared tables
Wszyscy użytkownicy wszystkich klientów w jednej tabeli users, rozdzieleni kolumną tenant_id. Standard w typowych SaaSach.
Plusy: prosto, tanio, łatwo zaczyna się. Minusy: jedno źle napisane zapytanie i wyciek danych między klientami. Indeksy rosną proporcjonalnie do sumy danych wszystkich klientów. Backup całości to backup wszystkiego.
2. Schema-per-tenant
Jedna baza, osobne schematy PostgreSQL per klient. Izolacja logiczna bez izolacji fizycznej.
Plusy: nadal jedno połączenie DB, tańsze niż osobne bazy. Izolacja lepsza niż shared tables.
Minusy: pg_catalog rośnie liniowo. Backup jednego klienta wymaga pg_dump --schema=.... Ograniczenia PostgreSQL na liczbę schematów (praktycznie ~1000).
3. Database-per-tenant
Osobna baza PostgreSQL dla każdego tenanta. Izolacja fizyczna i logiczna.
Plusy: pełna izolacja. Backup i restore per klient. Osobne migracje, osobne wersje. Klient enterprise może dostać własny serwer. Minusy: trudniej zarządzać połączeniami. Potrzebujesz puli po stronie aplikacji (my używamy PgBouncer + LRU cache po stronie NestJS).
Dlaczego wybraliśmy opcję 3
Polski rynek. Nasi potencjalni klienci enterprise pytają zanim podpiszą umowę: gdzie są moje dane, jak są izolowane, czy można je wyeksportować i zmienić region. Shared tables nie przejdzie audytu bezpieczeństwa w żadnym większym klientze B2B.
Drugi powód: migracje bez paniki. Gdy klient X ma produkcyjne zlecenia a klient Y testuje nową wersję, nie chcemy gromadzić się przy jednej migracji ALTER TABLE na 50 milionów wierszy. Z osobnymi bazami to tak, jakbyśmy wdrażali aktualizację na 50 osobnych środowisk — rolling, bezpiecznie, bez blokad.
Trzeci: no-code builder. Nasz generator DDL tworzy prawdziwe kolumny PostgreSQL, gdy klient dodaje pole do formularza. W modelu shared tables każde pole to albo JSONB (wolno, bez indeksów), albo kolumna pojawiająca się na wszystkich wierszach — u wszystkich klientów. Żaden kompromis nie pachnie dobrze.
Jaki jest koszt?
Dwa realne:
- Pooling połączeń — nie można po prostu otworzyć
pg.Poolna start aplikacji. Mamy LRU cacheMap<dbName, postgres.Sql>wTenantConnectionService, zamykający nieużywane połączenia po czasie. - Operacje cross-tenant — jeśli chcesz liczyć “ile wszystkich faktur wystawiono dziś na platformie”, musisz orkiestrować zapytania w aplikacji. Rzadko potrzebne, ale koszt jest.
Na dziś — 0 tenantów w produkcji, 3 w dev — nie czujemy bólu. Ale wiem gdzie on będzie, gdy przekroczymy 200 baz. Mamy przygotowany plan dzielenia tenantów między shardy PostgreSQL.
Jak się to ma do Odoo?
Odoo używa schema-per-tenant w wersji SaaS. To działa dla nich, bo mają ~lata dojrzałości, własne rozwiązania do backupów, własny fork PostgreSQL w Odoo.sh. My w 2026 nie mamy tego luksusu, ale mamy coś innego: czystą kartkę. Budujemy od razu tak, żeby klient enterprise mógł kupić u nas to, czego by nie dostał od konkurencji — własną bazę, na własnym serwerze, z własnym regionem.
Podsumowanie
Database-per-tenant nie jest dla każdego. Dla nas — polski B2B z ambicjami na enterprise — był jedynym sensownym wyborem. Zapłaciliśmy kosztem architektonicznej złożoności w warstwie połączeń. Zyskaliśmy argument sprzedażowy i spokojny sen operacyjny.
Jeśli budujesz SaaS i wahasz się między opcjami — zadaj sobie pytanie: “Czy mój największy przyszły klient podpisze u mnie, gdy zobaczy, że jego dane leżą w tej samej tabeli co dane jego konkurencji?”