RBAC in FastAPI: a clean implementation
How to add role-based access control to a FastAPI backend with dependency injection, without reinventing every endpoint.
The simple case
Most SMB apps have 3–4 roles: admin, member, viewer, and maybe a billing-only role. You do not need a permissions framework. You need a few dependencies and a discipline.
The pattern
from fastapi import Depends, HTTPException
from typing import Annotated
def require_role(*roles: str):
def checker(user: Annotated[User, Depends(current_user)]) -> User:
if user.role not in roles:
raise HTTPException(403, "insufficient permissions")
return user
return checker
AdminUser = Annotated[User, Depends(require_role("admin"))]
MemberUser = Annotated[User, Depends(require_role("admin", "member"))]
Now any endpoint can declare its required role:
@app.get("/admin/users")
async def list_users(user: AdminUser):
return await get_all_users()
The role is enforced by FastAPI's dependency injection before the handler runs.
Why not a permissions matrix
Most "permissions framework" implementations add a Permission model, a Role-Permission join, and a UI to manage it. For SMBs, this is overkill on day one.
Start with roles. If a real customer asks for fine-grained permissions, add them then. Premature flexibility is a tax.
Object-level checks
Role checks are not enough. A member of company A should not see orders from company B. Add object-level scoping inside the handler:
@app.get("/orders/{order_id}")
async def get_order(order_id: int, user: MemberUser):
order = await db.get_order(order_id)
if order.tenant_id != user.tenant_id:
raise HTTPException(404) # 404 not 403 — do not leak existence
return order
Note the 404. Returning 403 tells an attacker that the object exists. For tenant-scoped resources, return 404.
Multi-tenancy as a first-class concern
If your app is multi-tenant, every query should be filtered by tenant ID. Make this a database constraint, not a code convention. Postgres row-level security (RLS) enforces it at the database level. Worth setting up early.
What we will not skip
- Server-side enforcement. Frontend role checks are UX, not security. The backend must enforce every check.
- Audit logging. Every privileged action (create, delete, role change) gets logged with who, when, what, and from where.
These two together are what separates "we have RBAC" from "we have safe RBAC."