4.3 KiB
| title | aliases | tags | sources | created | updated | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FastAPI + MongoDB — Adding Roles with Schema Validator Migration |
|
|
|
2026-04-16 | 2026-04-16 |
FastAPI + MongoDB — Adding Roles with Schema Validator Migration
Adding a new role to a FastAPI + MongoDB application touches three layers: the Python enum, the React frontend routing, and the MongoDB collection schema validator. Missing the MongoDB migration causes the application to reject user creation for the new role at the database level — silently if error handling swallows validation errors.
Key Points
- MongoDB schema validators block inserts/updates with values not in the enum — adding a role to the Python enum alone is insufficient
- FastAPI lifespan is the right place for idempotent seed functions (default admin creation) — runs on every startup, safe to re-run
- When a role mirrors another role's permissions, add it alongside the existing role in all
require_roles()calls and all frontendcasestatements - Use
grep -r "REVIEWER\|reviewer"to find every location a role is referenced before starting — avoids missed locations - 21 occurrences was the count for the REVIEWER→add-LINGUIST change — expect 10-25 occurrences in a medium-sized monorepo
Details
The Three-Layer Change
Layer 1 — Python Enum (backend)
# app/models/user.py
class UserRole(str, Enum):
ADMIN = "admin"
REVIEWER = "reviewer"
LINGUIST = "linguist" # ← new
Layer 2 — FastAPI route guards
# In every endpoint that checked for REVIEWER:
@router.get("/qc-review")
async def qc_review(user = Depends(require_roles([UserRole.REVIEWER, UserRole.LINGUIST]))):
...
Use grep to find all require_roles calls:
grep -rn "require_roles\|UserRole.REVIEWER" app/
Layer 3 — MongoDB migration
# app/migrations/add_linguist_role.py
from app.db import get_db
async def up():
db = await get_db()
await db.command({
"collMod": "users",
"validator": {
"$jsonSchema": {
"properties": {
"role": {
"enum": ["admin", "reviewer", "linguist"] # ← added
}
}
}
}
})
Run once after deploy: python -m app.migrations.migrator up
Idempotent Seed in FastAPI Lifespan
Seeding a default admin user in the lifespan handler ensures it exists on every fresh deploy without requiring a manual step:
# app/main.py
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
await seed_default_admin() # idempotent — no-op if admin exists
yield
async def seed_default_admin():
db = await get_db()
existing = await db.users.find_one({"role": "admin"})
if not existing:
await db.users.insert_one({
"email": "admin@example.com",
"role": "admin",
"hashed_password": hash_password("changeme"),
})
Lifespan runs on startup — safe for production because it checks before inserting.
Frontend Route Mapping (React)
When a role shares all routes with an existing role, add it to every case in the router/sidebar:
// Sidebar.jsx — route visibility
const showQCRoutes = ['reviewer', 'linguist'].includes(user.role);
// Router switch — add alongside existing case
case 'linguist': // falls through to reviewer logic
case 'reviewer':
routes = QC_ROUTES;
break;
A grep -r "case 'reviewer'" frontend/src/ before starting identifies all locations to change.
Related Concepts
- wiki/tech-patterns/_index — FastAPI tech pattern article covers broader FastAPI setup
- wiki/concepts/shell-static-deploy-patterns — the deploy.sh fix that accompanied this role change
- wiki/concepts/monorepo-deploy-script-pitfall — the deploy script bug discovered in the same session
Sources
- daily/2026-04-16.md — LINGUIST role added to Ford QC web app (FastAPI + React monorepo); 21 REVIEWER occurrences found and updated; MongoDB migration written manually; seed_default_admin added to FastAPI lifespan