This guide covers running the development environment, executing tests, and configuring the project.
- Docker (29.x+) — for containerized development
- uv (Python package manager) — for local backend development
- Node.js (20.x+) — for local frontend development
- Python (3.12) — managed by
uv
Starts both backend and frontend services with health checks and hot-reload for the frontend.
# Start both services
docker compose up --build
# Run in background
docker compose up --build -d
# Stop services
docker compose downServices exposed:
| Service | URL | Port |
|---|---|---|
| Backend (FastAPI) | http://localhost:8000 |
8000 |
| Backend API Docs | http://localhost:8000/docs |
8000 |
| Frontend (Vite) | http://localhost:5173 |
5173 |
How it works:
- Backend starts first and passes a health check (
/healthendpoint) - Frontend starts only after backend is healthy (
depends_on: condition: service_healthy) - Frontend source (
./frontend/src) is mounted as a volume for hot-reload - Vite proxies
/api/*requests to the backend
cd backend
# Install dependencies (including dev)
uv sync --extra dev
# Run the server
uv run uvicorn octave.app:app --reload --host 0.0.0.0 --port 8000cd frontend
# Install dependencies
npm install
# Start dev server
npm run devcd backend
# Install dev dependencies first
uv sync --extra dev
# Run all tests
uv run pytest tests/ -v
# Run specific test file
uv run pytest tests/test_health.py -v
# Run with coverage
uv run pytest tests/ -v --cov=octave --cov-report=term-missingBackend quality checks:
# Linting (ruff)
uv run ruff check src/ tests/
# Auto-fix linting issues
uv run ruff check --fix src/ tests/
# Type checking (mypy)
uv run mypy src/cd frontend
# Install dependencies first
npm install
# Unit tests (Vitest)
npm run test
# Unit tests in watch mode
npm run test:watch
# E2E tests (Playwright)
npm run test:e2e
# Install Playwright browsers (first time)
npx playwright install chromiumFrontend quality checks:
# Linting (ESLint)
npm run lint
# Format code (Prettier)
npm run format
# Check formatting without writing
npm run format:check
# TypeScript type checking
npx tsc --noEmitThe backend reads configuration from environment variables. Create backend/.env for local development:
# Server
HOST=0.0.0.0
PORT=8000Note:
.envfiles are in.gitignoreand never committed.
The persistence layer (octave.db) reads OCTAVE_DB_* variables, via
DatabaseSettings in backend/src/octave/db/config.py:
| Variable | Default | Description |
|---|---|---|
OCTAVE_DB_ADAPTER |
sqlite |
Registered adapter name, or a module.path:ClassName import string for a third-party adapter package |
OCTAVE_DB_URL |
sqlite+aiosqlite:///octave.db |
Async SQLAlchemy URL; Alembic derives the sync mirror automatically |
OCTAVE_DB_EMBEDDING_DIM |
768 |
Width of the vector index; must match the embedding model in use |
OCTAVE_DB_AUTO_MIGRATE |
true |
Apply pending migrations at startup. false skips the upgrade but still verifies the DB is migrated (startup fails fast if not) |
Alembic CLI commands run from backend/ (uv run alembic upgrade head).
Migrations are auto-applied on app startup via octave.db.lifespan.db_lifespan
unless OCTAVE_DB_AUTO_MIGRATE=false.
The inference layer (octave.inference) reads OCTAVE_INFERENCE_* variables,
via InferenceSettings in backend/src/octave/inference/config.py:
| Variable | Default | Description |
|---|---|---|
OCTAVE_INFERENCE_ADAPTER |
openai |
Registered adapter name, or a module.path:ClassName import string for a third-party adapter package |
OCTAVE_INFERENCE_BASE_URL |
http://localhost:11434/v1 |
OpenAI-compatible engine endpoint (Ollama, vLLM, llama.cpp server, LM Studio, …) |
OCTAVE_INFERENCE_API_KEY |
(empty) | Bearer token; optional for local engines. Secret — never logged (redacted ***) |
OCTAVE_INFERENCE_DEFAULT_MODEL |
(unset) | Model used when a request doesn't specify one |
OCTAVE_INFERENCE_TIMEOUT_SECONDS |
120.0 |
Per-request timeout |
OCTAVE_INFERENCE_MAX_RETRIES |
2 |
SDK-level retries on transient failures |
HTTPS engines behind an internal CA: if the engine uses a certificate from
a private CA, httpx (certifi bundle) will fail the handshake with a generic
connection error even though curl succeeds. Point it at the system trust
store:
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtThis adds to certifi's bundle rather than replacing it, so public HTTPS
endpoints keep working. Verify any setup with backend/scripts/smoke_inference.py.
The MCP layer (octave.mcp) reads OCTAVE_MCP_* variables via
McpSettings in backend/src/octave/mcp/config.py:
| Variable | Default | Description |
|---|---|---|
OCTAVE_MCP_REQUEST_TIMEOUT_SECONDS |
30.0 |
Per-request timeout for client calls |
OCTAVE_MCP_INITIALIZE_TIMEOUT_SECONDS |
30.0 |
Budget for the initialize handshake during connect/restart |
OCTAVE_MCP_RESTART_BASE_DELAY_SECONDS |
1.0 |
First auto-restart delay; exponential backoff multiplies by 2 per attempt |
OCTAVE_MCP_RESTART_MAX_DELAY_SECONDS |
60.0 |
Cap on the backoff delay |
OCTAVE_MCP_RESTART_MAX_ATTEMPTS |
5 |
Consecutive failed restart cycles before a server enters crashed |
OCTAVE_MCP_PROBE_TIMEOUT_SECONDS |
5.0 |
Budget for the manager's confirming ping after a request timeout |
OCTAVE_MCP_STABILIZATION_SECONDS |
60.0 |
Time a connection must hold before the failure counter resets |
The auth layer (octave.auth) reads OCTAVE_AUTH_* variables via
AuthSettings in backend/src/octave/auth/config.py:
| Variable | Default | Description |
|---|---|---|
OCTAVE_AUTH_COOKIE_SECURE |
false |
Set true when the backend is served over HTTPS so the session cookie carries Secure |
OCTAVE_AUTH_IDLE_TTL_DAYS |
14 |
Sessions expire after this much inactivity (rolling) |
OCTAVE_AUTH_ABSOLUTE_TTL_DAYS |
90 |
Sessions expire this long after creation regardless of activity |
Lost owner passwords are recovered out-of-band:
uv run python -m octave.auth.cli reset-password <username> --password-file <path>
(requires direct DB access on the host; the new password is never passed via argv).
Verify any setup with uv run pytest tests/auth.
The frontend uses Vite's environment variable convention. Create frontend/.env for local development:
# API URL for non-Docker development
VITE_API_URL=http://localhost:8000Environment variable rules:
- Only variables prefixed with
VITE_are exposed to the browser - Access in code via
import.meta.env.VITE_API_URL - In Docker Compose,
VITE_API_URLis set tohttp://localhost:8000
docker-compose.yml defines two services:
Backend service:
- Builds from
backend/Dockerfile - Exposes port 8000
- Health check polls
/healthevery 30s (10s timeout, 3 retries, 5s start period)
Frontend service:
- Builds from
frontend/Dockerfile - Exposes port 5173
- Depends on backend being healthy
- Mounts
./frontend/srcfor hot-reload
Octave/
├── docker-compose.yml # Service orchestration
├── backend/
│ ├── Dockerfile # Backend container
│ ├── pyproject.toml # Python project config (uv, pytest, ruff, mypy)
│ ├── .python-version # Python 3.12
│ ├── uv.lock # Resolved dependencies
│ ├── src/octave/
│ │ ├── __init__.py
│ │ └── app.py # FastAPI application
│ └── tests/
│ └── test_health.py # Backend tests
└── frontend/
├── Dockerfile # Frontend container
├── package.json # Node project config
├── tsconfig.json # TypeScript config
├── vite.config.ts # Vite build + dev server
├── vitest.config.ts # Vitest unit test config
├── playwright.config.ts # Playwright E2E config
├── eslint.config.js # ESLint config
├── .prettierrc # Prettier config
├── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Root component
│ ├── index.css # Global styles
│ ├── vite-env.d.ts # Vite type declarations
│ └── components/
│ └── HealthCheck.tsx # Health check UI
└── tests/
├── App.test.tsx # Unit tests
└── e2e/
└── health.spec.ts # E2E tests
# Find what's using a port
lsof -i :8000 # Backend
lsof -i :5173 # Frontend
# Kill the process
kill -9 <PID># Clean rebuild
docker compose build --no-cacheEnsure ./frontend/src:/app/src volume mount is present in docker-compose.yml. The Vite dev server watches this directory.
The health check uses Python's urllib (available in standard library). Verify the /health endpoint responds:
docker compose exec backend python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/health').read())"