python
56 lines · 7 steps
Chaining tenant dependencies in FastAPI
A multi-tenant FastAPI app resolves the tenant, authenticates the user, and confirms membership by composing three async dependencies.
Explained by
highlit
1from fastapi import Depends, FastAPI, Header, HTTPException, status
2from sqlalchemy import select
3from sqlalchemy.ext.asyncio import AsyncSession
4
5from .database import get_session
6from .models import Membership, Tenant, User
7from .security import decode_access_token
8
9app = FastAPI()
10
11
12async def resolve_tenant(
13 host: str = Header(...),
14 session: AsyncSession = Depends(get_session),
15) -> Tenant:
16 subdomain = host.split(":")[0].split(".")[0]
17 if subdomain in ("www", "app", ""):
18 raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing tenant subdomain")
19
20 tenant = await session.scalar(
21 select(Tenant).where(Tenant.slug == subdomain, Tenant.is_active.is_(True))
22 )
23 if tenant is None:
24 raise HTTPException(status.HTTP_404_NOT_FOUND, "Unknown tenant")
25 return tenant
26
27
28async def current_user(
29 authorization: str = Header(...),
30 session: AsyncSession = Depends(get_session),
31) -> User:
32 scheme, _, token = authorization.partition(" ")
33 if scheme.lower() != "bearer" or not token:
34 raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid auth header")
35
36 payload = decode_access_token(token)
37 user = await session.get(User, payload["sub"])
38 if user is None:
39 raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found")
40 return user
41
42
43async def tenant_scope(
44 tenant: Tenant = Depends(resolve_tenant),
45 user: User = Depends(current_user),
46 session: AsyncSession = Depends(get_session),
47) -> Membership:
48 membership = await session.scalar(
49 select(Membership).where(
50 Membership.tenant_id == tenant.id,
51 Membership.user_id == user.id,
52 )
53 )
54 if membership is None:
55 raise HTTPException(status.HTTP_403_FORBIDDEN, "Not a member of this tenant")
56 return membership
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1FastAPI dependencies can depend on other dependencies, letting you build authorization from small, reusable, independently testable pieces.
- 2Deriving the tenant from the request Host header keeps tenant selection implicit and out of every route signature.
- 3Raising HTTPException with precise status codes turns each validation stage into a clear, self-documenting failure boundary.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/chaining-tenant-dependencies-in-fastapi-explained-python-7b53/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.