28 lip 2025·7 min czytania

Schemat bazy danych organizacji i zespołów B2B, który pozostaje przejrzysty

Schemat bazy danych organizacji i zespołów B2B: praktyczny wzorzec relacyjny dla zaproszeń, stanów członkostwa, dziedziczenia ról i zmian przyjaznych audytowi.

Schemat bazy danych organizacji i zespołów B2B, który pozostaje przejrzysty

Jaki problem rozwiązuje ten wzorzec schematu

Większość aplikacji B2B to nie są typowe „konta użytkowników”. To współdzielone przestrzenie robocze, gdzie ludzie należą do organizacji, dzielą się na zespoły i mają różne uprawnienia w zależności od roli. Sprzedaż, wsparcie, księgowość i administratorzy potrzebują różnych dostępów, a te uprawnienia zmieniają się w czasie.

Zbyt prosty model szybko się psuje. Jeśli trzymasz jedną tabelę users z pojedynczą kolumną role, nie wyrazisz sytuacji „ta sama osoba jest Adminem w jednej organizacji, a Viewerem w innej”. Trudno też obsłużyć przypadki typu kontraktor widzący tylko jeden zespół albo pracownik, który opuszcza projekt, ale nadal należy do firmy.

Zaproszenia to kolejny częsty źródło błędów. Gdy zaproszenie to tylko wiersz z emailem, trudno określić, czy osoba już „należy” do organizacji, do którego zespołu powinna dołączyć i co się stanie, jeśli zarejestruje się pod innym emailem. Małe niespójności łatwo przeradzają się w problemy bezpieczeństwa.

Ten wzorzec dąży do czterech celów:

  • Bezpieczeństwo: uprawnienia wynikają z jawnego członkostwa, a nie z założeń.
  • Jasność: orgy, zespoły i role mają jedno źródło prawdy.
  • Spójność: zaproszenia i członkostwa podążają przewidywalnym cyklem życia.
  • Historia: możesz wyjaśnić, kto nadał dostęp, zmienił rolę lub usunął kogoś.

Obietnica to pojedynczy model relacyjny, który pozostaje zrozumiały w miarę rozrostu funkcji: wiele organizacji na użytkownika, wiele zespołów w organizacji, przewidywalne dziedziczenie ról i zmiany przyjazne audytowi. To struktura, którą możesz wdrożyć dziś i rozszerzać później bez przepisywania wszystkiego.

Kluczowe pojęcia: orgy, zespoły, użytkownicy i członkostwa

Jeśli chcesz schematu czytelnego po sześciu miesiącach, zacznij od uzgodnienia kilku terminów. Większość zamieszania pochodzi z mieszania „kim jest ktoś” z „co może zrobić”.

Organization (org) to najwyższa granica najemcy. Reprezentuje klienta lub konto firmowe, które posiada dane. Jeśli dwóch użytkowników jest w różnych orgach, domyślnie nie powinni widzieć swoich danych. To jedno proste założenie zapobiega wielu przypadkowym dostępom między najemcami.

Team to mniejsza grupa w organizacji. Modele zespołów odzwierciedlają rzeczywiste jednostki robocze: Sprzedaż, Wsparcie, Księgowość czy „Projekt A”. Zespoły żyją pod organizacją, nie zastępują jej.

User to tożsamość. To login i profil: email, imię, hasło lub SSO, opcjonalnie ustawienia MFA. Użytkownik może istnieć bez żadnych uprawnień.

Membership to rekord dostępu. Odpowiada na pytanie: „Ten użytkownik należy do tej organizacji (i opcjonalnie do tego zespołu) ze statusem i tymi rolami.” Oddzielenie tożsamości (User) od dostępu (Membership) ułatwia modelowanie kontraktorów, offboardingu i dostępu wielo‑orgowego.

