DigiSpace
  • autor Yurii Mokryi

Laravel multi-tenant SaaS: routing domen i izolacja PostgreSQL w VetSpace

Laravel multi-tenant SaaS: routing domen i izolacja PostgreSQL w VetSpace

Platforma dla klinik musi odpowiedzieć na dwa pytania przed odczytaniem karty pacjenta: do której organizacji należy request i do jakich rekordów może mieć dostęp ten użytkownik? Nazwa domeny pomaga odpowiedzieć na pierwsze. Nie odpowiada na drugie i sama nie utrzymuje workera w tle w prawidłowym kontekście bazy danych.

Ten artykuł analizuje te granice w API Laravel VetSpace. To analiza kodu źródłowego identyfikacji tenanta, wyboru magazynu PostgreSQL, provisioningu i analizy w kolejce. Nie jest to twierdzenie, że niezależna ocena bezpieczeństwa wykazała pełną izolację, ani że projekt osiągnął zmierzony wynik komercyjny.

Użyteczny wynik to powtarzalna granica organizacji bez osobnej bazy kodu dla każdej kliniki. Rozwiązanie domeny wybiera kontekst kliniki, provisioning tworzy jej magazyn i początkowe rekordy, a jawne sprzątanie zapobiega pozostawieniu tego kontekstu aktywnego przez zadanie w kolejce. Współdzielone centralne rekordy nadal wymagają własnych kontroli autoryzacji.

Porównaj modele przechowywania przed wyborem

W projekcie ze wspólną tabelą wiersze niosą identyfikator organizacji. Istnieje jeden schemat do migracji, a raportowanie między organizacjami jest proste. Koszt to wszechobecny wymóg ograniczania: każde zapytanie, relacja, eksport i zadanie w tle musi wymuszać prawidłową granicę organizacji. Polityki bazy danych mogą dodać ochronę, ale wymagają starannego projektu uprawnień i kontekstu połączenia.

Projekt schema-per-tenant daje każdej organizacji własne tabele w jednej bazie PostgreSQL. Pozwala to uniknąć kolizji między identycznie nazwanymi tabelami i wspiera migracje specyficzne dla tenanta. Nie tworzy to osobnego serwera ani absolutnej bariery dostępu. Dokumentacja schematów PostgreSQL wprost wyjaśnia, że uprawnienia decydują o dostępie między schematami, a search_path wpływa na rozwiązywanie nazw.

Projekt database-per-tenant oddziela cele połączeń i może ułatwić definiowanie procedur backupu i odtwarzania dla poszczególnych klientów. Zwiększa też pracę operacyjną: migracje, monitoring, zarządzanie połączeniami i odtwarzanie muszą pokrywać wiele baz. Bazy na tym samym klastrze nadal współdzielą infrastrukturę. Żaden z tych wyborów sam w sobie nie stanowi gwarancji zgodności regulacyjnej.

Co VetSpace faktycznie implementuje

VetSpace używa stancl/tenancy i modelu Tenant implementującego TenantWithDatabase, z concerns pakietu HasDatabase i HasDomains. Pakiet obsługuje menedżery zarówno baz danych, jak i schematów PostgreSQL, jak opisano w jego dokumentacji multi-database. Własny menedżer aplikacji decyduje, którego użyć.

Istotny szczegół polega na tym, że decyzja nie jest po prostu „enterprise oznacza bazę, wszystko inne — schemat". Istniejący magazyn ma pierwszeństwo. To wyrażenie jest fragmentem UserRolePostgreSQLManager::resolveManager(); zależy od kontroli istnienia w tej klasie i importowanych klas menedżerów, więc nie jest samodzielnym skryptem provisioningu:

$managerClass = match (true) {
    $this->existsAsDatabase($name) => PostgreSQLDatabaseManager::class,
    $this->existsAsSchema($name) => PostgreSQLSchemaManager::class,
    $plan === 'enterprise' => PostgreSQLDatabaseManager::class,
    default => PostgreSQLSchemaManager::class,
};

Menedżer sprawdza katalog baz danych PostgreSQL i metadane schematów przez centralne połączenie. Dopiero gdy nie istnieje żadna forma magazynu, plan wybiera początkowy układ. To zachowuje starszych tenantów utworzonych z własną bazą, nawet jeśli ich obecny plan nie jest już enterprise. Zmiana planu nie migruje ich danych. Każde przeniesienie magazynu wymaga osobno zaprojektowanej procedury kopiowania, walidacji i przełączenia.

Oddziel centralne i kliniczne API

Repozytorium ma centralne route'y i route'y domen tenantów. Operacje centralne obejmują współdzielone rekordy właścicieli i zwierząt oraz rejestr tenantów. Operacje kliniczne obejmują filie, wizyty i terminy. To różne odpowiedzialności za dane w jednej aplikacji, a nie dowód niezależnie wdrożonych usług.

Na route'ach tenanta zapisana tablica middleware nie jest całą historią wykonania. TenancyServiceProvider dodaje middleware tenancy na początek listy priorytetów middleware Laravela. Inicjalizacja domeny wykonuje się więc przed uwierzytelnianiem nawet tam, gdzie auth:sanctum pojawia się wcześniej w deklaracji route'a. Wyszukiwanie tokena musi odbywać się przez zamierzone połączenie; przestawienie tablicy route'ów bez sprawdzenia priorytetu wykonania może dać mylącą poprawkę.

Użyteczna weryfikacja używa prawdziwego hostname'a tenanta, inicjalizuje oczekiwany magazyn i uwierzytelnia tokenem utworzonym w tym kontekście. Potem powtórz request wobec domeny innego tenanta i sprawdź odmowę. Sprawdzenie tylko istnienia domeny nie testuje granicy uwierzytelniania, a sprawdzenie tylko ścieżki pozytywnej pomija przypadkowy dostęp do rekordów innej organizacji.

