Building a multi-tenant application is often a necessity for many Software as a Service (SaaS) products, where you need to serve multiple clients (tenants) securely and efficiently. These clients might be different companies, organizations, or even different departments within the same company. FastAPI, with its modern and flexible design, is a great choice for putting together such an application. Let’s dive into how one might build a multi-tenant application using FastAPI.
Understanding multi-tenancy is crucial. Essentially, multi-tenancy means that a single application instance serves multiple clients, ensuring that each client’s data is isolated and secure. There are different strategies to achieve this, such as database schema separation, subdomain routing, and unique authentication schemes.
Database Schema Separation
One popular method is to use separate database schemas for each tenant. This keeps each tenant’s data isolated, minimizing the chance of inadvertent data mix-ups. If PostgreSQL is your database of choice, you might use separate schemas for each tenant. Here’s a basic example of how this setup could work using SQLAlchemy and Alembic for database migrations:
from fastapi import FastAPI, Request
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
app = FastAPI()
async def get_db_schema(request: Request):
tenant_id = request.path_params.get("tenant_id")
return f"{tenant_id}_schema"
@app.get("/tenants/{tenant_id}/data")
async def get_tenant_data(request: Request, tenant_id: str):
schema = await get_db_schema(request)
engine = create_engine(f"postgresql://user:password@host:port/dbname?schema={schema}")
Session = sessionmaker(bind=engine)
session = Session()
data = session.query(SomeModel).all() # SomeModel represents your database model
return {"data": data}
In this setup, the schema is dynamically selected based on the tenant identifier from the request path. It keeps the process organized and secure.
Subdomain Routing
Another effective approach is using subdomains to differentiate between tenants. Each tenant has its own subdomain, and the application uses this information to route requests to the appropriate tenant data. Here’s how you can do it in FastAPI:
from fastapi import FastAPI, Request
app = FastAPI()
async def get_tenant_from_subdomain(request: Request):
host = request.headers["host"]
subdomain = host.split(".")[0]
return subdomain
@app.get("/")
async def root(request: Request):
tenant = await get_tenant_from_subdomain(request)
return {"tenant": tenant}
This method leverages the HOST
header in HTTP requests to determine the subdomain and hence, the tenant. It’s efficient and keeps things manageable.
Authentication and Authorization
Authentication and authorization are key aspects of a multi-tenant application. Ensuring that each tenant’s users are properly authenticated and authorized is non-negotiable. This can be handled by using different authentication schemes for each tenant or by verifying specifics within JWT tokens.
If you’re using Azure AD, for instance, you might need to verify the iss
field in the JWT token to accept only specific tenants:
from fastapi import FastAPI, Security
from fastapi_azure_auth import MultiTenantAzureAuthorizationCodeBearer
app = FastAPI()
allowed_issuers = ["https://login.microsoftonline.com/{tenant1_id}/v2.0", "https://login.microsoftonline.com/{tenant2_id}/v2.0"]
auth_scheme = MultiTenantAzureAuthorizationCodeBearer(
allowed_issuers=allowed_issuers,
client_id="your_client_id",
tenant_id="your_tenant_id",
client_secret="your_client_secret",
)
@app.get("/protected")
async def protected_route(request: Request, token: str = Security(auth_scheme)):
return {"message": "Hello, authenticated user!"}
This setup makes sure that access is strictly limited to designated tenants by verifying the issuer in the JWT token.
Dynamic Authentication Schemes
Sometimes, it’s necessary to dynamically load different authentication schemes based on the tenant. This can get a bit complex but ensures each tenant’s users are authenticated correctly. You can achieve this by creating a custom decorator to load the appropriate authentication scheme based on the tenant:
from fastapi import FastAPI, Request, Depends
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
async def get_auth_scheme(request: Request):
tenant_id = request.path_params.get("tenant_id")
if tenant_id == "tenant1":
return OAuth2PasswordBearer(tokenUrl="/tenant1/token")
elif tenant_id == "tenant2":
return OAuth2PasswordBearer(tokenUrl="/tenant2/token")
else:
raise Exception("Unsupported tenant")
@app.get("/tenants/{tenant_id}/protected")
async def protected_route(request: Request, tenant_id: str, token: str = Depends(get_auth_scheme(request))):
return {"message": "Hello, authenticated user!"}
This allows dynamic switching between authentication schemes tailored to each tenant. It’s an advanced but vital strategy for more complex setups.
Wrapping it Up
Building a multi-tenant application with FastAPI involves several key steps. These include database schema separation, subdomain routing, and dynamic authentication schemes. By leveraging these methods and tools, your application can serve multiple clients securely and efficiently.
Whether you opt for separate database schemas, subdomains, or dynamic authentication schemes, FastAPI gives you the flexibility and capabilities needed to build a robust multi-tenant architecture. This approach not only boosts security but also simplifies the management of multiple clients within a single application instance. Making your application scalable, secure, and efficient is entirely achievable with FastAPI.