Proste znaczenia, które możesz używać w kodzie i UI:

  • Member: użytkownik z aktywnym członkostwem w orgu lub zespole.
  • Role: nazwany zestaw uprawnień (np. Org Admin, Team Manager).
  • Permission: pojedyncza dozwolona akcja (np. „view invoices”).
  • Tenant boundary: zasada, że dane są zasięgowane do organizacji.

Traktuj członkostwo jako mały automat stanów, nie jako boolean. Typowe stany to invited, active, suspended i removed. To utrzymuje zaproszenia, zatwierdzenia i offboarding spójnymi i audytowalnymi.

Pojedynczy model relacyjny: główne tabele i relacje

Dobry schemat multi‑tenant zaczyna się od jednej idei: przechowuj „kto należy gdzie” w jednym miejscu, a wszystko inne jako tabele pomocnicze. W ten sposób odpowiesz na podstawowe pytania (kto jest w orgu, kto jest w zespole, co może zrobić) bez skakania po niespowiązanych modelach.

Główne tabele, których zwykle potrzebujesz:

  • organizations: jeden wiersz na konto klienta (tenant). Zawiera nazwę, status, pola bilingowe i niezmienialne id.
  • teams: grupy wewnątrz organizacji (Wsparcie, Sprzedaż, Admin). Zawsze należą do jednej organizacji.
  • users: jeden wiersz na osobę. To jest globalne, nie per organizacja.
  • memberships: most łączący: „ten użytkownik należy do tej organizacji” i opcjonalnie „również do tego zespołu”.
  • role_grants (lub role_assignments): jakie role ma membership, na poziomie org, zespołu, lub obu.

Trzymaj klucze i ograniczenia ścisłe. Używaj zastępczych kluczy głównych (UUIDy lub bigintów) dla każdej tabeli. Dodaj klucze obce takie jak teams.organization_id -> organizations.id i memberships.user_id -> users.id. Potem dodaj kilka unikalnych ograniczeń, żeby zatrzymać duplikaty, zanim pojawią się w produkcji.

Zasady, które wychwytują większość błędnych danych wcześnie:

  • Jeden slug lub zewnętrzny klucz orga: unique(organizations.slug)
  • Nazwy zespołów w obrębie orgu: unique(teams.organization_id, teams.name)
  • Brak duplikatów członkostwa orgowego: unique(memberships.organization_id, memberships.user_id)
  • Brak duplikatów członkostwa w zespole (tylko jeśli modelujesz osobno): unique(team_memberships.team_id, team_memberships.user_id)

Zdecyduj, co ma być append‑only, a co edytowalne. Organizacje, zespoły i użytkownicy są edytowalni. Memberships często są edytowalne dla aktualnego stanu (active, suspended), ale zmiany powinny też zapisywać się do append‑only logu dostępu, by audyt był prosty później.

Zaproszenia i stany członkostwa, które pozostają spójne

Najprostszy sposób na utrzymanie czystego dostępu to traktować zaproszenie jako własny rekord, a nie jako pół‑utworzone membership. Membership oznacza „użytkownik aktualnie należy”. Zaproszenie oznacza „oferowaliśmy dostęp, ale nie jest on jeszcze realny”. Oddzielenie ich unika duchowych członków, półutworzonych uprawnień i zagadki „kto zaprosił tę osobę?”.

Prosty, niezawodny model stanów

Dla memberships użyj małego zestawu stanów, które możesz wytłumaczyć każdemu:

  • active: użytkownik ma dostęp do orgu (i do zespołów, których jest członkiem)
  • suspended: tymczasowo zablokowany, ale historia pozostaje nienaruszona
  • removed: już nie jest członkiem, zachowane dla audytu i raportów

Wiele zespołów unika stanu członkostwa „invited” i trzyma „invited” wyłącznie w tabeli zaproszeń. To zwykle jest czyściej: wiersze membership istnieją tylko dla użytkowników, którzy faktycznie mają dostęp (active) lub kiedy go mieli (suspended/removed).

