Backend reference
The backend is a FastAPI application in backend/. Entry point: backend/main.py.
Module structure
backend/
├── main.py # App factories, SessionMiddleware, SPA serving
├── session.py # ExpirableTokenDict: session + admin token stores
├── network.py # get_lan_ip() — shared LAN IP detection
├── db.py # SQLite connection, schema initialisation
├── log_buffer.py # Ring-buffer log handler (last 1 000 lines)
├── cli.py # Typer CLI (trove setup / trove start)
│
├── config/ # GET + PUT /api/config, XDG persistence
├── i18n/ # GET /api/i18n/{locale}, locale file loading
├── system/ # GET /api/system/check — RAM, disk, GPU, viable models
├── ollama/ # Install/pull/build (SSE), status, Modelfile generation
│
├── setup/ # Setup-mode wizard endpoints (/api/setup/*)
│ ├── router.py
│ └── service.py # ServiceInstaller Protocol + Real + Fake
│
└── app/ # App-mode endpoints (/api/app/*)
├── router.py # Admin endpoints, delegates to tasks + documents sub-routers
├── auth.py # bcrypt verify, admin cookie validation
├── tasks/ # Gem CRUD + runner (/api/app/gems/*)
└── documents/ # Document upload + library (/api/app/documents/*)
Service pattern
Every external dependency follows the same pattern:
class OllamaService(Protocol):
def get_status(self) -> OllamaStatus: ...
class RealOllamaService: # talks to the real Ollama process
...
class FakeOllamaService: # returns hardcoded test data
...
def get_ollama_service() -> OllamaService:
if os.getenv("TROVE_FAKE_OLLAMA"):
return FakeOllamaService()
return RealOllamaService()
FastAPI dependencies inject get_ollama_service(). Tests set TROVE_FAKE_OLLAMA=1 and never touch a real Ollama server.
Session and auth
Session tokens — all browser clients call GET /api/session on load. This returns a token that must be included as X-Trove-Session in every subsequent request. SessionMiddleware in main.py validates each token against the in-memory session_store. Tokens expire after 2 hours of inactivity.
Admin cookie — the admin login endpoint (POST /api/app/admin/login) verifies the password with bcrypt and sets an httponly cookie. Subsequent admin API calls are authenticated by this cookie. The cookie expires after 8 hours.
SSE streaming
Long-running operations (Ollama install, model pull, gem run) stream progress as Server-Sent Events. The pattern:
from sse_starlette.sse import EventSourceResponse
@router.post("/some-action")
def some_action():
def generate():
for line in do_the_work():
yield {"data": line}
yield {"data": "[DONE]"}
return EventSourceResponse(generate())
The frontend uses streamLines() from frontend/src/api/ollama.ts to consume SSE streams.
Adding a new domain
- Create
backend/newdomain/with__init__.py,router.py,service.py,models.py. - Follow the Protocol/Real/Fake pattern in
service.py. - Register the router in
backend/main.py(shared) or in the appropriate mode router. - Write tests in
tests/test_newdomain.pyusingFakeXxxService.