Bootcamp backendontwikkeling met FastAPI · Les

Strategieën en afwegingen voor tenantisolatie

Vergelijk isolatiemodellen met een gedeeld schema, een schema per tenant en een database per tenant voor SaaS-workloads.

Les 1 van 413 stappen

Strategieën en afwegingen voor tenantisolatie is een gratis Bootcamp backendontwikkeling met FastAPI-les op CoddyKit. Dit is les 1 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met FastAPI. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Waarom tenantisolatie belangrijk is

In een multi-tenant SaaS delen veel klanten (tenants) één draaiende FastAPI-applicatie. De centrale vraag is: hoe goed zijn de gegevens van elke tenant geïsoleerd?

Isolatie heeft invloed op vier zaken waartussen je voortdurend moet afwegen:

  • Beveiliging en impactradius — kan een bug rijen van Tenant A laten uitlekken naar Tenant B?
  • Kosten — hoeveel infrastructuur gebruikt elke tenant?
  • Operationele complexiteit — migraties, back-ups en herstelbewerkingen.
  • Aanpassing per tenant — kan één tenant extra kolommen of een aangepast schema krijgen?

Er bestaan drie canonieke modellen: shared-schema, schema-per-tenant en database-per-tenant. In de rest van deze les vergelijken we ze voor FastAPI-belastingen.

Shared-schema: één tabel met een tenant_id-kolom

Het eenvoudigste model: de rijen van elke tenant staan in de zelfde tabellen en worden onderscheiden door een kolom tenant_id. Elke query moet hierop filteren.

Dit is de goedkoopste en best schaalbare optie voor duizenden kleine tenants, maar de isolatie is puur logisch — één ontbrekende WHERE tenant_id = ... laat gegevens tussen tenants uitlekken.

Hieronder zie je de gebruikelijke vorm van een SQLAlchemy-model. Let op de geïndexeerde tenant_id in elke tabel die eigendom is van een tenant.

from sqlalchemy import String, Integer, ForeignKey, Index
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class Invoice(Base):
    __tablename__ = "invoices"
    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[str] = mapped_column(String, index=True)
    amount_cents: Mapped[int] = mapped_column(Integer)
    customer_email: Mapped[str] = mapped_column(String)

    # Composite index: nearly every query filters by tenant first
    __table_args__ = (Index("ix_invoices_tenant_id", "tenant_id"),)

De tenant uit het verzoek bepalen

Voordat er een query wordt uitgevoerd, moet je weten bij welke tenant het verzoek hoort. Veelgebruikte strategieën zijn:

  • Subdomein — acme.app.com → tenant acme.
  • JWT-claim — het access token bevat een tenant_id.
  • Header — X-Tenant-ID (intern of tussen services).

In FastAPI wordt dit een dependency die de tenant één keer bepaalt en valideert en deze daarna overal injecteert. Vertrouw nooit op een tenant-id die de client vrij kan instellen, tenzij deze cryptografisch is gebonden (bijvoorbeeld in een ondertekende JWT).

from fastapi import Depends, HTTPException, Request

async def get_current_tenant(request: Request) -> str:
    host = request.headers.get("host", "")
    sub = host.split(".")[0]
    if not sub or sub in {"www", "app"}:
        raise HTTPException(status_code=400, detail="Tenant could not be resolved")
    return sub

# Usage in a route:
# @app.get("/invoices")
# async def list_invoices(tenant_id: str = Depends(get_current_tenant)):
#     ...

Het gevaar van shared-schema: het filter vergeten

Het grootste risico bij shared-schema is dat een ontwikkelaar WHERE tenant_id = :tenant vergeet. De query slaagt nog steeds — alleen retourneert deze de gegevens van iedereen.

Twee verdedigingslagen schalen beter dan discipline:

  • Een repositorylaag die altijd tenant_id toevoegt, zodat routecode geen onbewerkte query zonder bereik kan uitvoeren.
  • Postgres Row-Level Security (RLS) als door de database afgedwongen vangnet (dit behandelen we hierna).

Hier garandeert een dunne repository dat het bereik op applicatieniveau wordt ingesteld.

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

class InvoiceRepository:
    def __init__(self, session: AsyncSession, tenant_id: str):
        self.session = session
        self.tenant_id = tenant_id

    async def list(self):
        # tenant_id is ALWAYS applied — callers cannot bypass it
        stmt = select(Invoice).where(Invoice.tenant_id == self.tenant_id)
        result = await self.session.execute(stmt)
        return result.scalars().all()

    async def get(self, invoice_id: int):
        stmt = select(Invoice).where(
            Invoice.id == invoice_id,
            Invoice.tenant_id == self.tenant_id,
        )
        return (await self.session.execute(stmt)).scalar_one_or_none()

