# OpusAgent Self-hosted REST API proxy to Claude models via Claude Code SDK, leveraging a Claude Pro/Max subscription. ## Architecture Two Docker containers behind Traefik reverse proxy: ``` Client → Traefik (HTTPS) → FastAPI (Python, :8000) ↔ Node.js Worker (:3001, internal) ↓ ↓ SQLite (WAL) Claude Code SDK ``` - **fastapi-service**: API REST, auth, rate limiting, async queue processing - **claude-worker**: Express server wrapping `@anthropic-ai/claude-code` SDK ## Key Paths | Area | Path | |------|------| | FastAPI entrypoint | `src/backend/main.py` | | Config (Pydantic Settings) | `src/backend/config.py` | | Database layer (aiosqlite) | `src/backend/db/database.py` | | API models + envelope | `src/backend/models/api.py` | | Routes | `src/backend/api/routes/{health,topics,requests,sessions}.py` | | Auth + rate limiter | `src/backend/api/dependencies.py` | | Worker HTTP client | `src/backend/services/worker_client.py` | | Queue processor | `src/backend/services/queue.py` | | Node.js worker | `worker/index.js` | | Tests | `tests/` | | Design spec (Italian) | `docs/superpowers/specs/` | | Implementation plan | `docs/superpowers/plans/` | ## Tech Stack - **Python 3.11+**: FastAPI, uvicorn, aiosqlite, httpx, slowapi, pydantic-settings - **Node.js 20**: Express, @anthropic-ai/claude-code SDK (v1.0.128+, async iterable API) - **Package manager**: uv (Python), npm (Node.js) - **Database**: SQLite with WAL mode, foreign keys enabled - **Infra**: Docker Compose, Traefik v3 (TLS via Let's Encrypt) ## Database Schema Three tables in SQLite: - **topics**: id (UUID), name (unique), system_prompt, summary, model (nullable), timestamps - **requests**: id (UUID), session_id, sdk_conversation_id, topic_id (FK), prompt, result, error, model, summarize (bool), status (pending/processing/completed/failed), timestamps - **closed_sessions**: session_id (PK), closed_at ## API Endpoints All require `X-Api-Key` header. All responses use envelope: `{success, data, error}`. - `GET /api/health` — health check (DB + worker) - `POST|GET /api/topics`, `GET|PUT|DELETE /api/topics/{id}` — topic CRUD - `POST /api/topics/{id}/summarize` — regenerate complete topic summary - `POST /api/requests` (202) — submit prompt (async queue), `GET|DELETE /api/requests[/{id}]` - `GET /api/sessions`, `DELETE /api/sessions/{id}` — session management ## Processing Flow 1. `POST /api/requests` → 202 Accepted (queued) 2. Background loop picks oldest pending, calls worker `POST /process` 3. Worker runs Claude Code SDK (`query()` async iterable) with optional session resume 4. Result stored in DB; SDK conversation ID saved in `sdk_conversation_id`; optional incremental summary appended to topic 5. Client polls `GET /api/requests/{id}` for result Model resolution: request-level > topic-level > `CLAUDE_MODEL` env var. ### Multi-turn sessions - `session_id: "new"` → stored as NULL; after completion, set to SDK's conversation ID - Custom `session_id` → preserved as-is; SDK conversation ID stored separately in `sdk_conversation_id` - Resume logic: queue looks up `sdk_conversation_id` from previous completed requests in the same session; only passes it to worker if found (avoids resume on non-existent SDK sessions) ## Commands ```bash # Development uv sync --all-groups uv run dev # uvicorn with --reload # Production docker compose up -d # both services docker compose exec claude-worker npx claude login # first-time auth (must run on active container, NOT run --rm) # Tests uv run pytest -v # Logs docker compose logs -f fastapi-service docker compose logs -f claude-worker ``` ## Environment Variables (.env) | Variable | Default | Purpose | |----------|---------|---------| | `API_KEY` | (required) | Auth key for all endpoints | | `API_HOST` | 0.0.0.0 | Server bind address | | `API_PORT` | 8000 | Server port | | `WORKER_URL` | http://claude-worker:3001 | Internal worker address | | `DB_PATH` | /data/opus-agent.db | SQLite database path | | `CLAUDE_MODEL` | claude-sonnet-4-6 | Default Claude model | | `RATE_LIMIT_PER_MINUTE` | 10 | Rate limit per API key | | `RATE_LIMIT_PER_HOUR` | 100 | Rate limit per API key | ## Design Decisions - **Sequential processing**: one request at a time (FIFO), no concurrency — simplifies Claude Code SDK session handling - **Async Python**: FastAPI + aiosqlite for non-blocking I/O while queue runs in background - **SQLite + WAL**: sufficient for single-writer pattern, no external DB needed - **Envelope pattern**: consistent `{success, data, error}` on all responses - **Session = conversation**: session_id maps to Claude Code SDK `resume` option - **Worker isolation**: not exposed to host, only reachable from fastapi-service via Docker internal network - **Claude auth volume**: credentials stored at `/root/.claude` (not `/home/node/.claude`) because the worker process runs as root inside the container - **SDK async iteration**: `query()` returns an async iterable (not a promise); iterate with `for await...of` and extract the message with `type === "result"` ## Deployment Domain: `opus-agent.tielogic.xyz` → Traefik → fastapi-service:8000 Traefik config: `/opt/docker/traefik/dynamic.yml` (file provider, auto-reload).