Jump to content

Creating Multi-Tenant SaaS APIs with FastAPI and SQLModel

From JOHNWICK

As SaaS (Software-as-a-Service) platforms evolve, supporting multiple clients — aka multi-tenancy — becomes a foundational requirement. But with great power comes complexity: how do you separate data securely, scale effortlessly, and still maintain a clean architecture?

In this guide, we’ll walk through building a multi-tenant SaaS API using FastAPI and SQLModel, exploring strategies like dynamic database routing, tenant isolation, and user-level scoping — all using Python’s modern async stack.

🚀 Why Multi-Tenancy Matters in SaaS If you’re building a B2B SaaS app, chances are each organization (or “tenant”) should:

  • Have their own isolated data
  • Experience no performance issues from other tenants
  • Access their data securely with zero leakage

There are three common approaches to implementing this:

  • Shared Database, Shared Schema
  • Shared Database, Isolated Schemas
  • Isolated Databases per Tenant

We’ll focus on the isolated DB per tenant model, which offers strongest isolation, ideal for handling custom needs and future enterprise clients.


🧱 Tech Stack Overview

  • FastAPI — for high-performance APIs with dependency injection
  • SQLModel — Pydantic + SQLAlchemy = type-safe ORM magic
  • Databases Library (or SQLAlchemy Core) — for async database connections
  • PostgreSQL — recommended for its schema support and maturity


🔁 Dynamic Database Routing Let’s say each tenant has its own DB like tenant_abc, tenant_xyz. The goal is to dynamically connect to the right DB based on the request, ideally using something like a tenant slug in the header or subdomain. ✅ Example Header-Based Tenant Routing

from fastapi import Request, HTTPException, Depends
from sqlmodel import Session
from app.db import get_session_for_tenant

def get_tenant_slug(request: Request) -> str:
    tenant = request.headers.get("X-Tenant")
    if not tenant:
        raise HTTPException(status_code=400, detail="Tenant header missing")
    return tenant

def get_db(tenant: str = Depends(get_tenant_slug)) -> Session:
    session = get_session_for_tenant(tenant)
    if not session:
        raise HTTPException(status_code=404, detail="Tenant DB not found")
    return session

Now any endpoint using Depends(get_db) automatically routes to the correct DB.


🗂 Setting Up Tenant DB Connections Let’s define a simple router that returns a DB session per tenant. You can either pool per-tenant connections or create on-demand.

from sqlmodel import create_engine, Session

TENANT_DBS = {
    "tenant_abc": "postgresql+psycopg2://abc_user:pass@localhost/tenant_abc",
    "tenant_xyz": "postgresql+psycopg2://xyz_user:pass@localhost/tenant_xyz"
}

def get_session_for_tenant(tenant: str) -> Session:
    db_url = TENANT_DBS.get(tenant)
    if not db_url:
        return None
    engine = create_engine(db_url, echo=False)
    return Session(engine)

✅ Best Practice: Cache or pool these connections instead of recreating engines on every request.


🔐 Tenant Isolation and Security Multi-tenancy is a security concern as much as a technical one. Follow these best practices:

  • Never trust client input without validation — validate tenant existence before routing
  • Use UUIDs or slugs for tenant identifiers (avoid auto-increment IDs)
  • Tenant-scoped users and permissions: users must be restricted to their tenant

Example:

def get_current_user(db: Session = Depends(get_db)):
    # Query from tenant's DB, not global DB
    user = db.exec(select(User).where(User.token == "some_token")).first()
    if not user:
        raise HTTPException(status_code=401)
    return user


🔄 Auto-Provisioning Tenants When a new organization signs up, you need to:

  • Create a new database
  • Run migrations (use Alembic!)
  • Seed default data

This can be automated:

import subprocess

def create_tenant_db(tenant_name: str):
    db_name = f"tenant_{tenant_name}"
    subprocess.run(["createdb", db_name])
    subprocess.run(["alembic", "upgrade", "head"])

You can wrap this in a FastAPI admin endpoint or background task.


🔍 Monitoring and Scaling Considerations Multi-tenant APIs can explode in size and traffic. Some tips:

  • Use connection pooling per tenant (via SQLAlchemy or databases)
  • Log tenant activity separately for debugging and analytics
  • Implement rate limiting per tenant
  • Use circuit breakers to isolate failing tenants


📦 Directory Structure Example

app/
├── main.py
├── models/
│   └── user.py
├── tenants/
│   └── router.py
├── db/
│   └── routing.py
└── utils/
    └── security.py

Keep tenant logic centralized. Avoid global session leaks.


✅ Final Thoughts FastAPI + SQLModel makes it easier than ever to implement clean, scalable multi-tenant APIs. With smart use of dependency injection, per-tenant DB sessions, and scoped access control, you can deliver secure, isolated experiences at scale. Whether you’re a solo indie hacker or leading a growing SaaS team, this setup future-proofs your backend for multi-tenant success.

Read the full article here: https://medium.com/@connect.hashblock/creating-multi-tenant-saas-apis-with-fastapi-and-sqlmodel-257061379f4e