Skip to content

Architecture

Lef edited this page Oct 4, 2026 · 4 revisions

Architecture

Seven containers from docker-compose.yml, two LLM servers on the host, and (optionally) a reverse proxy in front for public access.

 internet ──► reverse proxy (TLS) ─────────────► this machine
 LAN ─────────────────────────────────────────────► nginx :80 ──┬──► frontend/build (static, Vite)
                                                                  └──► backend 127.0.0.1:5000
                                                                       (Flask on gunicorn, /api/*,
                                                                        live updates over SSE)
                                                                           │
        ┌───────────────────────┬─────────────────────┬────────────────────┼────────────────────┐
        ▼                       ▼                     ▼                    ▼                    ▼
 postgresql :5432        chromadb :8000           redis :6379        monitoring :8001     Laya classifier
 (users, chronicles,     (memory + rule books,    (rate limits,      (CPU/RAM/GPU →        (ONNX on CPU,
  characters, messages,   bge-m3 embeddings)       lockouts,          system_status.json)   inside backend)
  dice, auth tables)                               SSE tickets)

 host: LM Studio :1234 (Storyteller models + bge-m3 embeddings)   Ollama :11434 (llama3.2:3b utility)

Only nginx (:80) is reachable from the network. Postgres, Redis, ChromaDB, monitoring, the frontend dev server and the backend listen on 127.0.0.1 only. (Docker writes its own firewall rules for published ports, so ufw alone doesn't protect them; the compose file binds them to localhost.)

Containers

Service Notes
nginx Host network. / → frontend, /api/ → backend (90 s read timeout for AI replies), the SSE path unbuffered. Trusts X-Forwarded-For only from the reverse proxy in front of it. Config in nginx/nginx.conf
frontend Dev profile only (--profile dev): the Vite dev server on :3000, and the image ./scripts/build-frontend.sh builds with. In production nginx serves frontend/build directly
backend Flask on gunicorn (2 workers × 48 threads, gthread), bound to 127.0.0.1 on the host network so it can reach the host LLMs. APP_SERVER=flask gives the old dev server
postgresql Postgres 16. On first start it loads backend/init_postgresql_schema.sql
chromadb chromadb/chroma:1.5.9, data in data/vector_db. All collections use bge-m3
redis Rate limits, login lockouts, one-time SSE tickets
monitoring Writes data/logs/system_status.json; needs the NVIDIA container runtime for GPU stats

Live chat updates

The browser swaps its login token for a 60-second, one-time ticket (POST /api/campaigns/<id>/events/ticket), then opens GET /api/campaigns/<id>/events?ticket=… (server-sent events). A database trigger bumps a per-chronicle counter on every message change; each stream polls that one row and tells the browser which room changed, and the browser fetches the new messages. If the stream fails, the browser falls back to polling every 5 seconds.

Dice

Rolls are made on the server (/api/campaigns/<id>/roll, /reroll, /rouse) and the server also posts the dice animation marker and the result line to the room, in the same transaction. Clients can't post dice rows themselves, so a dice card in the chat is always a real roll.

Database schema

The full schema is backend/init_postgresql_schema.sql. On startup the backend also runs migrate_db() in backend/database.py, which adds newer columns and tables if they are missing. Each of those checks runs once per process (an ALTER TABLE … IF NOT EXISTS still takes an exclusive lock in Postgres, so running them per request used to deadlock concurrent requests). CI checks that the schema file plus migrate_db() twice produce the identical pg_dump.

Rows are dicts (RealDictCursor): read columns by name. The JWT identity is a string: int(get_jwt_identity()) before comparing with ids.

Where things live

  • backend/routes/: API endpoints (auth, users, admin, campaigns, characters, locations, messages, dice, ai, events, rule_books)
  • backend/services/: AI roles/providers, language, classifier, Storyteller prompt, RAG/vector store, dice engines (wod_dice.py classic, v5_dice.py), auth security
  • frontend/src/app/: shell, router, auth; frontend/src/features/: play, chat, dice, chronicles, profile; frontend/src/design/: tokens, glyphs, components, atmosphere; frontend/src/i18n/: English and Greek
  • ml/laya/: classifier training pipeline (the model itself is in data/laya/model/)
  • books/: rule book scripts
  • data/: runtime data (gitignored)

Clone this wiki locally