Dwa oddzielne przepływy VetSpace: requesty domeny tenanta i analiza w kolejce wybierają kontekst tenanta przed odczytem danych kliniki
Kontekst requestu i kontekst workera to osobne punkty wejścia. Aplikacja musi ustalić właściwego tenanta dla każdego.

Provisioning to workflow, a nie jeden insert

Provider rejestruje pipeline TenantCreated: utwórz magazyn, zmigruj go, zasil danymi początkowymi, utwórz główną filię i zreplikuj właściciela. Kod ustawia shouldBeQueued(false), więc ten pipeline jest synchroniczny. Opisywanie go jako asynchronicznego onboardingu przeczyłoby implementacji.

Kolejność odzwierciedla prawdziwe zależności: tabele muszą istnieć przed ich danymi początkowymi, a początkowe rekordy kliniki — zanim klinika będzie używalna. Wprowadza to też przypadki częściowej awarii. Jeśli późniejszy krok zawiedzie, wiersz rejestru lub obiekt magazynu mogą już istnieć. Nie można zakładać, że transakcja w jednym połączeniu cofa każdą operację provisioningu między bazami.

Dla eksploatacji produkcyjnej wymagałbym udokumentowanego stanu gotowości i procedury odtwarzania, z testami awarii po każdym istotnym kroku. To rekomendacje, a nie twierdzenie, że sprawdzony pipeline implementuje kompletny system kompensacji. Praktyczna korzyść biznesowa wynika z wiedzy, czy klinika jest gotowa, a nie z traktowania częściowo ukończonej konfiguracji jako udanej rejestracji klienta.

Zadanie w kolejce musi ustalić własny kontekst

RunAiAnalysis niesie dwa identyfikatory: tenanta i analizy. Nie zakłada, że worker odziedziczył request przeglądarki. Handler wyszukuje tenanta w kontekście centralnym, wraca, gdy go nie ma, inicjalizuje tenancy, a potem wyszukuje analizę. Fragment podstawowego przetwarzania i sprzątania:

try {
    tenancy()->initialize($tenant);

    $analysis = AiAnalysis::find($this->analysisId);

    if (! $analysis) {
        return;
    }

    $service->analyze($analysis);
} finally {
    if (tenancy()->initialized) {
        tenancy()->end();
    }
}

To fragment ciała metody prawdziwego zadania, a nie kompletna klasa. Wcześniejsze wyszukiwanie tenanta i wstrzykiwanie zależności pozostają niezbędne. Blok finally wykonuje się, gdy przetwarzanie wraca lub rzuca wyjątek, zapobiegając pozostawieniu przez zadanie kontekstu tenanta aktywnego dla kolejnej pracy. To szczególnie ważne w ponownie używanym procesie workera.

Nie ustala to przetwarzania exactly-once, automatycznego odtwarzania providera ani właściwej polityki retry. Te zależą od konfiguracji kolejki i serwisu analizy. Brak tenanta lub analizy obecnie powoduje wczesny return; czy powinno to też zapisywać zdarzenie operacyjne, to osobna decyzja produktowa.

Centralne rekordy też potrzebują autoryzacji

Izolacja magazynu chroni tylko rekordy zapisane wewnątrz wybranego kontekstu tenanta. VetSpace ma też centralne rekordy właścicieli i zwierząt powiązane z klinikami. Trait ScopesToClinic ogranicza zapytania przez asocjacje clinic_owner i clinic_pet. Pusta lista klinik jest zmuszana do braku dopasowania, a nie zdejmowania ograniczenia.

To rozróżnienie ma znaczenie: centralny rekord może być przechowywany globalnie, lecz widoczny tylko przez autoryzowaną relację z kliniką. Review musi sprawdzić, które kontrolery używają mechanizmu ograniczania, jak wybierana jest uwierzytelniona klinika i jak autoryzowane są zapisy. Istnienie traita nie dowodzi, że każdy endpoint używa go poprawnie.

Co weryfikować i kiedy tego nie kopiować

Przed przyjęciem projektu przetestuj uwierzytelnianie domeny tenanta, odczyty i zapisy między klinikami, ponownie używanego workera obsługującego dwóch tenantów, częściową awarię provisioningu i odtwarzanie jednego tenanta z backupu. Ćwicz zarówno tenantów schema-backed, jak i database-backed. Test pokrywający tylko początkowy wybór planu ominie ważną gałąź istniejącego magazynu.

Efekt biznesowy wspierany przez kod jest strukturalny: jedna aplikacja może kierować pracę do odrębnych kontekstów klinik i ponownie używać zdefiniowanej sekwencji provisioningu. Nie przedstawiono tu zmierzonego zmniejszenia incydentów, godzin wsparcia ani czasu onboardingu. Takie twierdzenia wymagałyby rejestrów operacyjnych i zdefiniowanej linii bazowej.

Dla wewnętrznej aplikacji jednej organizacji ta machineria może być zbędna. Dla małego SaaS z prostą autoryzacją i raportowaniem współdzielone tabele mogą być łatwiejsze w eksploatacji. Dla silniejszej izolacji infrastrukturalnej różne bazy na jednym serwerze mogą wciąż być niewystarczające. Zacznij od wymagań własności, odtwarzania i dostępu, a potem wybierz najprostszy model, który je spełnia.

Po zakres implementacji, a nie szczegóły architektury, zobacz development w Laravel. Pierwsza użyteczna rozmowa dotyczy granicy danych i przypadków awarii, a nie liczby tenantów, którą diagram wydaje się obsługiwać.

Share this post