Server
FastAPI route layer for progression, spells, characters, rebuild workflows, and API-only wiki services.
Server
server/__init__.py defines the FastAPI app and operational endpoints. server/wiki.py defines the engineering wiki API.
This is distinct from the public static wiki in site/: the static site is a documentation frontend, while FastAPI owns the live API contracts and wiki diagnostics.
API Surface Areas
- Progressions: manifest + entity retrieval routes.
GET /api/tools/progression/manifestreturns the full manifest catalog, whileGET /api/tools/progression/{entity_type}/{edition}/{index}returns the manifest entry, generated progression JSON, canonical homebrew record (if any), and level-by-level review document as a composite response. This bundling avoids multiple round-trips when the UI needs to display a full entity view. - Spells: edition-scoped spell catalog route (
GET /api/tools/spells?edition=...) for UI and tooling lookup. The route uses a fallback strategy—if a 2024 spell table doesn't exist, it falls back to the 2014 merged view. This handles the incremental 2024 data availability without requiring the client to negotiate editions. - SRD Level-Up: stateless
/api/tools/srd-level-up/*routes (preview, apply, validate) operate on rawSrdCharacterStatewithout any persistence. The persisted/api/characters/{id}/level-up/*routes wrap the same stateless engine with character identity lookup and snapshot creation. - Characters: create/list/get/delete routes, snapshot history, feature catalog, ASI/feat choice inspection and swapping, and saved decision-plan persistence. All character data lives in
lorewright-app.sqlite, separate from the SRD content database. The decision-plan is a client-managed document that records intended level-up choices before they are applied. The level-up apply route uses optimistic locking oncurrent_levelto prevent concurrent modification conflicts, and the store layer normalizes duplicate snapshot levels so each character retains a single current snapshot per level. - Homebrew: canonical record write path (
PUT /api/tools/homebrew/{entity_type}/{index}). Validates that the payloadindexandtypefields match the URL parameters before writing to disk. The canonical JSON files inhomebrew/are the source of truth for homebrew content. - Reviews: level-by-level review read/write path. Review documents default to all 20 levels at
pendingstatus. On read, stored levels are merged over the default template so that missing levels are always present. This avoids schema migration when new review fields are added. - Rebuild: pipeline execution trigger for entity or all. Always runs
refresh_sqlite_databasefirst to ensure the SQLite source data is current before generating progressions, and returns either the manifest forscope: "all"or the regenerated entity payload forscope: "entity". - Wiki API-only: manifest, rendered pages, health report, source view, commit history, TOC payloads, backlink/source metadata, and stale-source reporting. The wiki router is mounted as an
APIRouterwith the/api/wikiprefix. Frontmatter parsing is implemented with stdlib only (no PyYAML dependency).
See API Contracts for endpoint-level expectations.
Public Static Wiki vs FastAPI Wiki API
site/renders public pages from markdown during the Next.js build and publishes them to Vercel.server/wiki.pyserves/api/wiki/*for local engineering use and automated checks.- API-only wiki capabilities include health, commit history, source metadata/staleness, historical source viewing, and page TOC data.
- Current implementation detail: the static site reads top-level
wiki/*.mdfiles, while the FastAPI wiki API recursively walkswiki/. The docs call out this difference without changing behavior in this pass.
Route Families Worth Keeping in Sync
/api/tools/progression/*and/api/tools/spells/api/tools/homebrew/*,/api/tools/reviews/*, and/api/tools/rebuild/api/tools/srd-level-up/*/api/characters,/api/characters/{id},/api/characters/{id}/snapshots/api/characters/{id}/level-up/*and/api/characters/{id}/decision-plan/api/characters/{id}/feature-catalogand/api/characters/{id}/progression/levels/{level}/asi-feat-*/api/wiki/*
Rebuild Coupling
POST /api/tools/rebuild executes the same pipeline modules used by CLI commands; see Rebuild Runbook. The API handler calls refresh_sqlite_database followed by either generate_all_progressions or generate_selected_progression depending on the request scope. This means a rebuild triggered from the API has the same side effects as running poetry run lorewright db-refresh && poetry run lorewright generate-progressions from the CLI—the only difference is that the API collapses both steps into a single request.
Design Rationale
The server is intentionally a thin routing layer. Route handlers perform argument validation, delegate to engine functions, and translate engine exceptions into HTTP status codes. There is no business logic in server/__init__.py beyond request/response shaping—all domain computation lives in engine/.
The dual-database architecture (5e-database.sqlite for SRD content, lorewright-app.sqlite for character state) reflects a separation of concerns: SRD data is regenerable from source JSON files, while character data is user-created and must be preserved across rebuilds. The server opens SQLite connections per-request rather than maintaining a connection pool, which is appropriate for SQLite's single-writer concurrency model and avoids cross-thread connection sharing issues.
Pydantic models defined inline in server/__init__.py (like RebuildRequestModel, ReviewDocumentModel, SpellCatalogItemModel) are server-specific request/response shapes that don't belong in the engine's schema layer. They exist only to validate API input at the routing boundary.
The choice to serve the wiki API from the same FastAPI app (rather than a separate service) keeps the deployment footprint minimal. Since the wiki API primarily reads files from the wiki/ directory and shells out to git for commit history, it has no shared state or resource contention with the main API routes.
Assumptions & Constraints
- Single-process deployment: The server assumes a single uvicorn process. There is no session affinity, distributed caching, or inter-process coordination. This is sufficient for the current use case (local dev + small team).
- SQLite file existence: Routes that read from
5e-database.sqlitegracefully handle a missing database (returning empty results), but character routes requirelorewright-app.sqliteto exist. The app database is created on first character write. - No authentication: All API routes are unauthenticated. The server is designed for local or trusted-network use only.
- Manifest as index: The progression API relies entirely on
progressions/manifest.jsonto locate entity files. If the manifest is out of sync with the filesystem, routes will return 404 errors even if the progression file exists on disk. - Homebrew write path is file-based:
PUTrequests to the homebrew and review routes write directly to the filesystem. There is no locking or conflict detection—concurrent writes to the same entity will last-write-wins.
Conceptual Model
The server mediates between three distinct storage backends:
- SRD content database (
5e-database.sqlite): read-only from the server's perspective. Populated by the ingest pipeline. Queried for spell catalogs and as input to level-up preview. - Progression artifacts (
progressions/): read-only JSON files generated by the pipeline. Served directly by entity retrieval routes. The manifest acts as an in-memory index loaded on each request. - App state database (
lorewright-app.sqlite): read-write. Owns character records, snapshots, and decision plans. This is the only store with user-created data.
The rebuild route is the only mutation path for stores (1) and (2), and it atomically regenerates both. Store (3) is never affected by rebuilds—character data persists independently.
Test Coverage
Primary API behavior checks are cataloged in Testing Strategy, especially:
tests/test_progression_tool_api.pytests/test_srd_level_up_api.pytests/test_wiki_api.py