-
Notifications
You must be signed in to change notification settings - Fork 2
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.)
| 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 |
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.
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.
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.
-
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.pyclassic,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 indata/laya/model/) -
books/: rule book scripts -
data/: runtime data (gitignored)