Backend
FastAPI service conventions: routers, the CallingUser dependency, database sessions, and startup sequence.
The backend is a FastAPI application (Python 3.11+) that owns all business logic, authentication, and database access. It runs on port 8000. Source lives under backend/src/.
backend/
├── main.py # Entry point (uvicorn, env setup, .env loading)
└── src/
├── app.py # FastAPI instance, CORS, lifespan hooks, router registration
├── models/ # SQLModel ORM models (one file per domain)
├── routers/ # FastAPI route handlers (one file per domain)
├── db/
│ ├── orm.py # Singleton ORM + get_session() dependency
│ └── engines/sqlite_engine.py
└── utils/ # security.py, permissions.py, password hashing, background tasksAll Python commands must use uv run. Do not use python, pip, venv, or conda directly.
Router-per-domain pattern
Each business domain has its own file in backend/src/routers/. Routers are registered in app.py with a URL prefix. The current routers are:
| File | Prefix | Notes |
|---|---|---|
auth.py | /auth | Login, refresh, logout, /me |
users.py | /users | User CRUD, password change, photo upload |
callings.py | /callings | Calling CRUD, slot assignment |
assignments.py | /assignments | High Council assignment CRUD |
speaking.py | /speaking | Speaking calendar and topics |
calling_kanban.py | /calling-kanban | Proposals, comments, approvals, board view |
ward.py | /wards | Ward list and detail |
presidency.py | /presidency-assignments | Stake Presidency assignment responsibilities and ward oversight |
temple_config.py | /temple-config | Temple recommend appointment configuration (singleton) |
appointment_types.py | /appointment-types | Appointment type CRUD |
appointment_availability.py | /appointment-availability | Interviewer availability windows and exceptions |
appointment_bookings.py | /appointment-bookings | Public booking, confirmation, reschedule, cancel, and admin views |
building_reservation.py | /reservations | Public reservation submission, admin approve/deny |
settings.py | /settings | Site settings (stake name, logo, hero image, contact info, nav visibility) |
health.py | — | Health check endpoint |
Adding a new router
# backend/src/routers/my_domain.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/")
def list_items():
...# backend/src/app.py
from src.routers.my_domain import router as my_domain_router
app.include_router(my_domain_router, prefix="/my-prefix")CallingUser dependency
CallingUser (in backend/src/utils/security.py) is the FastAPI dependency used to authenticate and authorize route handlers. Inject it with Depends().
from src.utils.security import CallingUser
from src.models.permissions import Permission
@router.get("/protected")
def protected_route(current_user = Depends(CallingUser())):
return {"user_id": current_user.id}Constructor options
| Option | Type | Effect |
|---|---|---|
require_fresh | bool | When True, rejects tokens obtained via /auth/refresh. Use on the password-change endpoint to ensure the user has recently re-authenticated. |
api_safe | bool | When True, returns a ResponseSafeUser that omits password_hash. Always use this on endpoints that return user data to clients. |
permissions | list[Permission] | Checks that the authenticated user holds all listed permission flags. Returns 403 if any flag is missing. |
allow_unchanged_password | bool | When True, bypasses the force_password_reset guard. Use on endpoints that must remain accessible before a forced password change (e.g., /auth/me). |
allow_anonymous | bool | When True, returns None instead of raising 401 when no token is present. Use for endpoints that are public but can optionally personalize for logged-in users. |
get_session() dependency
get_session() (in backend/src/db/orm.py) yields a SQLModel Session. All database operations must go through this session — do not create sessions manually.
from sqlmodel import Session
from src.db.orm import get_session
@router.get("/items")
def list_items(session: Session = Depends(get_session)):
return session.exec(select(Item)).all()In tests, get_session is overridden to use a temporary SQLite file so the real database is never touched. See Database for details.
Startup sequence
The lifespan hook in app.py runs these steps in order when the server starts:
- Initialize ORM — sets up the SQLModel engine and creates tables if they do not exist.
create_default_admin_user()— creates the initial admin user only when the database is empty. Requires theINITIAL_ADMIN_PASSWORDenvironment variable. The admin'sforce_password_resetflag is set totrueso the first login forces a password change.create_system_callings_and_assignments()— ensures the system-defined callings exist: High Councilor, Stake Presidency, and Bishop. These callings cannot be deleted through the API (system_defined=True).load_wards()— readsbackend/wards.csv(format:name,start_hour) and createsWardrows for any ward that does not already exist. Existing rows are not modified.upsert_temple_recommend_config()— ensures the singletonTempleRecommendConfigrow exists with sensible defaults. Safe to run on every start.upsert_site_settings()— ensures the singletonSiteSettingsrow exists with default values (stake name, hero text, etc.). Safe to run on every start.create_default_appointment_types()— seeds the initial appointment types (e.g., Temple Recommend, Limited Use Recommend) if none exist.pre_populate_fast_sunday_exceptions()— adds availability exceptions for upcoming Fast Sundays so the booking calendar blocks those days automatically.validate_email_credentials()— checks that the configured email provider credentials are valid and logs a warning if they are missing or rejected.- Background tasks — starts five async background loops:
- Session cleanup — periodically deletes expired
usersessionrows. - Speaking assignment cleanup — removes stale auto-generated speaking assignments.
- Backup loop — periodically creates database backups and notifies via Discord if configured.
- Booking expiry loop — expires unconfirmed appointment bookings that have passed their confirmation window.
- Appointment reminder loop — sends 24-hour email reminders to members with upcoming confirmed appointments.
- Session cleanup — periodically deletes expired
INITIAL_ADMIN_PASSWORD is only needed on the very first launch when the database is empty. After the admin user is created, the variable is ignored. Do not leave it set in production after the initial setup.
Running the backend
# First launch (creates admin user)
INITIAL_ADMIN_PASSWORD=admin123 uv run python main.py
# Normal start
uv run python main.py
# Dev with auto-reload
uv run python -m uvicorn src.app:app --reload --host localhost --port 8000Swagger UI is available at http://localhost:8000/docs in development (when DEV=true).