Zaproszenia mailowe zanim konto istnieje

Aplikacje B2B często zapraszają przez email, gdy konto użytkownika jeszcze nie istnieje. Przechowaj email w rekordzie zaproszenia razem z zakresem (org lub team), planowaną rolą i informacją, kto wysłał zaproszenie. Jeśli osoba później zarejestruje się tym emailem, możesz dopasować oczekujące zaproszenia i pozwolić im zaakceptować.

Gdy zaproszenie zostanie zaakceptowane, obsłuż to w jednej transakcji: oznacz zaproszenie jako accepted, utwórz membership i zapisz wpis audytu (kto zaakceptował, kiedy i jakim emailem).

Zdefiniuj jasne stany końcowe zaproszenia:

  • expired: po dacie ważności, nie można zaakceptować
  • revoked: unieważnione przez administratora, nieaktywne
  • accepted: skonwertowane w membership

Zabezpiecz się przed duplikatami zaproszeń wymuszając „tylko jedno oczekujące zaproszenie na org lub team dla danego emaila”. Jeśli wspierasz ponowne zaproszenia, albo wydłuż termin ważności istniejącego oczekującego zaproszenia, albo unieważnij stare i wydaj nowy token.

Role i dziedziczenie bez wprowadzania zamieszania

Utrzymaj porządek w modelu danych
Użyj modelu danych, który pozostaje czytelny, gdy dodajesz zespoły, wyjątki i historię audytu.
Zaprojektuj bazę

Większość aplikacji B2B potrzebuje dwóch poziomów dostępu: co ktoś może zrobić w całej organizacji i co może zrobić w konkretnym zespole. Mieszanie tego w jednej kolumnie role jest źródłem niespójności.

Role orgowe odpowiadają na pytania typu: czy ta osoba może zarządzać rozliczeniami, zapraszać ludzi lub widzieć wszystkie zespoły? Role zespołowe mówią: czy może edytować elementy w Zespole A, zatwierdzać zgłoszenia w Zespole B, czy tylko przeglądać?

Dziedziczenie ról jest najłatwiejsze do utrzymania, jeśli obowiązuje jedna zasada: rola orgowa ma zastosowanie wszędzie, chyba że zespół wyraźnie mówi inaczej. To utrzymuje zachowanie przewidywalne i redukuje duplikację danych.

Czysty sposób modelowania to przechowywanie przypisań ról z zakresem:

  • role_assignments: user_id, org_id, opcjonalne team_id (NULL oznacza org‑wide), role_id, created_at, created_by

Jeśli chcesz „jednej roli na zakres”, dodaj unikalne ograniczenie na (user_id, org_id, team_id).

Efektywny dostęp dla zespołu staje się wtedy prosty:

  1. Szukaj przypisania specyficznego dla zespołu (team_id = X). Jeśli istnieje, użyj go.

  2. W przeciwnym razie użyj przypisania ogólnorgowego (team_id IS NULL).

Dla zasad najmniejszego przywileju wybierz minimalną rolę orgową (często „Member”) i nie dawaj jej ukrytych uprawnień administracyjnych. Nowi użytkownicy nie powinni mieć domyślnie dostępu do zespołów, chyba że produkt tego wymaga — jeśli auto‑przydzielasz, rób to przez jawne tworzenie członkostw zespołowych, a nie przez ciche rozszerzanie roli orgowej.

Nadpisania powinny być rzadkie i oczywiste. Przykład: Maria jest org „Manager”, ale w zespole Finansów powinna być „Viewer”. Przechowujesz jedno przypisanie ogólnorgowe i jedno przypisanie zakresowe dla Finansów. Żadnego kopiowania uprawnień — wyjątek jest widoczny.

