demo.fovea.video deployment runbook
What demo.fovea.video is
A public demo of FOVEA. A QR code at an event booth points here. Anyone scanning it lands on a static tour catalog that runs entirely in the browser: no backend round-trips for the tour experience, no model service, no account required. Serious visitors who want to try the full app can sign in, but registration is disabled by default and accounts are minted only by the operator.
What the deployment does differently
| Component | Production (fovea.video) | Demo (demo.fovea.video) |
|---|---|---|
Frontend / | Video browser (auth required) | Public tour catalog (anonymous) |
| Model service | Live container | Not deployed; MSW intercepts the six routes |
| Registration | Enabled | Disabled (ALLOW_REGISTRATION=false) |
| nginx | Standard config | nginx.demo.conf with rate limits on /api/auth/* |
tour-content.json | Cached | Cache-Control: no-store |
| Build flags | none | VITE_TOUR_DEMO=1, VITE_DEMO_PUBLIC=1 |
The split lives behind a single workflow input. The build, the env layout, and the compose stack are otherwise identical to the production deploy, so improvements to one path land on the other without copy-paste.
How to deploy
From a clean tree on main
gh workflow run deploy.yml -f demo_mode=true
This runs the same deploy.yml you use for production with
demo_mode=true. The workflow then:
- Patches
.envon the server:ALLOW_REGISTRATION=false,VITE_TOUR_DEMO=1,VITE_DEMO_PUBLIC=1. - Swaps
annotation-tool/nginx.confforannotation-tool/nginx.demo.conf. - Skips
docker compose up model-service(the container is not built and not started). - Brings up
backendandfrontendexplicitly so the missing model service does not block the recreate.
Regular pushes to main continue to deploy without demo_mode, so
production stays untouched.
From the GitHub UI
Actions -> Deploy to Production -> Run workflow -> check
demo_mode.
How to seed accounts after the deploy
Registration is off, so visitors cannot mint their own accounts. To hand a partner an account at the demo deployment:
- Sign in as the admin user seeded by
prisma/seed.ts(theADMIN_PASSWORDGitHub secret). - Open the admin console.
Users->Create User.- Hand the email and password to the visitor.
The admin console's CreateUserDialog is operator-only and is
independent of the ALLOW_REGISTRATION env var, so it stays
operational under the demo deploy.
Verifying the deploy
After the workflow finishes:
curl -sI https://demo.fovea.video/
curl -sI https://demo.fovea.video/tour-content.json
curl -sI https://demo.fovea.video/mockServiceWorker.js
curl -sI https://demo.fovea.video/api/health
# Auth rate-limit smoke (expect 429s after the burst is exhausted):
for i in $(seq 1 50); do
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://demo.fovea.video/api/auth/login -d '{}'
done | sort | uniq -c
In a browser:
- Open
https://demo.fovea.video/. You should see the FOVEA wordmark, the "Flexible Ontology Visual Event Analyzer" tagline, and a 4x3 grid of tour cards. - DevTools console should show
[tour-demo] MSW worker active; model-service calls are mocked.before React mounts. - Click any tour tile. The engine launches and the spotlight overlays the active anchor.
- Click
Sign intop-right. The login page should show the FOVEA branding and, below the form, the "Self-registration is disabled on this deployment. To request an account, email admin@fovea.video" notice.
nginx hardening
nginx.demo.conf adds:
- Two
limit_req_zonescopes:login_zoneat600r/mwithburst 100,register_zoneat60r/mwithburst 20. Defense in depth even though registration is disabled. Cache-Control: no-storeon/tour-content.jsonso admin edits to the content bundle propagate on the next page load.expires 1y;plusCache-Control: public, immutableon/assets/for the hashed bundle output (nginx'sexpires 1ydirective emits themax-age=31536000component of theCache-Controlheader automatically).- 60 second cache on
/mockServiceWorker.jsso a worker-version bump propagates fast without making every QR-code visitor re-download it on each scan.
Rolling back
gh workflow run rollback.yml
The rollback workflow does not honour demo_mode; it restores
whatever shape was previously deployed.
To go from a demo deploy back to production shape without a rollback:
gh workflow run deploy.yml # default demo_mode=false -> production
This re-runs the deploy with demo_mode=false, which restores
ALLOW_REGISTRATION=true, restarts the model-service container, and
copies nginx.conf back into place.
How the demo deployment-laptop demo relates
The demo laptop runs a docker-compose stack locally for guided
demos. That stack uses docker-compose.tour-demo.yml, which sets
VITE_TOUR_DEMO=1 at build time but leaves auth and the rest of the
backend untouched. Both deployments share the same MSW interception
layer and the same tour content bundle, so what the operator shows on
the demo deployment screen is the same flow QR-code visitors get on their
phones. See Guide > Tour demo mode for
the worker details.
Maintenance
- Edit
annotation-tool/public/tour-content.jsonto retheme tours for a different domain or audience. The file is admin-editable and servedCache-Control: no-store, so changes take effect on the next page load. The bundle schema lives in the repo atannotation-tool/public/tour-content.schema.json. - Edit
annotation-tool/src/tours/scripts/*.tsto extend the engine. The Welcome tour and the Keyframes tour bracket the 4x3 grid; reorder insrc/tours/scripts/index.ts:getBuiltInTours. - Edit
annotation-tool/nginx.demo.confto tune rate limits or caching headers. - Before any tour-engine or fixture change, run
pnpm exec vitest run src/tours/pluspnpm exec playwright test --project=smoke test/e2e/smoke/tour-demo-*.spec.tsto keep the demo coverage green.