Files
srbhr--resume-matcher/docs/agent/architecture/backend-guide.md
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

5.2 KiB

Backend Guide

Lean, local-first FastAPI app for resume tailoring.

Tech Stack

Component Technology
Framework FastAPI
Database SQLite (SQLAlchemy 2.0 async + aiosqlite)
AI LiteLLM (100+ providers)
Doc Parsing markitdown
Validation Pydantic
Key encryption Fernet (cryptography)

Directory Structure

apps/backend/app/
├── main.py         # Entry point (lifespan: TinyDB→SQLite import, legacy-key fold-in)
├── config.py       # Settings from env/file; encrypted API-key read/write
├── crypto.py       # Fernet encrypt/decrypt for API keys at rest
├── database.py     # Async SQLAlchemy/SQLite facade (returns plain dicts)
├── models.py       # SQLAlchemy declarative Base + ORM models
├── db_engine.py    # SQLite engine/session factories (async + sync) + PRAGMAs
├── llm.py          # Multi-provider LLM
├── routers/        # health, config, resumes, jobs, applications, enrichment
├── services/       # parser, improver, cover_letter
├── schemas/        # Pydantic models (models.py, applications.py)
├── scripts/        # migrate_tinydb_to_sqlite.py (one-time importer)
└── prompts/        # templates.py

Database Operations

database.py is an async Database facade (global db singleton). Methods keep the same names/signatures as the old TinyDB wrapper but return plain dicts (never ORM rows). ORM models are in models.py; engine plumbing is in db_engine.py.

await db.create_resume(content, content_type, filename, is_master, processed_data)
await db.get_resume(resume_id)  dict | None
await db.list_resumes()  list[dict]
await db.update_resume(resume_id, updates)
await db.delete_resume(resume_id)  bool
await db.set_master_resume(resume_id)            # Exactly one master allowed
await db.create_job(content, resume_id)
await db.create_application(...) / list_applications / bulk_update_applications
get_api_key_ciphertexts() / replace_api_keys(...)  # sync; encrypted api_keys table

Tables: resumes, jobs, improvements, applications, api_keys (encrypted). DB file: data/resume_matcher.db.

Two engines, one file: a module-level async engine serves the document tables + applications; a sync engine serves the encrypted api_keys table (read on the synchronous LLM hot path). Both apply PRAGMAs journal_mode=WAL, foreign_keys=ON, busy_timeout. The single-master invariant is held by an asyncio.Lock plus a partial unique index. Jobs' dynamic pipeline fields (preview_hash(es), job_keywords, company/role) live in a metadata_json JSON column, flattened on read.

Encrypted API keys & migration

  • Keys (crypto.py): Fernet-encrypted, per-provider, in the api_keys table. Secret at data/.secret_key (chmod 600, gitignored, atomic write; plaintext only in memory). config.py injects decrypted keys at read time and strips them on save, so secrets never reach config.json. Set via POST /config/api-keys; PUT /config/llm-api-key no longer persists a key.
  • Migration (scripts/migrate_tinydb_to_sqlite.py): runs on lifespan startup. Imports a legacy data/database.json (TinyDB) into SQLite if present, then renames it database.json.migrated. Idempotent. migrate_legacy_keys() likewise folds legacy plaintext keys into the encrypted store.

LLM Features

Feature Description
API Key Passing Direct to litellm (avoids race conditions)
JSON Mode Auto-enabled for supported providers
Retry Logic 2 retries, temperature 0.1→0.0
Timeouts 30s (health), 120s (completion), 180s (JSON)

Prompt Guidelines

  1. Use {variable} for substitution (single braces)
  2. Include JSON schema examples
  3. End with "Output ONLY the JSON object"

API Endpoints Quick Ref

GET  /api/v1/health              # liveness probe (no LLM call)
GET  /api/v1/status              # Full status (LLM + DB isolated; 200 on partial failure)
GET/PUT /api/v1/config/llm-api-key            # no longer persists a key
GET/POST/DELETE /api/v1/config/api-keys       # per-provider encrypted keys
POST /api/v1/resumes/upload      # PDF/DOCX
POST /api/v1/resumes/improve     # Tailor (LLM)
GET  /api/v1/resumes/{id}/pdf
DELETE /api/v1/resumes/{id}
GET  /api/v1/applications        # Kanban tracker: grouped list (+ POST/PATCH/DELETE/bulk)

Data Flow

Upload: File → markitdown → Markdown → LLM parse → JSON → SQLite (via db)

Improve: Resume + Job → Extract keywords (LLM) → Tailor (LLM) → Store. Routers call services; services call app/llm.py; persistence goes through the async db facade. /improve/confirm also best-effort auto-creates an applied card in the tracker.

Error Handling

Log details server-side, generic messages to clients:

except Exception as e:
    logger.error(f"Failed: {e}")
    raise HTTPException(500, "Operation failed.")

Running

cd apps/backend
cp .env.example .env
uv run uvicorn app.main:app --reload --port 8000

Adding New Endpoints

  1. Create router in app/routers/
  2. Add Pydantic models to app/schemas/models.py
  3. Register router in app/main.py