Door de database afgedwongen isolatie met Postgres RLS

Met Row-Level Security kan Postgres zelf rijen weigeren die niet bij de huidige tenant horen — zelfs als de applicatie het filter vergeet. Je stelt per verzoek een sessievariabele in en het beleid gebruikt deze.

Hierdoor verandert shared-schema van "hoop dat de WHERE aanwezig is" in "de database garandeert het". De afweging: elke verbinding moet de variabele instellen, wat zorgvuldig moet worden afgestemd op connection pooling (stel deze per transactie in).

-- One-time DDL
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.current_tenant', true));

-- Per request / per transaction, the app runs:
-- SET LOCAL app.current_tenant = 'acme';
-- Now SELECT * FROM invoices only returns acme's rows automatically.

RLS koppelen aan een FastAPI-verzoek

Om RLS te laten werken, opent elk verzoek een transactie, voert het SET LOCAL app.current_tenant uit en voert het vervolgens alle queries daarin uit. SET LOCAL is gebonden aan de transactie, zodat een verbinding uit de pool de waarde niet laat uitlekken naar de volgende tenant.

Dit patroon combineert een applicatiedependency (de tenant bepalen) met handhaving door de database (RLS-beleid) — gelaagde beveiliging.

from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

async def tenant_session(
    tenant_id: str = Depends(get_current_tenant),
) -> AsyncSession:
    async with SessionLocal() as session:
        async with session.begin():
            # bind param avoids SQL injection of the tenant value
            await session.execute(
                text("SET LOCAL app.current_tenant = :t"),
                {"t": tenant_id},
            )
            yield session

Schema-per-tenant: dezelfde database, aparte naamruimten

Bij schema-per-tenant bevat één Postgres-database veel schema's — tenant_acme.invoices, tenant_globex.invoices. De tabellen zijn identiek, maar fysiek van elkaar gescheiden door het schema.

Voordelen: sterkere isolatie dan shared-schema, geen kolom tenant_id nodig, back-ups en herstel per tenant zijn eenvoudiger en je kunt een tenant verwijderen door een schema te verwijderen.

Nadelen: migraties moeten in elk schema worden uitgevoerd (N keer) en Postgres presteert slechter bij zeer veel schema's en tabellen (duizenden schema's maken de catalogus groter). Het meest geschikt voor tientallen tot enkele honderden grotere tenants.

Queries routeren op basis van schema (search_path)

Postgres bepaalt niet-gekwalificeerde tabelnamen met behulp van search_path. Per verzoek stel je dit in op het schema van de tenant, waarna dezelfde ORM-modellen de tabellen van die tenant lezen en beschrijven.

Gebruik net als bij RLS SET LOCAL search_path binnen een transactie, zodat een verbinding uit de pool het schema van de ene tenant nooit meeneemt naar een verzoek van een andere tenant.

from sqlalchemy import text

async def schema_scoped_session(
    tenant_id: str = Depends(get_current_tenant),
):
    schema = f"tenant_{tenant_id}"
    async with SessionLocal() as session:
        async with session.begin():
            # quote_ident-style guard: validate before interpolating identifiers
            if not tenant_id.isalnum():
                raise HTTPException(400, "Invalid tenant identifier")
            await session.execute(text(f'SET LOCAL search_path TO "{schema}", public'))
            yield session

Migraties in veel schema's

De operationele tol van schema-per-tenant bestaat uit migraties. Eén Alembic-upgrade moet op elk tenantschema worden toegepast. Je loopt de lijst met tenants langs, stelt het schema in en voert de migratie uit.

Plan op gedeeltelijke fouten: als schema 200 van 300 mislukt, heb je idempotente migraties nodig die kunnen worden hervat. Dit is de belangrijkste reden waarom teams schema-per-tenant beperken tot honderden en niet duizenden tenants.

# Conceptual loop run by an Alembic env.py or a management command
tenant_schemas = ["tenant_acme", "tenant_globex", "tenant_initech"]

def run_migrations_for_all(connection, run_one):
    failures = []
    for schema in tenant_schemas:
        try:
            connection.execute(f'SET search_path TO "{schema}"')
            run_one(connection)  # apply the same upgrade per schema
        except Exception as exc:  # noqa: BLE001
            failures.append((schema, str(exc)))
    if failures:
        raise RuntimeError(f"Migration failed for: {failures}")

Database-per-tenant: maximale isolatie

Met database-per-tenant krijgt elke tenant een eigen database (soms zelfs een eigen server). De isolatie is zo sterk mogelijk: aparte verbindingen, aparte back-ups en zelfs aparte regio's voor naleving van regels rond gegevenslocatie.