Nazwy ról dobrze działają dla powszechnych wzorców. Używaj jawnych uprawnień tylko gdy masz rzeczywiste wyjątki (np. „może eksportować, ale nie może edytować”), albo gdy wymogi zgodności potrzebują precyzyjnej listy akcji. Nawet wtedy trzymaj ten sam pomysł zakresu, żeby model mentalny został spójny.

Zmiany przyjazne audytowi: kto zmienił dostęp

Jeśli aplikacja przechowuje tylko aktualną rolę w wierszu membership, tracisz historię. Gdy ktoś zapyta „kto nadał Alexowi admina w zeszły wtorek?”, nie masz pewnej odpowiedzi. Potrzebujesz historii zmian, nie tylko stanu bieżącego.

Najprostsze podejście to dedykowana tabela audytu, która zapisuje zdarzenia dostępu. Traktuj ją jako append‑only dziennik: nie edytujesz starych wierszy, tylko dopisujesz nowe.

Praktyczna tabela audytu zwykle zawiera:

  • actor_user_id (kto wykonał zmianę)
  • subject_type i subject_id (membership, team, org)
  • action (invite_sent, role_changed, membership_suspended, team_deleted)
  • occurred_at (kiedy się wydarzyło)
  • reason (opcjonalny tekst, np. „offboarding kontraktora”)

Aby uchwycić „przed” i „po”, zapisz mały snapshot interesujących pól. Ogranicz go do danych związanych z kontrolą dostępu, nie pełnych profili użytkowników. Na przykład: before_role, after_role, before_state, after_state, before_team_id, after_team_id. Jeśli wolisz elastyczność, użyj dwóch kolumn JSON (before, after), ale trzymaj payload mały i spójny.

Dla memberships i teams soft delete zwykle jest lepszy niż hard delete. Zamiast usuwać wiersz, oznacz go jako nieaktywny polami typu deleted_at i deleted_by. To utrzymuje integralność kluczy obcych i ułatwia wytłumaczenie dawnego dostępu. Hard delete ma sens dla naprawdę tymczasowych rekordów (np. wygasłych zaproszeń), ale tylko gdy masz pewność, że nie będą potrzebne później.

Z tym podejściem szybko odpowiesz na pytania zgodności:

  • Kto nadał lub usunął dostęp i kiedy?
  • Co dokładnie się zmieniło (rola, zespół, stan)?
  • Czy usunięcie dostępu było częścią normalnego offboardingu?

Krok po kroku: projektowanie schematu w relacyjnej bazie danych

Wypuść portal wielo‑tenantowy
Zbuduj portal klienta ze zmienianiem organizacji, widokami zespołów i ekranami opartymi o role.
Stwórz aplikację

Zacznij prosto: jedno miejsce, które mówi kto należy gdzie i dlaczego. Buduj w małych krokach i dodawaj reguły po drodze, aby dane nie odpływały w stan „prawie poprawne”.

Praktyczna kolejność działań (sprawdzi się w PostgreSQL i innych relacyjnych DB):

  1. Utwórz organizations i teams, każde z stabilnym kluczem głównym (UUID lub bigint). Dodaj teams.organization_id jako klucz obcy i wcześnie zdecyduj, czy nazwy zespołów muszą być unikalne w obrębie orgu.

  2. Trzymaj users oddzielnie od członkostw. W users trzymaj pola tożsamości (email, status, created_at). W memberships trzymaj „należy do orgu/zespołu” z user_id, organization_id, opcjonalnym team_id i kolumną state (active, suspended, removed).

  3. Dodaj invitations jako własną tabelę, a nie kolumnę w membership. Przechowaj organization_id, opcjonalne team_id, email, token, expires_at i accepted_at. Wymuś unikalność dla „jedno otwarte zaproszenie na org + email + team”, aby nie generować duplikatów.

  4. Modeluj role przez jawne tabele. Prosty sposób: roles (admin, member itd.) i role_assignments wskazujące na zakres orgowy (brak team_id) lub zespołowy (team_id ustawione). Trzymaj reguły dziedziczenia spójne i przetestowalne.

  5. Dodaj trail audytu od pierwszego dnia. Użyj access_events z polami actor_user_id, target_user_id (lub email dla zaproszeń), action (invite_sent, role_changed, removed), scope (org/team) i created_at.

