Deployment
How the pieces reach the public internet. Full step-by-step runbooks live in the repo — TESTNET.md (contract + Hub) and PRODUCTION.md (Cloudflare go-live) — this page is the map.
Target topology
| URL | What | Where |
|---|---|---|
https://<domain> | Landing (landingpageObulus/) | Cloudflare Pages |
https://app.<domain> | Cockpit (AppObulus/) | Cloudflare Pages |
https://docs.<domain> | This site (gitbook/) | Cloudflare Pages |
https://api.<domain> | Hub (Fastify) | VPS · nginx · Cloudflare proxy |
The fronts — one Pages project per folder
Each static site is its own git-connected Cloudflare Pages project with a build watch path, so a push only redeploys the folder it touched:
| Setting | landing | app | docs |
|---|---|---|---|
| Root directory | landingpageObulus | AppObulus | gitbook |
| Build | npm ci && npm run build | idem | idem |
| Output | dist | dist | dist |
| Watch path | landingpageObulus/* | AppObulus/* | gitbook/* |
The app reads its chain identity at build time (VITE_API_URL, VITE_CHAIN_ID, VITE_ESCROW_ADDRESS, VITE_USDC_ADDRESS, VITE_RPC_URL); a malformed or missing value renders a clear configuration-error screen, never a blank page. The landing's CTAs are env-driven (VITE_APP_URL, VITE_DOCS_URL, VITE_GITHUB_URL, VITE_SECURITY_URL).
The contract — deliberate, guarded, verified
forge script script/DeployBaseSepolia.s.sol --rpc-url base_sepolia --broadcast --verify- Network guards:
require(block.chainid == 84532)(Sepolia) /8453(mainnet — expects a hardware/multisig owner + treasury). - USDC resolves automatically: Circle's canonical address per chain, or a freely-mintable
MockUSDCwithUSE_MOCK_USDC=true(testnet bot liquidity). - The script writes
contrat/deployments/<chainId>.json;scripts/testnet-env.mtsthen fans the addresses out to the Hub (deployments.json) and the cockpit (AppObulus/.env.local) — no manual copying, no drift. - Contracts are immutable: a bad parameter means a new address, never a migration.
The Hub — VPS, systemd, origin-locked nginx
- Unit
obulus-hub(agents/deploy/obulus-hub.service): non-root user,Restart=on-failure,NoNewPrivileges, reads/opt/obulus/backend/.env. - Chain mode requires
ARBITER_ADDRESS+TREASURY_ADDRESS(boot refuses demo fallbacks on a real chain) and flipsSTRICT_SIGNATURES=true. - Behind the proxy:
TRUST_PROXY=1so the per-IP rate limit keys on the real client IP;CORS_ORIGINlists the exact front origins. - nginx (
agents/deploy/nginx/): Cloudflare origin certificate, per-IPlimit_req, real-IP restoration fromCF-Connecting-IP, and a$cf_edgegate that 403s any direct-to-origin connection that didn't come through Cloudflare — plus HSTS andnosniff. - Health:
curl https://api.<domain>/health→"mode":"onchain".
The arena bots (optional but great)
arena-agents.service runs the seller/buyer/arbiter bots against the deployed contract — a continuous public proof that the full lifecycle works. Keys are generated on the server (npm run keygen -w agents, chmod 600) and are never the well-known dev keys: the bots refuse those on any non-local RPC.
CI is the gate
Every push runs four parallel jobs — backend/SDK/agents tests, the full Foundry suite (fuzz + invariants), both front builds, and the wallet e2e (anvil + deployed contract + Hub in chain mode + Playwright driving the real cockpit). Nothing reaches main broken without telling on itself.
Rollback
- Pages: dashboard → previous deployment, one click.
- Hub: restore
.env.bak, removedeployments.json(back to simulated),systemctl restart obulus-hub. - Contract: deploy a new address and re-run
testnet-env.mts— immutability cuts both ways.