src/lodestar/web.py is a FastAPI app: GET / serves a single-page chat, POST /api/chat
runs the agent, GET /health is a probe. The repo ships a Dockerfile so
any container host works.
The production deploy at lodestar.sanyer.org is the TypeScript Worker in
worker/ — the full agentic pipeline: keyword router hint →
Claude's native tool-use loop (retrieve_knowledge, web_search stub) → hybrid
retrieval (Workers AI bge-small-en-v1.5 embeddings + a seeded Vectorize index,
RRF-fused with BM25; degrades to BM25-only if the dense side fails) → the final
answer streamed token-by-token. It is parity-tested against the Python source
(tests/test_worker_parity.py, worker/test/ — router, tools, RRF, prompts, BM25).
After editing data/knowledge.json, re-seed the vector index:
CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… node scripts/seed-vectorize.mjs
(from worker/; token needs Workers AI:Read + Vectorize:Edit). Index creation,
disaster recovery, and orphan-vector cleanup after snippet deletions live in
restore-runbook.md.
cd worker
npm ci
npx wrangler dev # local, offline (TEST_MODE=true via .dev.vars)
npx wrangler deploy # publish (the custom domain rides wrangler.jsonc routes)
npx wrangler secret put ANTHROPIC_API_KEY # one-time; never in vars or files
npx wrangler kv namespace create BUDGET_KV # one-time; put the printed id in wrangler.jsonc- The assets binding serves
src/lodestar/static/(the sameindex.htmlthe Python apps use);/static/*requests are rewritten inworker/src/index.tsso the page is host-agnostic. - Daily budget: live
/api/chatrequests are counted per UTC day inBUDGET_KVand refused with 429 pastDAILY_BUDGET(default 300, set inwrangler.jsoncvars). Setting it to0refuses all live traffic — a manual kill switch. Seedocs/security.mdfor the design. - Rollback: re-create the DNS A record
lodestar → <shared-host IP>(DNS-only) and the prior cPanel deploy below resumes serving immediately.
uv sync # fastapi + uvicorn are runtime deps
uv run lodestar-web # http://127.0.0.1:8000 (offline mock by default)
# live Claude:
ANTHROPIC_API_KEY=sk-ant-... TEST_MODE=false uv run lodestar-webdocker build -t lodestar .
docker run -p 8000:8000 -e TEST_MODE=true lodestar # offline demo
docker run -p 8000:8000 -e TEST_MODE=false -e ANTHROPIC_API_KEY=sk-ant-... lodestar # liveAll three read the Dockerfile and inject $PORT automatically.
Railway — railway init → railway up; set env vars ANTHROPIC_API_KEY and
TEST_MODE=false in the dashboard.
Fly.io — fly launch (detects the Dockerfile) → fly secrets set ANTHROPIC_API_KEY=… TEST_MODE=false → fly deploy.
Render — New → Web Service → connect the repo → Docker runtime → add the two env vars.
- Offline-safe: with
TEST_MODE=truethe app runs with no API key (mock model + BM25/hash retrieval) — good for a zero-cost public demo. - Live mode: set
ANTHROPIC_API_KEY+TEST_MODE=false. On first request the embedding model downloads from HuggingFace (cloud hosts can reach it); if not, retrieval falls back to BM25 automatically. - Cost control: the app rate-limits per IP (
RATE_LIMIT_PER_MIN, default 12) and caps message length; a live-key public demo should also have a spend limit set in the provider console (documented as a hardening step insecurity.md).
cPanel runs Python apps over WSGI via Phusion Passenger ("Setup Python App"). Lodestar's ASGI
app is wrapped by passenger_wsgi.py (via a2wsgi). Use the lean
dependency set (requirements-lean.txt) — the heavy semantic-RAG
stack (lancedb/fastembed/onnxruntime) is RAM/CPU/inode-hungry for a shared tier, and the
app degrades to BM25 retrieval, which is ample for the small curated knowledge base.
1. DNS — point the subdomain at the hosting. Namecheap → Domain List → sanyer.org →
Advanced DNS → add: A record · Host lodestar · Value <account IP> (shown in cPanel's
"General Information" panel, e.g. 162.0.212.4) · TTL Automatic.
2. Create the Python app. cPanel → Setup Python App → Create Application:
Python = newest available; Application root = lodestar.sanyer.org; Application URL =
lodestar.sanyer.org; Startup file = passenger_wsgi.py; Entry point = application. Note the
source …/bin/activate command it prints — that's the app's virtualenv.
3. Code + deps (SSH or cPanel Git Version Control). Enable SSH (cPanel → Manage Shell), then from the app root:
cd ~/lodestar.sanyer.org
git clone https://github.com/wolfieman/lodestar.git . # public repo
source ~/virtualenv/lodestar.sanyer.org/<ver>/bin/activate
pip install -r requirements-lean.txt4. Environment variables. Setup Python App → Environment variables: TEST_MODE=false,
ANTHROPIC_API_KEY=sk-ant-…, LODESTAR_PROVIDER=anthropic. Set these in the UI (not a committed
file). Then Restart.
5. Verify. curl https://lodestar.sanyer.org/health → {"status":"ok"}; open the URL to chat.
Optional — try full semantic RAG. Over SSH: pip install lancedb fastembed onnxruntime,
restart, and watch memory. If it runs under the account limits, keep it (auto-used); if it OOMs,
uninstall and the app falls back to BM25. For ~23 docs the quality difference is minor.