Po stworzeniu tych tabel uruchom kilka podstawowych zapytań administracyjnych, które potwierdzą rzeczywistość: „kto ma dostęp orgowy?”, „które zespoły nie mają adminów?” oraz „które zaproszenia wygasły, ale nadal są otwarte?”. Te pytania ujawniają brakujące ograniczenia na wczesnym etapie.

Zasady i ograniczenia, które zapobiegają bałaganowi w danych

Uniknij długu technologicznego od pierwszego dnia
Wdróż produkcyjny backend z generowaniem kodu w Go i czytelną regeneracją, gdy wymagania się zmienią.
Generuj kod

Schemat pozostaje sensowny, gdy baza, a nie tylko kod, wymusza granice najemcy. Najprostsza zasada: każda tabela zależna od najemcy ma org_id, a każde zapytanie uwzględnia ten filtr. Nawet jeśli ktoś zapomni filtra w aplikacji, baza powinna utrudnić przypadkowe połączenia między orgami.

Bariery, które trzymają dane w czystości

Zacznij od kluczy obcych, które zawsze wskazują „wewnątrz tej samej org”. Na przykład, jeśli masz team_memberships oddzielnie, wiersz powinien referować team_id i user_id, ale też zawierać org_id. Dzięki kluczom złożonym możesz wymusić, że wskazany zespół należy do tej samej organizacji.

Ograniczenia, które zapobiegają najczęstszym problemom:

  • Jedno aktywne członkostwo orgowe na użytkownika: unique na (org_id, user_id) z warunkiem dla aktywnych wierszy (tam gdzie wspierane).
  • Jedno oczekujące zaproszenie na email na org lub team: unique na (org_id, team_id, email) gdzie state = 'pending'.
  • Tokeny zaproszeń unikalne globalnie i nigdy nieużywane ponownie: unique na invite_token.
  • Zespół należy dokładnie do jednej org: teams.org_id NOT NULL z kluczem obcym do orgs(id).
  • Zakończ członkostwa zamiast usuwać je: przechowuj ended_at (i opcjonalnie ended_by) by chronić historię audytu.

Indeksowanie pod zapytania, których faktycznie używasz

Zindeksuj zapytania, które aplikacja wykonuje cały czas:

  • (org_id, user_id) dla „w jakich orgach jest ten użytkownik?”
  • (org_id, team_id) dla „lista członków tego zespołu”
  • (invite_token) dla „zaakceptuj zaproszenie”
  • (org_id, state) dla „oczekujące zaproszenia” i „aktywni członkowie”

Trzymaj nazwy orgów edytowalne. Używaj niezmiennego orgs.id wszędzie, a orgs.name (i slug) traktuj jako pola edytowalne. Zmiana nazwy dotyka wtedy jednego wiersza.

Przenoszenie zespołu między orgami zwykle jest decyzją polityczną. Najbezpieczniejsza opcja to zabronić tego (lub sklonować zespół), bo członkostwa, role i historia audytu są związane z orgiem. Jeśli musisz pozwolić na ruch, zrób to w jednej transakcji i zaktualizuj wszystkie wiersze potomne trzymające org_id.

Aby uniknąć porzuconych rekordów, gdy użytkownicy odchodzą, unikaj twardych kasowań. Dezaktywuj użytkownika, zakończ jego członkostwa i ogranicz kasowanie na wierszach rodzica (ON DELETE RESTRICT) chyba że naprawdę chcesz kaskadowo usuwać.

Przykład scenariusza: jedna organizacja, dwa zespoły, bezpieczne zmiany dostępu

