obsidian/wiki/concepts/fastapi-mongodb-role-migration.md
2026-04-18 22:04:01 +01:00

4.3 KiB

title aliases tags sources created updated
FastAPI + MongoDB — Adding Roles with Schema Validator Migration
fastapi-role-enum
mongodb-role-migration
fastapi-lifespan-seed
mongodb-validator-migration
fastapi
mongodb
roles
migration
python
backend
daily/2026-04-16.md
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 frontend case statements
  • 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.

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