LDS Stake Portal

Database

SQLite schema, key tables, KanbanStage derivation, and Alembic migration workflow.

The backend uses SQLite for local development with a schema that is PostgreSQL-compatible. Switching databases requires only a change to the DB_ENGINE environment variable — no code changes.

VariableDefaultDescription
DB_ENGINEsqliteDatabase engine (sqlite or postgresql)
DATABASE_PATH./database.dbPath to the SQLite file (SQLite only)

Key tables

user

ColumnTypeNotes
idintegerPrimary key
emailstringUnique
password_hashstringBcrypt hash — never returned to clients
force_password_resetboolWhen true, user must change password before using the portal
fnamestringFirst name
lnamestringLast name
activeboolSoft-delete flag
phonestringOptional
biostringOptional
profile_imagestringPath or URL to profile photo

calling

ColumnTypeNotes
idintegerPrimary key
namestringUnique
max_slotsintegerMaximum concurrent holders
is_publicboolWhether the calling is visible on public-facing pages
system_definedbooltrue for High Councilor, Stake Presidency, Bishop — cannot be deleted via API

usercalling

ColumnTypeNotes
idintegerPrimary key
user_idintegerFK → user
calling_idintegerFK → calling
slot_numberintegerWhich slot this user occupies (1-indexed)

permissions

ColumnTypeNotes
idintegerPrimary key
foreign_idintegerUser ID or Calling ID, depending on is_calling
is_callingboolfalse = user grant; true = calling grant (inherited by all holders)
scopesintegerBitmask of Permission IntFlag values

ward

ColumnTypeNotes
idintegerPrimary key
namestringWard name
bishop_idintegerFK → usercalling (the bishop's calling assignment row)
start_timefloatMeeting start time in decimal hours (e.g., 9.0 = 9:00 AM, 10.5 = 10:30 AM)

callingproposal

ColumnTypeNotes
idintegerPrimary key
fname / lnamestringPerson being called or released
spouse_namestringOptional
proposed_callingstringFree-text description of the calling
ward_idintegerFK → ward
submitterintegerFK → user (who submitted the proposal)
is_releasebooltrue for releases, false for new callings
submitted_atdatetime

kanbanupdate

ColumnTypeNotes
idintegerPrimary key
proposal_idintegerFK → callingproposal
updater_idintegerFK → user
from_stageintegerPrevious KanbanStage value
to_stageintegerNew KanbanStage value
updated_atdatetime

callingcomment

ColumnTypeNotes
idintegerPrimary key
proposal_idintegerFK → callingproposal
commenter_idintegerFK → user
comment_textstring
created_atdatetime
edited_atdatetimenull if never edited

callingapproval

ColumnTypeNotes
idintegerPrimary key
proposal_idintegerFK → callingproposal
approver_idintegerFK → user
approvedbooltrue = approved, false = rejected
created_atdatetime

usersession

ColumnTypeNotes
idintegerPrimary key
user_idintegerFK → user
token_hashstringSHA-256 hash of the refresh token
ip_addressstringClient IP at time of login
expires_atdatetimeAfter this time the session is invalid

KanbanStage derivation

KanbanStage is not a column on callingproposal. The current stage of a proposal is derived from the to_stage value of its most recent kanbanupdate row.

There is no single source-of-truth column — you must query the latest KanbanUpdate to determine where a proposal currently sits in the pipeline.

ValueStageNotes
0SP_APPROVALStarting stage for new callings
1HC_APPROVALAuto-advances after SP approval threshold is met
2INTERVIEWStarting stage for releases (skips 0–1)
3SUSTAINAfter interview
4SET_APARTAfter sustaining (callings only; releases skip this stage)
5LCR_UPDATEAfter set apart
6DONETerminal stage

Auto-advance thresholds are controlled by the SP_APPROVAL_THRESHOLD and HC_APPROVAL_THRESHOLD environment variables.


Alembic migrations

Always run Alembic commands from the backend/ directory.

uv run alembic -c alembic.ini revision --autogenerate -m "add my column"
uv run alembic -c alembic.ini upgrade head
uv run alembic -c alembic.ini downgrade -1
uv run alembic -c alembic.ini history --verbose

Autogenerate compares the current SQLModel models against the current database schema. Make sure your model changes are saved before running revision --autogenerate.


Test isolation

Tests override the get_session() FastAPI dependency with a session bound to a temporary SQLite file created at test start and deleted at test end. The real database.db is never touched during a test run.

This means migrations do not need to be applied before running tests — the test database is created fresh from SQLModel metadata each time.

# Run all tests
uv run pytest

# Run a single test file
uv run pytest src/tests/test_users.py

# Run a single test
uv run pytest src/tests/test_auth.py::test_login_sets_cookie_and_returns_token

On this page