Jak zabezpieczyć dane wielu firm w jednej aplikacji z PostgreSQL?
Krótka odpowiedź: kontrola dostępu do pojedynczych wierszy w PostgreSQL ma sens w aplikacji abonamentowej, w której wielu klientów współdzieli tabele, ale każdy wiersz musi być filtrowany przez kontekst firmy ustawiany przez aplikację. Nie zastępuje autoryzacji w NestJS ani testów, ale dodaje barierę w silniku bazy, która chroni przed zapomnianym `WHERE tenant_id = ...`, błędnym zapytaniem z warstwy pracy z bazą i częścią pomyłek w narzędziach administracyjnych.
Najpierw problem: jeden system dla wielu firm obniża koszt, ale koncentruje ryzyko
Aplikacja abonamentowa obsługująca wielu klientów jest kusząca, bo jedna aplikacja i jedna infrastruktura pracują dla wielu firm. Zamiast utrzymywać osobną bazę dla każdej organizacji, trzymasz dane w tych samych tabelach, zwykle z kolumną identyfikującą firmę. Koszt infrastruktury spada, migracje są prostsze, a wdrażanie funkcji jest szybsze.
Takie podejście ma jednak jedno brutalne ryzyko: wyciek danych między klientami. Jeśli klient A zobaczy faktury, użytkowników, notatki z obsługi klienta albo dokumenty klienta B, problem nie jest "bugiem". To incydent bezpieczeństwa, utrata zaufania, potencjalne konsekwencje prawne i rozmowa z największym klientem, której nikt nie chce prowadzić.
Dlatego w systemach dla klientów firmowych nie wystarczy mówić "nasza aplikacja zawsze dodaje filtr firmy". Wystarczy jeden punkt dostępu, raport, eksport CSV, zadanie w tle, widok administratora albo surowe zapytanie bez filtra. Kontrola dostępu do pojedynczych wierszy w PostgreSQL nie usuwa całego ryzyka, ale przenosi część izolacji z pamięci programisty do silnika bazy.
Wspólne tabele, osobna baza czy podejście mieszane: trzy sposoby izolacji klientów
Zanim wybierzesz polityki dostępu w bazie, musisz zdecydować, jak oddzielasz klientów. Osobna baza albo osobny schemat dla klienta daje najmocniejszą izolację i łatwiejsze odtwarzanie danych pojedynczej firmy, ale kosztuje więcej w operacjach, migracjach i automatyzacji. Wspólne tabele z separacją przez identyfikator firmy są najtańsze w skali, ale wymagają bardzo dobrych zabezpieczeń procesu.
Podejście mieszane jest kompromisem: najwięksi lub regulowani klienci dostają osobną bazę albo schemat, a mniejsze firmy działają we wspólnych tabelach. To bywa rozsądne w aplikacji dla klientów firmowych, gdzie duży klient płaci za silniejszą izolację, a mniejsi klienci potrzebują ekonomii skali.
Kontrola dostępu do wierszy dotyczy głównie wspólnych tabel. AWS Prescriptive Guidance mówi wprost, że taka ochrona jest wymagana do utrzymania izolacji klientów przy wspólnym PostgreSQL i centralizuje egzekwowanie izolacji na poziomie bazy. W praktyce oznacza to jasną mapę: aplikacja ustawia kontekst firmy, PostgreSQL egzekwuje polityki na tabelach klientów, a pamięć podręczna, wyszukiwarka i funkcje AI dostają własne zasady izolacji. To nie znaczy, że każda tabela ma wyglądać tak samo. Oznacza, że każda tabela z danymi klientów musi mieć świadomą politykę dostępu.
- Osobna baza: najlepsza izolacja, wyższy koszt operacyjny i migracje dla każdego klienta.
- Wspólne tabele: najlepsza ekonomia skali, najwyższy wymóg dyscypliny w izolacji danych.
- Podejście mieszane: wspólne tabele dla większości, osobne środowisko dla dużych lub regulowanych klientów.
Co naprawdę robi kontrola dostępu do wierszy w PostgreSQL
Oficjalna dokumentacja PostgreSQL opisuje kontrolę dostępu do wierszy jako polityki ograniczające, które wiersze mogą być zwracane, wstawiane, aktualizowane albo usuwane. Po włączeniu tej ochrony normalny dostęp do tabeli musi być dopuszczony przez politykę. Jeśli nie ma polityki, baza domyślnie blokuje dostęp: wiersze nie są widoczne ani modyfikowalne.
Dla aplikacji obsługującej wiele firm oznacza to prostą ideę: tabela `invoices` może mieć politykę porównującą identyfikator firmy w wierszu z kontekstem aktualnej sesji. Aplikacja po uwierzytelnieniu ustawia kontekst sesji, a baza automatycznie dodaje filtr do zapytań. Nawet jeśli kod zapomni `WHERE tenant_id = ...`, normalny użytkownik nie powinien zobaczyć cudzych wierszy.
To brzmi jak magia, ale nią nie jest. Taka polityka działa dla konkretnej tabeli, roli i komendy. Właściciel tabeli oraz role z uprawnieniem omijania tych polityk mogą je ominąć. Dlatego projekt produkcyjny musi obejmować role aplikacyjne, wymuszenie polityk także dla właściciela tabeli, testy migracji oraz jasny podział między połączeniami aplikacji, administratora i utrzymania.
Wzorzec dla NestJS: kontekst firmy, transakcja i reset
Najczęstszy błąd nie leży w samej polityce bazy, tylko w ustawianiu kontekstu. Zaplecze po autoryzacji musi wiedzieć, która firma wykonuje żądanie, a potem ustawić ją w sesji PostgreSQL. W praktyce używamy kontekstu żądania w NestJS, strażników autoryzacji i warstwy klienta bazy, która nie pozwala wykonać zapytania bez ustawionego kontekstu tam, gdzie jest wymagany.
Przy puli połączeń trzeba uważać: połączenie jest używane ponownie. Dlatego kontekst firmy powinien być ustawiany na czas transakcji lub żądania i resetowany. W PostgreSQL często stosuje się `set_config('app.current_tenant', tenantId, true)` w transakcji, gdzie trzeci argument `true` ogranicza ustawienie do aktualnej transakcji.
Jeżeli używasz Prisma, Drizzle, Kysely albo surowego `pg`, zasada jest ta sama: biblioteka do zapytań nie jest granicą bezpieczeństwa. Pomaga budować zapytania, ale izolacja w PostgreSQL wymaga świadomego cyklu życia połączenia, testów i zabezpieczeń blokujących przypadkowe użycie superużytkownika albo danych dostępowych z prawem omijania polityk.
Polityki, których potrzebuje prawdziwy produkt abonamentowy
Najprostsza polityka porównująca identyfikator firmy jest tylko początkiem. Produkcyjny produkt abonamentowy potrzebuje osobnych zasad dla odczytu, tworzenia, zmiany i usuwania danych, bo widoczność wiersza i prawo do jego utworzenia lub zmiany to nie to samo. PostgreSQL pozwala używać `USING` dla dostępu i `WITH CHECK` dla danych zapisywanych.
Przykład: użytkownik może widzieć faktury swojej firmy, ale nie może zmienić identyfikatora firmy na fakturze przez dane wysłane w formularzu lub żądaniu. `WITH CHECK` powinien wymusić, że nowy lub zaktualizowany wiersz nadal należy do aktualnego klienta. Dla tabel wspólnych, jak plany subskrypcji, przełączniki funkcji albo katalog publiczny, polityka może być inna albo izolacja na poziomie wierszy nie jest właściwą granicą.
Ważne są też role. Aplikacja produkcyjna nie powinna łączyć się jako właściciel tabel ani superużytkownik. Osobne role dla działania aplikacji, migracji, raportów tylko do odczytu i awaryjnego administratora zmniejszają ryzyko, że jedna biblioteka lub skrypt wyłączy całą izolację.
Testy izolacji: nie ufaj polityce, dopóki jej nie złamiesz
Izolacja w bazie musi mieć testy negatywne. Nie wystarczy sprawdzić, że klient A widzi swoje faktury. Trzeba sprawdzić, że klient A nie widzi faktur B, nie może wstawić wiersza należącego do B, nie może przepisać swojego wiersza na B i nie dostaje danych przez relację, widok, funkcję SQL, eksport, wyszukiwarkę albo zadanie w tle.
Dobre testy tworzą dwóch klientów, dwóch użytkowników, dane o podobnym kształcie i uruchamiają scenariusze przez aplikację oraz bezpośrednio na warstwie komunikacji z bazą. Warto testować też migracje: nowa tabela z danymi klientów nie może wejść na produkcję bez polityki dostępu i indeksu po identyfikatorze firmy.
Supabase przypomina, że kontrola dostępu do wierszy jest mocna, ale trzeba ją włączyć. PostgreSQL przypomina, że jeśli ochrona jest włączona, a brak polityki, baza domyślnie blokuje dostęp. To dobre zabezpieczenie, pod warunkiem że proces przeglądu wykrywa tabele bez polityk, zanim klienci odkryją brak danych albo cudze dane po wdrożeniu.
Wydajność: izolacja w bazie nie jest darmowa, ale chaos też kosztuje
Najczęstsze pytanie dyrektora technologii brzmi: czy polityki wierszy spowolnią bazę? Odpowiedź brzmi: mogą, jeśli są złożone, odwołują się do innych tabel bez indeksów albo wymagają funkcji wykonywanych dla każdego wiersza. Prosta polityka po `tenant_id` na dobrze indeksowanych tabelach jest zwykle dużo mniej problematyczna niż ręczne filtry rozsiane po całym kodzie.
Projektuj izolację od początku: `tenant_id` jako podstawowa kolumna w tabelach należących do klienta, indeksy z `tenant_id`, świadome ograniczenia unikalności, partycjonowanie dopiero po pomiarach, osobne ścieżki dla tabel globalnych i unikanie polityk, które robią ciężkie podzapytania dla każdego wiersza.
Warto też pamiętać, że izolacja w PostgreSQL nie rozwiązuje wszystkich problemów danych. Analityka, hurtownie, zdarzenia, indeks wyszukiwarki i pamięć podręczna muszą mieć własną izolację klientów. Jeżeli Elasticsearch albo Redis dostają klucz bez prefiksu klienta, ochrona w PostgreSQL nie uratuje danych poza bazą.
Kiedy izolacja w PostgreSQL nie wystarczy
Polityki wierszy są mocnym zabezpieczeniem procesu, ale nie są strategią bezpieczeństwa samą w sobie. Jeżeli klient wymaga osobnego regionu danych, osobnego tworzenia i odtwarzania kopii zapasowych, dedykowanego klucza szyfrowania, własnych okien utrzymaniowych albo certyfikacji, wspólne tabele z politykami wierszy mogą nie wystarczyć. Wtedy potrzebujesz osobnej bazy albo podejścia mieszanego.
Izolacja w bazie nie zastępuje też autoryzacji biznesowej. To, że użytkownik należy do organizacji klienta, nie znaczy, że może widzieć listę płac, eksportować wszystkie faktury albo usuwać projekt. Uprawnienia ról, plan subskrypcji, przełączniki funkcji i status konta nadal muszą być obsłużone w aplikacji i często odzwierciedlone w politykach.
Nie używaj izolacji w bazie jako pretekstu do pominięcia analizy zagrożeń. Trzeba przeanalizować administratorów, obsługę klienta, połączenia z innymi systemami, zdarzenia zewnętrzne, zadania w tle, kopie zapasowe, narzędzia analityczne, audyt, logi i narzędzia diagnostyczne. Wyciek może przyjść nie tylko z publicznego punktu dostępu aplikacji, ale też z "pomocnego" eksportu dla działu obsługi klienta.
Koszt wdrożenia: co naprawdę wpływa na budżet
Część serwerowa aplikacji dla wielu klientów z NestJS, izolacją w PostgreSQL, autoryzacją, podstawowym panelem i płatnościami zwykle zaczyna się od 160 000-300 000 PLN za sensowną pierwszą wersję. Zaawansowany produkt dla klientów firmowych z procesami, audytem, rozliczeniami, połączeniami z innymi systemami, importami danych i raportowaniem może iść wyżej. Sama polityka w bazie nie jest droga. Droga jest pewność, że nikt jej nie obejdzie.
Największe składniki kosztu to struktura danych, role i zasady uprawnień, migracje, testy izolacji, pula połączeń, połączenia z innymi systemami, audyt logów, narzędzia administracyjne i narzędzia obsługi klienta, import istniejących danych oraz monitorowanie działania. Jeżeli produkt ma funkcje AI lub wyszukiwanie w dokumentach, trzeba też rozwiązać izolację klientów w wektorach, wyszukiwaniu kontekstu i pamięci podręcznej.
GMI daje wstępną wycenę w 48h, ale stałą cenę dopiero po rozpoznaniu biznesu, projektu i technologii. W tym rozpoznaniu ustalamy sposób izolacji klientów, mapę danych, analizę zagrożeń, zakres pierwszej wersji, polityki dostępu w bazie, testy i ryzyka utrzymania. To uczciwsze niż obietnica "bezpiecznej aplikacji" na podstawie samego stosu technologicznego.
Lista kontrolna przed pracami programistycznymi
Ta lista powinna przejść przez dyrektora technologii, właściciela produktu i osobę odpowiedzialną za bezpieczeństwo przed pierwszym etapem prac nad częścią serwerową.
- Które tabele należą do klienta, które są globalne, a które tylko administracyjne?
- Czy każda tabela z danymi klienta ma identyfikator firmy, indeks i politykę dostępu w PostgreSQL?
- Jak aplikacja ustawia kontekst firmy i czy jest on resetowany przy puli połączeń?
- Które role mogą omijać polityki wierszy i czy aplikacja produkcyjna ich nie używa?
- Czy mamy testy negatywne dla SELECT, INSERT, UPDATE, DELETE, eksportów i zadań w tle?
- Jak izolujemy dane w pamięci podręcznej, wyszukiwarce, zdarzeniach, analityce, kopiach zapasowych i funkcjach AI?
- Czy duzi lub regulowani klienci wymagają osobnej bazy albo podejścia mieszanego zamiast wspólnych tabel?
Jak GMI projektuje taką część serwerową
Zaczynamy od rozpoznania biznesu, projektu i technologii: mapujemy klientów, role, dane, połączenia z innymi systemami, rozliczenia, operacje administracyjne i ryzyka bezpieczeństwa. Dopiero potem decydujemy, czy system ma używać wspólnych tabel z politykami wierszy, osobnych baz, podejścia mieszanego czy kilku wariantów naraz. To ważne, bo zła decyzja o izolacji potrafi wymusić drogą migrację po pierwszych dużych klientach.
Technicznie najczęściej używamy NestJS, PostgreSQL, Prisma lub innej typowanej biblioteki do zapytań, Redis tam, gdzie potrzebna jest pamięć podręczna albo kolejka, Next.js dla panelu i React Native, jeśli produkt ma część mobilną. Kontrola dostępu w PostgreSQL jest częścią większej architektury: autoryzacja, dziennik audytu, monitorowanie działania, testy kontraktów oraz bezpieczne narzędzia obsługi klienta.
Biznesowo klient dostaje własność kodu źródłowego, brak uzależnienia od dostawcy, stałą cenę po rozpoznaniu projektu i jasny sposób utrzymania. To szczególnie ważne w produkcie abonamentowym, bo bezpieczeństwo nie jest jednorazową funkcją. Każda nowa tabela, połączenie z systemem i funkcja AI musi dalej respektować granicę klienta.
Źródła i dalsza lektura
PostgreSQL Row Security Policies: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
AWS - rekomendacje izolacji wierszy dla PostgreSQL obsługującego wielu klientów: https://docs.aws.amazon.com/prescriptive-guidance/latest/saas-multitenant-managed-postgresql/rls.html
Supabase - przewodnik po kontroli dostępu do wierszy: https://supabase.com/docs/guides/database/postgres/row-level-security
PostgreSQL - konfiguracja sesji i ustawienia wykonywania: https://www.postgresql.org/docs/current/runtime-config-client.html
Prisma - surowy SQL i transakcje: https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries
GMI przewodnik o wydzielaniu zamówień w NestJS: /blog/nestjs-order-microservices-when-to-split
GMI przewodnik o NestJS, Prisma i PostgreSQL przy większej skali: /blog/nestjs-prisma-postgresql-at-scale
GMI tworzenie oprogramowania z AI jako częścią procesu: /services/ai-native-software-company
Najczęstsze pytania
- Czy kontrola dostępu do wierszy w PostgreSQL wystarczy do zabezpieczenia aplikacji dla wielu klientów?
- Nie wystarczy sama ochrona w bazie. Kontrola dostępu do wierszy w PostgreSQL jest bardzo mocną dodatkową obroną, ale nadal potrzebujesz autoryzacji w aplikacji, dobrych ról PostgreSQL, testów negatywnych, izolacji pamięci podręcznej, wyszukiwarki i zdarzeń oraz procesu przeglądu migracji. Chroni głównie tabele PostgreSQL, nie cały system.
- Kiedy wybrać wspólne tabele z politykami wierszy zamiast osobnej bazy dla klienta?
- Wspólne tabele z politykami wierszy mają sens, gdy wielu klientów korzysta z podobnego produktu, potrzebujesz niskiego kosztu infrastruktury, szybkich migracji i wspólnego wdrażania funkcji. Osobna baza jest lepsza przy silnych wymaganiach regulacyjnych, dedykowanych kopiach zapasowych i odtwarzaniu, osobnym regionie danych albo wymaganej izolacji dużego klienta.
- Jak aplikacja ustawia kontekst klienta dla polityk w PostgreSQL?
- Po uwierzytelnieniu aplikacja ustala firmę użytkownika i ustawia zmienną sesji PostgreSQL, np. `app.current_tenant`, zwykle na czas transakcji. Polityka porównuje identyfikator firmy w tabeli z tą zmienną. Przy puli połączeń trzeba pilnować resetu kontekstu, żeby następne żądanie nie odziedziczyło cudzej firmy.
- Czy Prisma działa z kontrolą dostępu do wierszy w PostgreSQL?
- Tak, ale Prisma nie jest granicą bezpieczeństwa. Polityki działają w PostgreSQL, więc zapytania Prisma też im podlegają, jeśli używasz właściwej roli i ustawiasz kontekst klienta na połączeniu lub transakcji. Wymaga to opakowań, testów i ostrożności przy surowym SQL oraz migracjach.
- Ile kosztuje część serwerowa aplikacji dla wielu klientów z izolacją w PostgreSQL?
- Sensowna pierwsza wersja z NestJS, izolacją w PostgreSQL, autoryzacją, panelem i płatnościami często zaczyna się od 160 000-300 000 PLN. Zaawansowany produkt dla klientów firmowych z procesami, audytem, połączeniami z innymi systemami i importami danych może kosztować więcej. Stałą cenę podajemy po rozpoznaniu projektu.
- Czy izolacja w PostgreSQL zabezpiecza dane w pamięci podręcznej, wyszukiwarce i funkcjach AI?
- Nie bezpośrednio. Polityki wierszy działają w PostgreSQL. Redis, Elasticsearch, zdarzenia, hurtownia danych, kopie zapasowe i dane używane przez funkcje AI muszą mieć własną izolację klientów: prefiksy kluczy, filtry, indeksy dla klienta albo osobne zasoby tam, gdzie wymaga tego ryzyko. Granica klienta musi istnieć w każdej warstwie, w której pojawiają się jego dane.
Treść zaktualizowano: 11 lipca 2026