Creating Multi-Tenant SaaS APIs with FastAPI and SQLModel
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