Voordelen: een harde beveiligingsgrens, eenvoudig herstel per tenant, gemakkelijke isolatie van een "lawaaierige buur" en eenvoudige verwijdering van gegevens per tenant (verwijder de database).

Nadelen: de hoogste kosten en operationele overhead — je beheert een connection pool per database, kunt niet eenvoudig analyses over tenants heen uitvoeren en het onboarden van een tenant betekent dat je een database moet inrichten. Het meest geschikt voor weinig, grote tenants met strenge nalevingsvereisten (bijvoorbeeld zakelijke B2B-klanten).

Verbindingen routeren voor database-per-tenant

De applicatie houdt een register bij dat tenants aan database-URL's koppelt en een cache met engines en pools. Een dependency bepaalt de tenant, zoekt de DSN op en geeft een sessie terug die aan die database is gebonden.

Het cachen van engines is essentieel: bij elk verzoek een nieuwe engine maken put de verbindingen uit. Cache één engine per tenant en hergebruik de bijbehorende pool.

from functools import lru_cache
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

TENANT_DSNS = {
    "acme": "postgresql+asyncpg://app@db-acme/acme",
    "globex": "postgresql+asyncpg://app@db-globex/globex",
}

@lru_cache(maxsize=256)
def engine_for(tenant_id: str):
    dsn = TENANT_DSNS.get(tenant_id)
    if dsn is None:
        raise HTTPException(404, "Unknown tenant")
    return create_async_engine(dsn, pool_size=5, max_overflow=2)

async def db_per_tenant_session(tenant_id: str = Depends(get_current_tenant)):
    maker = async_sessionmaker(engine_for(tenant_id), expire_on_commit=False)
    async with maker() as session:
        yield session

Korte controle: een isolatiemodel kiezen

Een B2B-start-up verwacht in het eerste jaar 5.000 kleine tenants onboarden. Ze willen de laagste infrastructuurkosten en het eenvoudigste migratieproces en zijn bereid te investeren in strikt scopen van queries plus een door de database afgedwongen vangnet. Welk isolatiemodel past het best?

Samenvatting: de juiste isolatiestrategie kiezen

Drie modellen op een schaal van goedkoop en gedeeld naar duur en geïsoleerd:

  • Shared-schema — één kolom tenant_id. Het goedkoopst, schaalbaar tot duizenden kleine tenants, één migratie. Risico: uitsluitend logische isolatie; beperk dit risico met een repository die het bereik instelt plus RLS.
  • Schema-per-tenant — voor elke tenant één schema via search_path. Sterkere isolatie, eenvoudig back-up maken en verwijderen per tenant. Kosten: migraties worden N keer uitgevoerd; geschikt tot honderden tenants.
  • Database-per-tenant — één database per tenant, met een engine die per tenant wordt gecachet. De sterkste isolatie en controle over gegevenslocatie. Kosten: de hoogste operationele en infrastructuurkosten; het meest geschikt voor weinig grote tenants met strenge nalevingsvereisten.

Factoren voor de keuze: het aantal en de omvang van tenants, vereisten voor naleving en gegevenslocatie, tolerantie voor migraties en budget. Veel echte systemen zijn hybride — shared-schema voor de lange staart van kleine klanten en database-per-tenant voor zakelijke accounts. In FastAPI is de scheidslijn in alle drie hetzelfde: een dependency voor het bepalen van de tenant die de juiste sessie injecteert.

Gratis beginnen

Leer Bootcamp backendontwikkeling met FastAPI met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
21
Lessen
84

Veelgestelde vragen

Is de les “Strategieën en afwegingen voor tenantisolatie” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Bootcamp backendontwikkeling met FastAPI, waaronder “Strategieën en afwegingen voor tenantisolatie”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Wat leer ik in “Strategieën en afwegingen voor tenantisolatie”?

Vergelijk isolatiemodellen met een gedeeld schema, een schema per tenant en een database per tenant voor SaaS-workloads. Je oefent met Bootcamp backendontwikkeling met FastAPI door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met FastAPI te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met FastAPI op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 1 van 4.

Hoe lang duurt de les “Strategieën en afwegingen voor tenantisolatie”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met FastAPI?

Ja. Elke les over Bootcamp backendontwikkeling met FastAPI bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Strategieën en afwegingen voor tenantisolatie
  2. Tenantcontextresolutie en middleware
  3. Beveiliging en partitionering op rijniveau
  4. Gebruiksmeting, quota's en billinghooks
← Terug naar Bootcamp backendontwikkeling met FastAPI