Multi-Tenant Architecture with FastAPI: Design Patterns and Pitfalls
As the senior technical architect of a multi-SaaS platform, I work closely in the architectures pertaining to multi tenancies and i started to design a backend architecture that could serve multiple tenants (businesses) securely, scalably, and cost-effectively. After exploring several frameworks, I chose FastAPI for its performance, simplicity, and strong async support.
But building a multi-tenant architecture isn’t just about choosing the right framework — it’s about laying down a clean, extensible design that supports isolated tenant data, secure authentication, and operational efficiency.
Get Koushik Sathish’s stories in your inbox
Join Medium for free to get updates from this writer.
Here’s what I’ve learned, the design patterns I followed, and the mistakes I’d avoid if I were starting today.
🧱 What is Multi-Tenancy in SaaS?
Multi-tenancy means a single instance of your application serves multiple customers (tenants), each with their own isolated data and experience. You can implement this using various strategies:
- Shared Database, Shared Schema
- Shared Database, Isolated Schema
- Isolated Database per Tenant
I chose Shared DB with Tenant-Aware Rows, powered by PostgreSQL + Supabase RLS (Row Level Security). It offers a strong balance of simplicity, cost-efficiency, and security for early-stage SaaS platforms.
🧩 Design Patterns I Used
1. Tenant Context Injection (per request)
Each incoming request carries a tenant_id (via JWT claims or headers). In FastAPI, I use a dependency that extracts and validates this tenant_id on every request:
from fastapi import Depends, Request, HTTPException
def get_tenant_id(request: Request):
tenant_id = request.headers.get("X-Tenant-ID")
if not tenant_id:
raise HTTPException(status_code=400, detail="Missing tenant_id")
return tenant_idThis gets injected into route handlers and used in DB queries.
2. Supabase Auth + RLS Enforcement
Instead of manually enforcing tenant-level access in the app, I use Supabase’s RLS to enforce row-level policies inside PostgreSQL. This is secure by design and works like this:
-- Example RLS policy in Supabase
CREATE POLICY "Tenant data only"
ON invoices
FOR SELECT
USING (tenant_id = auth.jwt() ->> 'tenant_id');This drastically reduces bugs from missing WHERE clauses in your backend.
3. Database Models with tenant_id Field
Every tenant-specific model includes a tenant_id. Example with SQLAlchemy:
class Customer(Base):
__tablename__ = "customers"
id = Column(UUID, primary_key=True)
tenant_id = Column(String, index=True)
name = Column(String)
email = Column(String)4. Async Queries with SQLAlchemy 2.x
FastAPI thrives with async, so I use the async SQLAlchemy engine and session:
async with async_session() as session:
result = await session.execute(
select(Customer).where(Customer.tenant_id == current_tenant)
)This keeps things fast and non-blocking.
⚠️ Pitfalls to Watch Out For
❌ 1. Forgetting to Filter by Tenant
Early on, I had endpoints returning all users — not just those belonging to the tenant. You must never trust frontend filtering. Always enforce tenant scoping at the backend and/or DB level.
❌ 2. Hardcoding Tenant Logic
Avoid littering your app with if tenant_id == 'xyz' logic. This makes your code brittle and non-extensible. Instead, generalize your business logic and isolate special cases behind service layers.
❌ 3. Lack of Observability Per Tenant
It’s tempting to log everything in a flat way — but when onboarding multiple customers, per-tenant logs and metrics are essential for debugging and SLA tracking. I now tag every log and metric with tenant_id.
❌ 4. Poor Testing of Multi-Tenant Scenarios
Test cases often assume one tenant. You need tests for:
- Accessing unauthorized data
- Creating conflicting resources across tenants
- Failing gracefully when tenant_id is missing
🛠 Tools That Helped Me
- Supabase Auth for JWT-based identity and tenant claims
- Supabase RLS for secure data filtering
- PostgreSQL for multi-tenant storage
- SQLAlchemy 2.x (async) for modern DB interactions
- Sentry + OpenTelemetry for per-tenant error tracking
🧠 Final Thoughts
Multi-tenancy isn’t a technical afterthought — it’s a foundational decision. FastAPI gave me the flexibility to move fast, while Supabase helped enforce critical boundaries at the database level.
If you’re building a SaaS with multiple customers, start with tenant-awareness baked in. Don’t bolt it on later. And whatever you do — never trust the frontend to handle tenant scoping.
👉 Follow me for more posts on building production-grade SaaS platforms with Python, FastAPI, and modern cloud-native tooling.
Let me know what you’re building. Always happy to geek out on multi-tenant design!