Wyobraź sobie firmę Northwind Co z jedną org i dwoma zespołami: Sprzedaż i Wsparcie. Zatrudniają kontraktorkę Mię do obsługi zgłoszeń Support na miesiąc. Model powinien pozostać przewidywalny: jedna osoba, jedno członkostwo orgowe, opcjonalne członkostwa zespołowe i jasne stany.

Administrator orgu (Ava) zaprasza Mię emailem. System tworzy rekord zaproszenia związany z orgiem, status pending i datą wygaśnięcia. Nic więcej się nie zmienia — nie ma „pół‑użytkownika” o niejasnym dostępie.

Gdy Mia akceptuje, zaproszenie oznaczane jest jako accepted, tworzy się wiersz membership z state = active. Ava nadaje Mi się rolę orgową member (nie admin). Następnie Ava dodaje ją do zespołu Support i przypisuje rolę zespołową jak support_agent.

Dodajmy twist: Ben jest pełnoetatowym pracownikiem z rolą orgową admin, ale nie powinien widzieć danych Supportu. Obsłużysz to nadpisaniem na poziomie zespołu, które jawnie degraduje jego rolę w zespole Support, zachowując równocześnie jego uprawnienia orgowe do ustawień.

Tydzień później Mia łamie politykę i zostaje zawieszona. Zamiast usuwać wiersze, Ava ustawia state = suspended. Członkostwa zespołowe mogą pozostać w bazie, ale przestają działać, bo członkostwo orgowe nie jest aktywne.

Historia audytu pozostaje czysta, bo każda zmiana to zdarzenie:

  • Ava zaprosiła Mię (kto, co, kiedy)
  • Mia zaakceptowała zaproszenie
  • Ava dodała Mię do Support i przypisała support_agent
  • Ava ustawiła nadpisanie dla Bena w Support
  • Ava zawiesiła Mię

Dzięki temu UI może pokazać jasne podsumowanie dostępu: status orgu (active/suspended), rolę orgową, listę zespołów z rolami i nadpisaniami oraz „ostatnie zmiany dostępu”, które tłumaczą, dlaczego ktoś widzi lub nie widzi Sales czy Support.

Typowe błędy i pułapki do uniknięcia

Centralizuj zarządzanie dostępem
Zastąp rozsypane skrypty jedną aplikacją, która porządkuje zaproszenia, role i zmiany dostępu.
Wypróbuj AppMaster

Większość błędów w dostępie wynika z „prawie poprawnych” modeli danych. Schemat na początku wygląda dobrze, potem nawarstwiają się przypadki brzegowe: ponowne zaproszenia, przenoszenia zespołów, zmiany ról i offboarding.

Częstą pułapką jest mieszanie zaproszeń i członkostw w jednym wierszu. Jeśli trzymasz „invited” i „active” w tym samym rekordzie bez jasnego znaczenia, zaczniesz zadawać niemożliwe pytania typu „czy ta osoba jest członkiem, jeśli nigdy nie zaakceptowała?”. Trzymaj zaproszenia i członkostwa oddzielnie albo zrób jasny i spójny automat stanów.

Inny częsty błąd to pojedyncza kolumna roli w tabeli użytkownika i „koniec tematu”. Role prawie zawsze są zakreślone zakresem (rola orgowa, rola zespołowa, rola projektowa). Globalna rola zmusza do obejść typu „użytkownik jest adminem u jednego klienta, ale tylko do odczytu u innego”, co łamie oczekiwania multi‑tenant i generuje kłopoty wsparcia.

Pułapki, które zwykle bolą później:

  • Przez przypadek dozwalane członkostwa cross‑org (team_id wskazuje na org A, membership na org B).
  • Twarde usuwanie memberships i utrata śladu „kto miał dostęp tydzień temu?”.
  • Brak reguł unikalności, więc użytkownik dostaje duplikatowy dostęp przez identyczne wiersze.
  • Ciche nakładanie się dziedziczenia (org admin + członek zespołu + nadpisanie), przez co nikt nie potrafi wyjaśnić, skąd się wziął dostęp.
  • Traktowanie „zaakceptowano zaproszenie” jako zdarzenia UI, a nie faktu bazy danych.

