Waarom ik dit wilde bouwen
Fluxbase begon als een single-tenant backend, en dat werkte prima voor mijn eigen projecten. Maar ik bleef stuiten op een gemis: self-hosted Supabase ondersteunt geen multi-tenancy, en dat wilde ik Fluxbase wel kunnen bieden. Teams en organisaties die één instance delen met sterke data-isolatie leek me te waardevol om te laten liggen.
De belangrijkste eis was isolatie op PostgreSQL-niveau, niet alleen filtering in de applicatielaag die omzeild kan worden door een bug. En ik wilde de bestaande single-tenant ervaring waar mensen al mee in productie zaten niet breken.
De oplossing bleek een hybride database architectuur
[6]
te zijn, gebouwd op PostgreSQL’s postgres_fdw
[1]
"But postgres_fdw provides more transparent and standards-compliant syntax for accessing remote tables, and can give better performance in many cases."
View source ↗
en Row Level Security
[3]
"When row security is enabled on a table (with ALTER TABLE ... ENABLE ROW LEVEL SECURITY), all normal access to the table for selecting rows or modifying rows must be allowed by a row security policy."
View source ↗
.
Architectuur in het kort
Fluxbase gebruikt een hybride model. Een default tenant deelt de hoofd database: geen aparte database, geen FDW, geen wijzigingen. Named tenants krijgen elk hun eigen PostgreSQL database, maar wel op dezelfde PostgreSQL instance. Dat is belangrijk, want de foreign data wrapper verbindt databases binnen één cluster, niet over aparte servers. Geen netwerk-hop, geen extra authenticatie, en transacties blijven lichtgewicht.
Elke tenant database heeft een eigen public schema voor gebruikersdata, volledig geisoleerd. De tabellen die Fluxbase zelf beheert (authenticatie, storage, edge functions en zo verder) bestaan eenmalig in de hoofd database. Tenant databases benaderen ze via foreign tables, waarbij Row Level Security afdwingt dat elke tenant alleen eigen data ziet.
Geisoleerde gebruikersdata"]:::user FDW1["Foreign tables 🔗
auth.* · storage.* · functions.* · …"]:::foreign end subgraph GLOBEX[" Tenant DB: globex"] PUB2["public.* 📊
Geisoleerde gebruikersdata"]:::user FDW2["Foreign tables 🔗
auth.* · storage.* · functions.* · …"]:::foreign end LOCK["🔒
NOBYPASSRLS"]:::lock end FDW1 -->|"postgres_fdw"| LOCK FDW2 -->|"postgres_fdw"| LOCK LOCK --> AUTH LOCK --> STORE LOCK --> FUNC style PG fill:#f8f9ff,stroke:#d4d8f0,stroke-width:2px,stroke-dasharray:8 4 style MAIN fill:#ffffff,stroke:#4466ee,stroke-width:2px style ACME fill:#ffffff,stroke:#059669,stroke-width:2px style GLOBEX fill:#ffffff,stroke:#7c3aed,stroke-width:2px style MORE_M fill:none,stroke:none,color:#94a3b8 style LOCK fill:#fef2f2,stroke:#ef4444,stroke-width:2px
Backward Compatibility
De default tenant is de sleutel tot backward compatibility. Die gebruikt de hoofd database pool zonder extra bewegende onderdelen. Bestaande single-tenant deployments blijven identiek werken. Geen FDW, geen extra databases, geen configuratiewijzigingen.
Multi-tenancy is opt-in. Je betaalt de overhead pas als je een named tenant aanmaakt. Voor deployments die het nooit nodig hebben verandert er niets.
Hoe een request wordt gerouteerd
Als een request een tenant identificeert, handelt een connection pool router de routering transparant af [7] "Connection pool router die queries routeert naar de juiste database op basis van tenant context." View source ↗ :
Geisoleerde gebruikersdata"]:::local TENANT -->|"infrastructure queries"| FDW["postgres_fdw"]:::fdw FDW -->|"via NOBYPASSRLS role"| RLS["RLS filters
by tenant_id"]:::rls RLS --> RESULT["Scoped results"]:::result
Voor de default tenant gaat alles direct naar de hoofd database. FDW is daar helemaal niet bij betrokken.
postgres_fdw als lijm
Elke named tenant database gebruikt de postgres_fdw extensie om shared infrastructure tabellen uit de hoofd database te benaderen. Vanuit het perspectief van de applicatie zien deze foreign tables eruit en gedragen ze zich als lokale tabellen
[1]
"Now you need only SELECT from a foreign table to access the data stored in its underlying remote table. You can also modify the remote table using INSERT, UPDATE, DELETE, COPY, or TRUNCATE."
View source ↗
.
De setup gebeurt automatisch als een tenant wordt aangemaakt [7] "Tenant manager handelt database aanmaak, FDW setup, role provisioning en schema imports af in een enkele transactie." View source ↗ :
- Maak de tenant database aan: een verse PostgreSQL database binnen dezelfde instance
- Maak een per-tenant FDW role aan op de hoofd database. Deze role wordt aangemaakt met
NOBYPASSRLS[4] "These clauses determine whether a role bypasses every row-level security (RLS) policy. NOBYPASSRLS is the default." View source ↗ , wat betekent dat Row Level Security niet omzeild kan worden - Import foreign schemas [2] "IMPORT FOREIGN SCHEMA creates foreign tables that represent tables existing on a foreign server." View source ↗ : de infrastructure schemas worden geimporteerd als foreign tables die naar de hoofd database wijzen
- Configureer de user mapping. De applicatie verbindt via de per-tenant FDW role, die een standaard tenant identifier heeft ingesteld op role niveau [5] "ALTER ROLE kan configuratie defaults instellen die van toepassing zijn zodra de role een sessie start, inclusief custom settings zoals app.current_tenant_id." View source ↗
Het resultaat: infrastructure tabellen bestaan eenmalig in de hoofd database, maar elke tenant database ziet ze als lokale tabellen. Het NOBYPASSRLS attribuut van de FDW role zorgt dat RLS policies ook via foreign table toegang worden afgedwongen.
WHERE clausules worden pushed
Een logische zorg bij FDW is performance. Gelukkig pushed postgres_fdw WHERE clausules door naar de externe server
[9]
"postgres_fdw attempts to optimize remote queries to reduce the amount of data transferred from foreign servers. This is done by sending query WHERE clauses to the remote server for execution."
View source ↗
. Als een tenant gebruikers opvraagt met een filter, dan gebeurt de filtering op de hoofd database in plaats van alle rijen ophalen en lokaal filteren.
Transactieconsistentie
postgres_fdw beheert externe transacties transparant
[8]
"The remote transaction is committed or aborted when the local transaction commits or aborts."
View source ↗
. Een transactie die zowel de eigen tabellen van de tenant als foreign tables raakt, wordt atomair gecommit of teruggedraaid. Omdat alles op dezelfde PostgreSQL instance draait blijft dit snel, want er is geen coordinatie tussen servers nodig.
Row Level Security
FDW geeft toegang tot shared tabellen. RLS geeft isolatie.
Op elke infrastructure tabel staat Row Level Security ingeschakeld met policies die filteren op een tenant identifier [3] "When row security is enabled on a table (with ALTER TABLE ... ENABLE ROW LEVEL SECURITY), all normal access to the table for selecting rows or modifying rows must be allowed by a row security policy." View source ↗ . De per-tenant FDW role heeft deze identifier als standaard ingesteld, dus queries via FDW worden automatisch gescooped. Geen applicatiecode nodig.
Het defense-in-depth model:
- Superusers en
BYPASSRLSroles slaan RLS over [3] "Superusers and roles with the BYPASSRLS attribute always bypass the row security system when accessing a table." View source ↗ , gereserveerd voor interne operaties zoals migraties - Service roles respecteren RLS en worden gebruikt voor normale request verwerking
- FDW roles worden aangemaakt met
NOBYPASSRLS[4] "NOBYPASSRLS is de standaard. De role kan geen row-level security policy omzeilen op tabellen die hij benadert." View source ↗ , zodat foreign table queries altijd gefilterd worden
Als een bug in de applicatie de verkeerde tenant context meestuurt, voorkomt RLS nog steeds cross-tenant data toegang. De isolatie wordt op database niveau afgedwongen, niet in applicatiecode.
Voordelen
Deze architectuur heeft een aantal praktische voordelen naast isolatie:
Schema migraties eenmalig uitvoeren. Omdat alle infrastructure tabellen in de hoofd database leven en via FDW worden benaderd, hoef ik ze bij een Fluxbase upgrade maar één keer te migreren. Elke tenant ziet automatisch het bijgewerkte schema via de foreign tables. Geen migraties draaien tegen elke afzonderlijke tenant database voor infrastructure wijzigingen.
Gedeeld infrastructure onderhoud. Eén set auth, storage en function tabellen om te monitoren, back-uppen en optimaliseren. Het operationele oppervlak groeit niet mee met het aantal tenants.
Same-cluster performance. Omdat alle databases op dezelfde PostgreSQL instance draaien hebben FDW queries geen netwerk overhead. De foreign data wrapper is gewoon een manier om database grenzen binnen één cluster over te steken.
Transparant voor applicatiecode. Queries zien er hetzelfde uit, of ze nu een lokale tabel of een foreign table raken. De router regelt de verbinding, RLS regelt de filtering, en de applicatie schrijft gewoon SQL.
Het resultaat
De combinatie van postgres_fdw en Row Level Security geeft Fluxbase sterke tenant isolatie op een enkele PostgreSQL instance:
- Data-isolatie: de gebruikersdata van elke tenant leeft in een aparte database
- Gedeelde infrastructure: auth, storage, functions bereikbaar via FDW, gefilterd door RLS
- Backward compatible: bestaande deployments werken ongewijzigd
- Enkele migratie: infrastructure schema wijzigingen propageren automatisch via FDW
Dit alles draait op PostgreSQL alleen. Geen Redis, geen message queues, geen extra services. Gewoon PostgreSQL die doet wat het het beste kan.