Multitenancy#
This page explains how to implement Multi-Tenancy in a FastAPI project - A Practical Guide. When building software solutions that serve multiple organizations or clients, the need for a multi-tenant architecture arises. This allows a single instance of the application to serve different customers, while keeping their data isolated.
In FastAPI, multi-tenancy can be implemented using different strategies for database isolation and schema management. This guide will walk you through how to implement multi-tenancy in FastAPI, focusing on the Single Database, Multiple Schemas approach.
π New to App-Generator? Sign IN with GitHub or Generate Web Apps in no time (free service).
Weβll use dynamic database configuration, schema separation, and API endpoints to provide an isolated environment for each tenant.
Project Structure#
Here is the recommended structure for the project:
multi_tenant_fastapi/
βββ app/
β βββ __init__.py
β βββ main.py # Main application entry point
β βββ database.py # Database connection and schema creation
β βββ schemas.py # Pydantic response models
β βββ base_models.py # Common mixin classes for models
β βββ tenant_a/
β β βββ __init__.py
β β βββ models.py # Tenant-specific models
β βββ tenant_b/ # Additional tenant (optional)
β β βββ __init__.py
β β βββ models.py
β βββ routers/
β βββ __init__.py
β βββ tenant.py # API routing logic
βββ .env # Environment variables
βββ requirements.txt # Dependencies
βββ README.rst # Documentation
Strategies for Data Isolation#
Multiple Databases: Each tenant has its own database. While this offers the highest isolation, it requires managing multiple database connections and can be more resource-intensive.
Single Database, Shared Schema: All tenants share the same database and schema. Data isolation is achieved through tenant-specific identifiers. This is efficient but requires careful query design to ensure data separation.
Single Database, Multiple Schemas: Each tenant has its own schema within a shared database. This is a balanced approach, providing a high level of data separation without the overhead of managing multiple databases.
For this guide, we will implement the Single Database, Multiple Schemas strategy.
Multi-Tenancy Implementation Steps#
1. Setting Up the Database#
Weβll use SQLAlchemy for ORM-based interactions with the PostgreSQL database. To begin, youβll need to configure the database connection in your FastAPI app.
File: app/database.py
import os
from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import CreateSchema
from dotenv import load_dotenv
load_dotenv()
# Database connection configuration
db_host = os.getenv("DB_HOST")
db_user = os.getenv("DB_USER")
db_password = os.getenv("DB_PASSWORD")
db_name = os.getenv("DB_NAME")
db_port = int(os.getenv("DB_PORT", 5432))
SQLALCHEMY_DATABASE_URL = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def create_schema_if_not_exists(schema_name: str):
"""Create schema if not already present."""
with engine.connect() as conn:
conn.execute(CreateSchema(schema_name, if_not_exists=True))
conn.commit()
2. Define a Dynamic Base Class for Each Tenant#
Each tenant will have their own schema, so we need to dynamically generate a base class tied to the tenantβs schema. This will allow tenant-specific models to inherit from it.
File: app/database.py
def get_base(tenant_name: str):
"""Get a base class tied to a tenant's schema."""
create_schema_if_not_exists(tenant_name)
metadata = MetaData(schema=tenant_name)
return declarative_base(metadata=metadata)
3. Defining Tenant-Specific Models#
We now define tenant-specific models like `Product` and `Order`. These models will use the schema of the tenant to ensure data isolation.
File: app/tenant_a/models.py
from base_models import ProductMixin, OrderMixin
from database import get_base
Base = get_base('tenant_a')
class Product(Base, ProductMixin):
__tablename__ = 'products'
class Order(Base, OrderMixin):
__tablename__ = 'orders'
4. Creating Mixins for Common Model Fields#
We define mixins to avoid repeating common fields across multiple models. These mixins will be inherited by tenant-specific models.
File: app/base_models.py
from sqlalchemy import Column, Integer, String, DateTime, Float
class ProductMixin:
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(String)
price = Column(Float)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
class OrderMixin:
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, index=True)
quantity = Column(Integer)
total_price = Column(Float)
order_date = Column(DateTime, default=datetime.datetime.utcnow)
5. Creating the Tables Dynamically#
We use SQLAlchemyβs metadata.create_all() method to create the necessary tables in the tenantβs schema. Hereβs how you can create the tables for tenant_a.
File: app/main.py
from tenant_a.models import Base as TenantABase
from database import engine
TenantABase.metadata.create_all(bind=engine)
6. Handling Tenant-Specific Routing#
In FastAPI, we need to route requests dynamically based on the tenant. This can be done by using headers to identify the tenant and then loading the appropriate models.
File: app/routers/tenant.py
from fastapi import APIRouter, Depends, Header
from sqlalchemy.orm import Session
from database import get_db
from tenant_a.models import Product, Order
from schemas import Domain # Enum for domain names
router = APIRouter()
@router.get("/products")
def get_all_products(db: Session = Depends(get_db), domain: Domain = Header(None)):
# Based on the domain header, select the tenant's schema
tenant = get_tenant_model(domain)
products = db.query(tenant.Product).all()
return products
@router.get("/orders")
def get_all_orders(db: Session = Depends(get_db), domain: Domain = Header(None)):
tenant = get_tenant_model(domain)
orders = db.query(tenant.Order).all()
return orders
Summary#
In this guide, we implemented multi-tenancy in FastAPI using the Single Database, Multiple Schemas strategy. By dynamically generating models and schemas for each tenant, we ensured that data is isolated while leveraging the same codebase.
FastAPIβs powerful dependency injection system, SQLAlchemy ORM, and Pydantic models make it easy to manage multi-tenancy effectively.
This approach is scalable, allowing you to easily add new tenants and manage tenant-specific data without altering the core application logic. Whether youβre building SaaS platforms or managing data for multiple clients, multi-tenancy provides a flexible solution for serving multiple users while maintaining clean data isolation.
Links#
π New to App-Generator? Join our 10k+ Community using GitHub One-Click SignIN.
π
Download
products and start fast a new projectπ Bootstrap your startUp, MVP or Legacy project with a custom development sprint