Krótki przykład: kontraktor został zaproszony do orgu, dołączył do Team Sales, potem został usunięty i ponownie zaproszony miesiąc później. Jeśli nadpisujesz stary wiersz, tracisz historię. Jeśli pozwalasz na duplikaty, może mieć dwa aktywne memberships. Jasne stany, zakresy ról i właściwe ograniczenia zapobiegają obu problemom.

Szybkie kontrole i kolejne kroki do wdrożenia w aplikacji

Zanim zaczniesz pisać kod, przejrzyj model na papierze i sprawdź, czy nadal ma sens. Dobry model dostępu multi‑tenant powinien być nudny: te same zasady mają zastosowanie wszędzie, a „przypadki specjalne” są rzadkie.

Szybka lista kontrolna, aby wychwycić typowe luki:

  • Każde membership wskazuje dokładnie jednego użytkownika i jedną organizację, z unikalnym ograniczeniem zapobiegającym duplikatom.
  • Stany zaproszeń, członkostw i usunięć są jawne (nie implikowane przez NULL), a przejścia są ograniczone (np. nie można zaakceptować wygasłego zaproszenia).
  • Role przechowuje się w jednym miejscu, a efektywny dostęp oblicza spójnie (w tym zasady dziedziczenia, jeśli ich używasz).
  • Usuwanie orgów/zespołów/użytkowników nie kasuje historii (stosuj soft delete lub archiwizację tam, gdzie potrzebne są ślady audytu).
  • Każda zmiana dostępu emituje zdarzenie audytu z informacją o aktorze, obiekcie, zakresie, znaczniku czasu i powodzie/źródle.

Przetestuj projekt pytaniami: jeśli nie odpowiesz czysto jednym zapytaniem i jasną regułą, prawdopodobnie potrzebujesz ograniczenia lub dodatkowego stanu:

  • Co się stanie, jeśli użytkownik zostanie zaproszony dwukrotnie, a potem email się zmieni?
  • Czy admin zespołu może usunąć właściciela orgu z tego zespołu?
  • Jeśli rola orgowa daje dostęp do wszystkich zespołów, czy jeden zespół może to nadpisać?
  • Jeśli zaproszenie jest zaakceptowane po zmianie roli, która rola ma zastosowanie?
  • Gdy support pyta „kto usunął dostęp”, czy możesz to szybko udowodnić?

Spisz, co administratorzy i support muszą rozumieć: stany członkostwa (i co je wywołuje), kto może zapraszać/usunąć, co znaczy dziedziczenie ról prostym językiem i gdzie szukać zdarzeń audytowych podczas incydentu.

Wdróż ograniczenia najpierw (unique, foreign keys, dozwolone przejścia), potem zbuduj logikę biznesową wokół nich, tak aby baza pomagała trzymać porządek. Trzymaj decyzje polityczne (dziedziczenie włączone/wyłączone, domyślne role, wygaśnięcie zaproszeń) w tabelach konfiguracyjnych zamiast zakodowanych stałych.

Jeśli chcesz to zbudować bez ręcznego pisania każdego backendu i panelu admina, AppMaster (appmaster.io) może pomóc: zamodelujesz te tabele w PostgreSQL i zaimplementujesz przejścia zaproszeń i członkostw jako jawne procesy biznesowe, a system wygeneruje realny kod do wdrożenia produkcyjnego.

FAQ

Dlaczego nie powinienem trzymać jednej kolumny `role` w tabeli `users`?

Użyj oddzielnego rekordu membership, aby role i uprawnienia były związane z organizacją (a opcjonalnie z zespołem), a nie z globalną tożsamością użytkownika. Dzięki temu ta sama osoba może być Adminem w jednej organizacji i Viewerem w innej bez obejść.

