Datenbankschema für B2B‑Organisationen und Teams, das übersichtlich bleibt
Datenbankschema für B2B‑Organisationen und Teams: ein praktisches relationales Muster für Einladungen, Mitgliedschaftsstatus, Rollenvererbung und revisionssichere Änderungen.

Welches Problem dieses Schema‑Musters löst
Die meisten B2B‑Apps sind nicht einfach „Nutzer‑Accounts“‑Apps. Sie sind geteilte Arbeitsbereiche, in denen Menschen zu einer Organisation gehören, in Teams aufgeteilt sind und je nach Rolle unterschiedliche Berechtigungen haben. Sales, Support, Finance und Admins brauchen verschiedene Zugriffe, und diese Zugriffe ändern sich im Laufe der Zeit.
Ein zu simples Modell bricht schnell. Wenn du nur eine users‑Tabelle mit einer einzigen role‑Spalte pflegst, kannst du nicht ausdrücken, dass dieselbe Person in einer Organisation Admin und in einer anderen Viewer ist. Du kannst auch nicht übliche Fälle abbilden wie Auftragnehmer, die nur ein Team sehen sollen, oder einen Mitarbeiter, der ein Projekt verlässt, aber noch zur Firma gehört.
Einladungen sind eine weitere häufige Fehlerquelle. Wenn eine Einladung nur eine E‑Mail‑Zeile ist, wird unklar, ob die Person schon „in“ der Organisation ist, welchem Team sie beitreten soll und was passiert, wenn sie sich mit einer anderen E‑Mail registriert. Kleine Inkonsistenzen hier verwandeln sich oft in Sicherheitsprobleme.
Dieses Muster verfolgt vier Ziele:
- Sicherheit: Berechtigungen ergeben sich aus expliziten Memberships, nicht aus Annahmen.
- Klarheit: Orgs, Teams und Rollen haben jeweils eine einzige Quelle der Wahrheit.
- Konsistenz: Einladungen und Memberships folgen einem vorhersehbaren Lebenszyklus.
- Historie: Du kannst nachvollziehen, wer Zugriff gewährt, Rollen geändert oder jemanden entfernt hat.
Das Versprechen ist ein einziges relationales Modell, das verständlich bleibt, während die Features wachsen: mehrere Orgs pro User, mehrere Teams pro Org, vorhersehbare Rollenvererbung und revisionsfreundliche Änderungen. Eine Struktur, die du heute umsetzen und später erweitern kannst, ohne alles neu zu schreiben.
Wichtige Begriffe: Orgs, Teams, Users und Memberships
Wenn du ein Schema willst, das nach sechs Monaten noch lesbar ist, stimme dich zuerst auf ein paar Begriffe ein. Die meiste Verwirrung entsteht durch das Vermischen von „wer jemand ist“ mit „was er tun kann“.
Eine Organization (Org) ist die oberste Mandanten‑Grenze. Sie repräsentiert den Kunden oder das Business‑Konto, dem die Daten gehören. Sind zwei Nutzer in verschiedenen Orgs, sollten sie standardmäßig nicht die Daten des anderen sehen. Diese eine Regel verhindert viele unbeabsichtigte Cross‑Tenant‑Zugriffe.
Ein Team ist eine kleinere Gruppe innerhalb einer Org. Teams bilden reale Arbeitseinheiten ab: Sales, Support, Finance oder „Projekt A“. Teams ersetzen nicht die Org‑Grenze; sie leben darunter.
Ein User ist eine Identität. Es ist das Login und Profil der Person: E‑Mail, Name, Passwort oder SSO‑ID und ggf. MFA‑Einstellungen. Ein User kann existieren, ohne dass er irgendwo Zugriff hat.
Eine Membership ist der Zugriffsdatensatz. Sie beantwortet: „Dieser User gehört zu dieser Org (und optional zu diesem Team) mit diesem Status und diesen Rollen.“ Identität (User) getrennt von Zugriff (Membership) zu halten, macht Auftragnehmer‑Modelle, Offboarding und Multi‑Org‑Zugriff viel einfacher zu modellieren.
Einfache Bedeutungen, die du im Code und UI verwenden kannst:
- Member: ein User mit einer aktiven Membership in einer Org oder einem Team.
- Role: ein benannter Bündel von Berechtigungen (z. B. Org Admin, Team Manager).
- Permission: eine einzelne erlaubte Aktion (z. B. „view invoices“).
- Tenant boundary: die Regel, dass Daten auf eine Org beschränkt sind.
Behandle Membership als einen kleinen State‑Machine, nicht als Boolean. Typische Zustände sind invited, active, suspended und removed. Das hält Einladungen, Genehmigungen und Offboarding konsistent und auditierbar.
Das einzelne relationale Modell: Kern‑Tabellen und Beziehungen
Ein gutes Multi‑Tenant‑Schema startet mit einer Idee: Speichere „wer wo gehört“ an einem Ort und halte alles andere als unterstützende Tabellen. So kannst du grundlegende Fragen beantworten (Wer ist in der Org, wer ist im Team, was darf er?) ohne quer über viele Modelle springen zu müssen.
Kern‑Tabellen, die du üblicherweise brauchst:
- organizations: eine Zeile pro Kundenkonto (Tenant). Enthält Name, Status, Billing‑Felder und eine unveränderliche id.
- teams: Gruppen innerhalb einer Organization (Support, Sales, Admin). Gehört immer zu genau einer Organization.
- users: eine Zeile pro Person. Global, nicht pro Organisation.
- memberships: die Brücke, die sagt „dieser User gehört zu dieser Organization“ und optional „auch zu diesem Team“.
- role_grants (oder role_assignments): welche Rollen eine Membership hat, auf Org‑Ebene, Team‑Ebene oder beides.
Halte Keys und Constraints strikt. Verwende surrogate Primary Keys (UUIDs oder bigints) für jede Tabelle. Füge Foreign Keys wie teams.organization_id -> organizations.id und memberships.user_id -> users.id hinzu. Dann setze ein paar Unique‑Constraints, um Duplikate früh zu verhindern.
Regeln, die die häufigsten fehlerhaften Daten früh auffangen:
- Ein Org‑Slug oder externer Key:
unique(organizations.slug) - Team‑Namen pro Org:
unique(teams.organization_id, teams.name) - Keine doppelte Org‑Membership:
unique(memberships.organization_id, memberships.user_id) - Keine doppelte Team‑Membership (falls du Team‑Membership separat modellierst):
unique(team_memberships.team_id, team_memberships.user_id)
Entscheide, was append‑only ist und was updatable. Organizations, Teams und Users sind updatable. Memberships sind oft für den aktuellen Zustand updatable (active, suspended), aber Änderungen sollten zusätzlich in ein append‑only Access‑Log geschrieben werden, damit Audits später einfach sind.
Einladungen und Mitgliedschafts‑Zustände, die konsistent bleiben
Der einfachste Weg, um Zugriff sauber zu halten, ist eine Einladung als eigenen Datensatz zu behandeln, nicht als halb fertige Membership. Eine Membership bedeutet „dieser User hat aktuell Zugriff“. Eine Einladung bedeutet „wir haben Zugang angeboten, aber er ist noch nicht real“. Die Trennung vermeidet Ghost‑Members, halb erstellte Berechtigungen und das Rätsel „wer hat diese Person eingeladen?“.
Ein einfaches, verlässliches Zustandsmodell
Für Memberships verwende eine kleine Menge Zustände, die sich jedem erklären lassen:
active: der User kann auf die Org (und alle Teams, deren Mitglied er ist) zugreifensuspended: temporär gesperrt, die Historie bleibt erhaltenremoved: kein Mitglied mehr, zu Audit‑ und Reporting‑Zwecken erhalten
Viele Teams vermeiden einen Membership‑Zustand invited und führen invited strikt nur in der invitations‑Tabelle. Das ist oft sauberer: Membership‑Zeilen existieren nur für User, die tatsächlich Zugriff haben (active), oder die ihn früher hatten (suspended/removed).
E‑Mail‑Einladungen bevor ein Account existiert
B2B‑Apps laden häufig per E‑Mail ein, wenn noch kein User‑Account existiert. Speichere die E‑Mail auf dem Invitation‑Record, zusammen mit dem Anwendungsbereich (Org oder Team), der vorgesehenen Rolle und wer eingeladen hat. Meldet sich die Person später mit dieser E‑Mail an, kannst du offene Einladungen zuordnen und das Akzeptieren erlauben.
Wenn eine Einladung angenommen wird, handle das in einer Transaktion: markiere die Einladung als accepted, erstelle die Membership und schreibe einen Audit‑Eintrag (wer akzeptierte, wann und welche E‑Mail verwendet wurde).
Definiere klare Endzustände für Einladungen:
expired: Ablaufdatum überschritten, kann nicht mehr akzeptiert werdenrevoked: von einem Admin storniert, kann nicht mehr akzeptiert werdenaccepted: wurde in eine Membership umgewandelt
Vermeide doppelte Einladungen, indem du „nur eine offene Einladung pro Org oder Team pro E‑Mail“ durchsetzt. Wenn du Re‑Invites unterstützt, verlängere entweder die Laufzeit der bestehenden offenen Einladung oder widerrufe die alte und erstelle einen neuen Token.
Rollen und Vererbung, ohne Zugriff verwirrend zu machen
Die meisten B2B‑Apps brauchen zwei Zugriffsebenen: was jemand auf Organisationsebene tun kann und was er innerhalb eines bestimmten Teams tun kann. Das in ein einziges role‑Feld zu mischen ist der Punkt, an dem Apps inkonsistent werden.
Org‑Rollen beantworten Fragen wie: Darf diese Person Billing verwalten, Leute einladen oder alle Teams sehen? Team‑Rollen beantworten: Darf sie Einträge in Team A bearbeiten, Anfragen in Team B genehmigen oder nur anschauen?
Rollenvererbung ist am einfachsten handhabbar, wenn sie einer Regel folgt: Eine Org‑Rolle gilt überall, sofern ein Team nicht explizit etwas anderes festlegt. Das macht das Verhalten vorhersehbar und reduziert doppelte Daten.
Eine saubere Modellierung ist, Rollenmit Zuweisungs‑Scope zu speichern:
role_assignments:user_id,org_id, optionalteam_id(NULL bedeutet org‑weit),role_id,created_at,created_by
Wenn du „eine Rolle pro Scope“ willst, füge einen Unique‑Constraint auf (user_id, org_id, team_id) hinzu.
Die effektiven Zugriffsrechte für ein Team ergeben sich dann so:
-
Suche nach einer team‑spezifischen Zuweisung (
team_id = X). Falls vorhanden, verwende diese. -
Sonst zur org‑weiten Zuweisung zurückfallen (
team_id IS NULL).
Für Least‑Privilege‑Defaults wähle eine minimale Org‑Rolle (oft „Member“) und gib ihr keine versteckten Admin‑Befugnisse. Neue Nutzer sollten nicht stillschweigend Team‑Zugriff erhalten, es sei denn, dein Produkt benötigt das wirklich. Falls du Auto‑Grants machst, erzeuge explizite Team‑Memberships, anstatt heimlich die Org‑Rolle zu erweitern.
Overrides sollten selten und offensichtlich sein. Beispiel: Maria ist Org‑„Manager“ (kann einladen, Reports sehen), aber im Finance‑Team soll sie „Viewer“ sein. Speichere eine org‑weite Zuweisung für Maria und zusätzlich eine team‑spezifische Ausnahme für Finance. Keine Berechtigungskopien; die Ausnahme ist sichtbar.
Rollennamen funktionieren gut für gängige Muster. Nutze explizite Permissions nur bei echten Ausnahmen (z. B. „darf exportieren, darf aber nicht editieren“) oder wenn Compliance eine genaue Liste erlaubter Aktionen verlangt. Auch dann behalte das Scope‑Konzept bei, damit das mentale Modell konsistent bleibt.
Revisionsfreundliche Änderungen: wer hat Zugriff geändert
Wenn deine App nur die aktuelle Rolle in einer Membership‑Zeile speichert, verlierst du die Geschichte. Fragt jemand: „Wer hat Alex letzten Dienstag Admin‑Zugriff gegeben?“, hast du keine verlässliche Antwort. Du brauchst Änderungsverlauf, nicht nur aktuellen Zustand.
Der einfachste Ansatz ist eine dedizierte Audit‑Log‑Tabelle, die Zugriffsereignisse aufzeichnet. Behandle sie als append‑only Journal: alte Audit‑Zeilen werden nie editiert; du fügst nur neue hinzu.
Eine praktische Audit‑Tabelle enthält meist:
actor_user_id(wer die Änderung vorgenommen hat)subject_typeundsubject_id(Membership, Team, Org)action(invite_sent, role_changed, membership_suspended, team_deleted)occurred_at(Zeitpunkt)reason(optional, Freitext wie „contractor offboarding“)
Um „vorher“ und „nachher“ zu erfassen, speichere kleine Snapshots der relevanten Felder. Halte die Payload auf Zugriffs‑Daten begrenzt, nicht komplette User‑Profile. Beispielsweise: before_role, after_role, before_state, after_state, before_team_id, after_team_id. Wenn du Flexibilität bevorzugst, verwende zwei JSON‑Spalten (before, after), aber halte die Inhalte klein und konsistent.
Bei Memberships und Teams ist soft delete meist besser als hard delete. Markiere stattdessen mit Feldern wie deleted_at und deleted_by als deaktiviert. So bleiben Foreign‑Keys intakt und vergangene Zugriffe lassen sich leichter erklären. Hard Deletes können für wirklich temporäre Datensätze (z. B. abgelaufene Einladungen) sinnvoll sein, aber nur wenn du sicher bist, dass du sie später nicht brauchst.
Mit diesem Setup kannst du häufige Compliance‑Fragen schnell beantworten:
- Wer hat Zugriff gewährt oder entfernt, und wann?
- Was genau hat sich geändert (Rolle, Team, Zustand)?
- Wurde Zugriff im Rahmen eines normalen Offboarding‑Flows entfernt?
Schritt für Schritt: Schema in einer relationalen DB entwerfen
Fange einfach an: ein Ort, der aussagt, wer wo gehört und warum. Baue in kleinen Schritten, und füge Regeln hinzu, damit die Daten nicht in „fast korrekt“ abdriften.
Eine praktische Reihenfolge, die in PostgreSQL und anderen relationalen DBs gut funktioniert:
-
Erstelle
organizationsundteams, jeweils mit stabilem Primary Key (UUID oder bigint). Fügeteams.organization_idals Foreign Key hinzu und entscheide früh, ob Team‑Namen innerhalb einer Org einzigartig sein müssen. -
Halte
usersgetrennt von Memberships. Packe Identitätsfelder inusers(email, status, created_at). Packe „gehört zu Org/Team“ in einememberships‑Tabelle mituser_id,organization_id, optionalteam_id(falls du es so modellierst) und einerstate‑Spalte (active, suspended, removed). -
Füge
invitationsals eigene Tabelle hinzu, nicht als Spalte in Membership. Speichereorganization_id, optionalteam_id,email,token,expires_atundaccepted_at. Erzwinge Einzigartigkeit für „eine offene Einladung pro org + email + team“, damit keine Duplikate entstehen. -
Modelle Rollen mit expliziten Tabellen. Ein einfacher Ansatz:
roles(admin, member, etc.) plusrole_assignments, die entweder auf Org‑Scope (keinteam_id) oder Team‑Scope (team_idgesetzt) zeigen. Halte die Vererbungsregeln konsistent und testbar. -
Füge von Anfang an eine Audit‑Spur hinzu. Nutze eine
access_events‑Tabelle mitactor_user_id,target_user_id(oder E‑Mail für Einladungen),action(invite_sent, role_changed, removed),scope(org/team) undcreated_at.
Nachdem diese Tabellen existieren, führe ein paar Admin‑Abfragen aus, um die Realität zu validieren: „Wer hat org‑weiten Zugriff?“, „Welche Teams haben keine Admins?“ und „Welche Einladungen sind abgelaufen, aber noch offen?“ Diese Fragen decken fehlende Constraints früh auf.
Regeln und Constraints, die chaotische Daten verhindern
Ein Schema bleibt übersichtlich, wenn die Datenbank, nicht nur dein Code, Mandanten‑Grenzen durchsetzt. Die einfachste Regel: Jede tenant‑geboardschte Tabelle trägt org_id, und jede Abfrage beinhaltet sie. Selbst wenn jemand in der App einen Filter vergisst, sollte die Datenbank Cross‑Org‑Verbindungen erschweren.
Leitplanken, die die Daten sauber halten
Fange mit Foreign Keys an, die immer „innerhalb derselben Org“ verweisen. Beispiel: Wenn du Team‑Membership separat speicherst, sollte eine team_memberships‑Zeile team_id und user_id referenzieren und ebenfalls org_id tragen. Mit zusammengesetzten Keys kannst du erzwingen, dass das referenzierte Team zur selben Org gehört.
Constraints, die die üblichen Probleme verhindern:
- Eine aktive Org‑Membership pro User und Org: Unique auf
(org_id, user_id)mit partiellem Index für aktive Zeilen (wenn unterstützt). - Eine offene Einladung pro E‑Mail pro Org/Team: Unique auf
(org_id, team_id, email)mit Bedingungstate = 'pending'. - Einladungstokens sind global einzigartig und werden nie wiederverwendet: Unique auf
invite_token. - Team gehört genau einer Org:
teams.org_idNOT NULL mit Foreign Key zuorgs(id). - Beende Memberships statt sie zu löschen: speichere
ended_at(und optionalended_by) für Audit‑Historie.
Indexierung für die Abfragen, die du wirklich machst
Indexiere die Abfragen, die deine App ständig ausführt:
(org_id, user_id)für „in welchen Orgs ist dieser User?“(org_id, team_id)für „Mitglieder dieses Teams auflisten“(invite_token)für „Einladung annehmen“(org_id, state)für „offene Einladungen“ und „aktive Mitglieder“
Halte Org‑IDs veränderbar. Nutze eine unveränderliche orgs.id überall und behandle orgs.name (und Slugs) als editierbare Felder. Ein Umbenennen betrifft dann nur eine Zeile.
Das Verschieben eines Teams zwischen Orgs ist meist eine Policy‑Entscheidung. Die sicherste Option ist, es zu verbieten (oder das Team zu klonen), weil Memberships, Rollen und Audit‑Historie org‑gescoped sind. Wenn du Moves erlaubst, mach es in einer einzigen Transaktion und aktualisiere alle Child‑Zeilen, die org_id tragen.
Um verwaiste Datensätze zu vermeiden, wenn Nutzer gehen, vermeide harte Löschungen. Deaktiviere den User, beende seine Memberships und setze Lösch‑Restriktionen auf Parent‑Zeilen (ON DELETE RESTRICT), es sei denn, du willst wirklich Kaskadenlöschen.
Beispiel‑Szenario: eine Org, zwei Teams, Zugriffsänderungen sicher durchführen
Stell dir eine Firma namens Northwind Co vor mit einer Org und zwei Teams: Sales und Support. Sie stellen eine Auftragnehmerin, Mia, für einen Monat ein, um Support‑Tickets zu bearbeiten. Das Modell sollte vorhersehbar bleiben: eine Person, eine Org‑Membership, optionale Team‑Memberships und klare Zustände.
Eine Org‑Adminin (Ava) lädt Mia per E‑Mail ein. Das System erstellt einen Invitation‑Datensatz, der an die Org gebunden ist, mit Status pending und einem Ablaufdatum. Nichts anderes ändert sich — es gibt keinen „halben User“ mit unklarem Zugriff.
Wenn Mia akzeptiert, wird die Einladung auf accepted gesetzt und eine Org‑Membership mit Zustand active erstellt. Ava setzt Mias Org‑Rolle auf member (nicht admin). Dann fügt Ava eine Support‑Team‑Membership hinzu und weist eine Team‑Rolle wie support_agent zu.
Ein Twist: Ben ist Vollzeit‑Mitarbeiter mit Org‑Rolle admin, soll aber die Support‑Daten nicht sehen. Das lässt sich mit einem team‑spezifischen Override lösen, der seine Team‑Rolle für Support explizit herabstuft, während seine org‑weiten Admin‑Rechte für die Organisationseinstellungen erhalten bleiben.
Eine Woche später verstößt Mia gegen Richtlinien und wird suspended. Statt Zeilen zu löschen, setzt Ava Mias Org‑Membership‑Zustand auf suspended. Team‑Memberships können bestehen bleiben, sind aber wirkungslos, solange die Org‑Membership nicht aktiv ist.
Die Audit‑Historie bleibt sauber, weil jede Änderung ein Event ist:
- Ava hat Mia eingeladen (wer, was, wann)
- Mia hat die Einladung angenommen
- Ava hat Mia dem Support hinzugefügt und
support_agentzugewiesen - Ava hat Bens Support‑Override gesetzt
- Ava hat Mia gesuspended
Mit diesem Modell kann die UI eine klare Zugriffsübersicht anzeigen: Org‑Status (active/suspended), Org‑Rolle, Team‑Liste mit Rollen und Overrides sowie ein „Letzte Zugriffsänderungen“‑Feed, der erklärt, warum jemand Sales oder Support sehen darf oder nicht.
Häufige Fehler und Fallen, die du vermeiden solltest
Die meisten Zugriffsfehler entstehen durch „fast richtig“ Modelle. Das Schema sieht anfangs gut aus, aber Randfälle häufen sich: Re‑Invites, Team‑Verschiebungen, Rollenänderungen und Offboarding.
Eine häufige Falle ist das Vermischen von Einladungen und Memberships in einer Zeile. Wenn du invited und active in demselben Datensatz ohne klare Bedeutung speicherst, stellst du Fragen wie „Ist diese Person Mitglied, wenn sie nie akzeptiert hat?“. Halte Einladungen und Memberships getrennt oder mache die State‑Machine explizit und konsistent.
Ein weiterer häufiger Fehler ist eine einzelne Rollen‑Spalte in der users‑Tabelle und das Thema als erledigt abzuhaken. Rollen sind fast immer scoped (Org‑Rolle, Team‑Rolle, Projekt‑Rolle). Eine globale Rolle erzwingt Hacks wie „User ist Admin für einen Kunden, aber schreibgeschützt für einen anderen“, was Multi‑Tenant‑Erwartungen bricht und Support‑Probleme verursacht.
Fallen, die später weh tun:
- Unbeabsichtigte Cross‑Org‑Team‑Membership (team_id zeigt auf Org A, membership auf Org B)
- Harte Löschungen von Memberships und Verlust der historischen Zugriffsinfo
- Fehlende Unique‑Rules, sodass ein User doppelte Zugriffszeilen bekommt
- Stille Vererbungsstapel (Org‑Admin plus Team‑Member plus Override), sodass niemand erklären kann, warum Zugriff besteht
- „Einladung angenommen“ nur als UI‑Ereignis zu behandeln anstatt als Datenbank‑Tatsache
Ein schnelles Beispiel: Ein Auftragnehmer wird eingeladen, tritt Team Sales bei, wird entfernt und einen Monat später wieder eingeladen. Überschreibst du die alte Zeile, verlierst du die Historie. Erlaubst du Duplikate, kann er zwei aktive Memberships haben. Klare Zustände, scoped Rollen und passende Constraints verhindern beides.
Schnelle Checks und nächste Schritte für die Integration in deine App
Bevor du codest, prüfe dein Modell auf Papier: Macht es auch in komplexeren Fällen noch Sinn? Ein gutes Multi‑Tenant‑Access‑Modell sollte „langweilig“ wirken: dieselben Regeln gelten überall, und Spezialfälle sind selten.
Eine kurze Checkliste, um gängige Lücken zu finden:
- Jede Membership verweist auf genau einen User und eine Org, mit Unique‑Constraint, um Duplikate zu verhindern.
- Invitation‑, Membership‑ und Removal‑Zustände sind explizit (nicht durch NULLs impliziert) und Übergänge sind begrenzt (z. B. kann eine abgelaufene Einladung nicht akzeptiert werden).
- Rollen werden an einem Ort gespeichert und effektiver Zugriff wird konsistent berechnet (inkl. Vererbungsregeln, falls vorhanden).
- Löschen von Orgs/Teams/Users zerstört nicht die Historie (nutze Soft‑Delete oder Archivfelder für Audit‑Trails).
- Jede Zugriffsänderung erzeugt ein Audit‑Event mit Actor, Ziel, Scope, Zeitstempel und Grund/Quelle.
Teste das Design mit realen Fragen. Wenn du sie nicht mit einer Abfrage und einer klaren Regel beantworten kannst, brauchst du möglicherweise einen Constraint oder einen zusätzlichen Zustand:
- Was passiert, wenn ein User zweimal eingeladen wird und dann die E‑Mail geändert wird?
- Kann ein Team‑Admin einen Org‑Owner aus dem Team entfernen?
- Wenn eine Org‑Rolle Zugriff auf alle Teams gewährt, kann ein Team sie überschreiben?
- Wenn eine Einladung nach einer Rollenänderung akzeptiert wird, welche Rolle gilt?
- Wenn Support fragt „Wer hat Zugriff entfernt?“, kannst du das schnell beweisen?
Schreibe auf, was Admins und Support‑Teams wissen müssen: Membership‑Zustände (und was sie auslösen), wer einladen/entfernen kann, was Rollenvererbung in einfachen Worten bedeutet und wo Audit‑Events bei einem Vorfall zu finden sind.
Implementiere zuerst Constraints (Uniques, Foreign Keys, erlaubte Übergänge), baue dann die Business‑Logik darum herum, damit die Datenbank dich in Schach hält. Lege Policy‑Entscheidungen (Inheritance an/aus, Standardrollen, Invite‑Expiry) in Konfigurations‑Tabellen, statt in Code‑Konstanten.
Wenn du das ohne manuelles Backend und Admin‑UI aufbauen willst, kann AppMaster (appmaster.io) dir helfen, diese Tabellen in PostgreSQL zu modellieren und Invite‑/Membership‑Übergänge als explizite Business‑Prozesse umzusetzen, während echter Produktions‑Code generiert wird.
FAQ
Verwende einen separaten Membership‑Eintrag, damit Rollen und Zugriffe an eine Organisation (und optional ein Team) gebunden sind, nicht an die globale User‑Identität. So kann dieselbe Person in einer Organisation Admin und in einer anderen Viewer sein — ohne Hacks.
Behalte sie getrennt: Eine Einladung ist ein Angebot mit E‑Mail, Gültigkeit und Scope, während eine Membership bedeutet, dass der Benutzer tatsächlich Zugriff hat. Das verhindert „Geistermitglieder“, unklare Zustände und Sicherheitsprobleme bei E‑Mail‑Änderungen.
Eine kleine Menge wie active, suspended und removed reicht für die meisten B2B‑Apps. Wenn du den Status invited nur in der invitations‑Tabelle führst, bleiben Membership‑Zeilen eindeutig: sie repräsentieren aktuellen oder früheren Zugriff, nicht ausstehende Zugriffe.
Speichere Organisations‑ und Team‑Rollen als Zuweisungen mit Scope (org‑weit, wenn team_id NULL ist; team‑spezifisch, wenn team_id gesetzt ist). Beim Zugriff auf ein Team gilt: zuerst nach einer teamspezifischen Zuweisung suchen, sonst auf die org‑weite zurückfallen.
Eine einfache, vorhersehbare Regel: Org‑Rollen gelten standardmäßig überall; Team‑Rollen überschreiben nur, wenn sie explizit gesetzt sind. Halte Overrides selten und sichtbar, damit Zugriffsgründe erklärbar bleiben.
Erzwinge „nur eine offene Einladung pro Org/Team pro E‑Mail“ mittels Unique‑Constraint und definiere klare Zustände pending/accepted/revoked/expired. Bei Re‑Invites kannst du die bestehende offene Einladung verlängern oder die alte widerrufen und einen neuen Token ausgeben.
Jede tenant‑bezogene Zeile sollte org_id tragen, und deine Foreign‑Keys/Constraints sollten verhindern, dass Ressourcen verschiedener Orgs vermischt werden (z. B. dass ein Team einer anderen Org zugeordnet ist). So wird der Schaden durch fehlende Filter im Code begrenzt.
Führe ein append‑only Access‑Event‑Log, das festhält, wer was wem wann und in welchem Scope (Org/Team) getan hat. Speichere die relevanten Vorher/Nachher‑Felder (Rolle, Zustand, Team), damit Fragen wie „Wer gab letzten Dienstag Admin‑Rechte?“ zuverlässig beantwortbar sind.
Vermeide harte Löschungen für Memberships und Teams; markiere sie als beendet/deaktiviert, damit die Historie abrufbar bleibt und Foreign‑Keys nicht brechen. Bei Einladungen kannst du sie ebenfalls behalten (auch wenn abgelaufen), um eine vollständige Sicherheitsgeschichte zu haben; mindestens sollten Tokens nicht wiederverwendet werden.
Indexiere deine Hot‑Paths: (org_id, user_id) für Org‑Membership‑Checks, (org_id, team_id) für Team‑Member‑Listen, (invite_token) für Einladungsannahme und (org_id, state) für Admin‑Ansichten wie „aktive Mitglieder“ oder „ausstehende Einladungen“. Indexe sollten reale Abfragen abbilden, nicht jede Spalte.