Czy zaproszenie powinno od razu tworzyć wiersz membership?

Trzymaj je osobno: invitation to oferta zawierająca email, zakres i datę wygaśnięcia, a membership oznacza, że użytkownik faktycznie ma dostęp. To zapobiega „duchowym członkom”, niejasnemu statusowi i błędom bezpieczeństwa, gdy adresy e‑mail się zmieniają.

Jakie stany członkostwa powinienem stosować?

Mały zestaw stanów, np. active, suspended i removed, wystarcza w większości aplikacji B2B. Jeśli trzymasz invited wyłącznie w tabeli zaproszeń, wpisy membership pozostają jednoznaczne: reprezentują bieżący lub przeszły dostęp, a nie dostęp oczekujący.

Jak modelować role orgowe vs role zespołowe bez wprowadzania zamieszania?

Przechowuj role orgowe i zespołowe jako przydziały ze wskazanym zakresem (org‑wide gdy team_id jest NULL, lub specyficzne dla zespołu gdy team_id jest ustawiony). Przy sprawdzaniu dostępu do zespołu najpierw szukaj przypisania dla konkretnego zespołu; jeśli go nie ma, użyj przypisania orgowego.

Jaka jest najprostsza zasada dziedziczenia ról?

Zacznij od prostej, przewidywalnej zasady: role orgowe domyślnie stosują się wszędzie, a role zespołowe nadpisują tylko wtedy, gdy to jest jawnie ustawione. Trzymaj nadpisania rzadkie i widoczne, żeby można było łatwo wyjaśnić, skąd się bierze dostęp.

Jak zapobiec duplikatom zaproszeń i obsłużyć ponowne zaproszenia?

Wymuszaj „tylko jedno oczekujące zaproszenie na org/team na email” za pomocą unikalnego ograniczenia i jasnego cyklu życia: pending/accepted/revoked/expired. Jeśli potrzebujesz ponownych zaproszeń, zaktualizuj istniejące oczekujące zaproszenie albo najpierw je unieważnij przed wydaniem nowego tokena.

Jak wymusić granicę najemcy (tenant boundary) w bazie danych?

Każdy wiersz zależny od najemcy powinien zawierać org_id, a klucze obce/ograniczenia powinny uniemożliwiać mieszanie organizacji (np. zespół referencjonowany przez membership musi należeć do tej samej organizacji). To zmniejsza ryzyko błędów wynikających z brakujących filtrów w kodzie aplikacji.

Jak sprawić, by zmiany w dostępie były przyjazne dla audytu?

Prowadź append‑only dziennik zdarzeń dostępu, który zapisuje kto zrobił co, komu, kiedy i w jakim zakresie (org lub zespół). Zapisuj kluczowe pola przed/po (role, stan, team), żeby móc szybko odpowiedzieć: „kto nadał admina w zeszły wtorek?”.

Czy powienienem hard‑delete’ować memberships i zaproszenia?

Unikaj twardych usunięć dla memberships i zespołów; oznacz je jako zakończone/wyłączone, aby historia pozostała dostępna i klucze obce nie zostały złamane. W przypadku zaproszeń można również je zachować (nawet wygasłe) dla pełnego śladu bezpieczeństwa; przynajmniej nie ponownie używaj tokenów.

Które indeksy są najważniejsze dla tego schematu?

Zindeksuj ścieżki, których używasz najczęściej: (org_id, user_id) dla sprawdzeń członkostwa w orgu, (org_id, team_id) dla list członków zespołu, (invite_token) dla akceptacji zaproszenia oraz (org_id, state) dla ekranów administracyjnych (np. „aktywni członkowie”, „oczekujące zaproszenia”). Indeksy twórz zgodnie z realnymi zapytaniami, nie dla każdej kolumny.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij