Skip to content

Local development

How to run each Metrica service locally: the backend API, the dashboard, the admin console, the marketing landing and the CV worker.

Metrica shares this machine with mintveo, which already owns 5432 and 8000. Metrica therefore uses the +1 ports so the two stacks never collide.

Service Port Notes
Docker Postgres 5433 container maps 5433:5432 (mintveo owns host 5432)
Backend API 8001 mintveo’s backend owns 8000
Dashboard (frontend/) 5183 3000/3001 taken by other local tools
Admin console (admin/) 5185 set in admin/vite.config.ts
Landing (landing/) 5190 set in landing/vite.config.ts
  • Docker (for Postgres)
  • uv — Python env + runner for backend/ and worker/
  • bun — package manager / runner for the JS apps

uv run and bunx use each project’s local environment automatically — you do not need to source .venv/bin/activate.

The backend expects Postgres on localhost:5433. Bring it up first:

Terminal window
docker compose -f backend/docker-compose.yml up -d

This starts postgres:16 with:

Setting Value
user / password postgres / postgres
database retailpulse
host port 5433 (maps to container 5432)
volume retailpulse_pgdata (data persists across restarts)

Stop it with docker compose -f backend/docker-compose.yml down (add -v to wipe the data volume).

Set up env once:

Terminal window
cd backend
cp .env.example .env
uv sync

Key vars in backend/.env:

Var Local value
DATABASE_URL postgresql+asyncpg://postgres:postgres@localhost:5433/retailpulse
SUPABASE_URL / SUPABASE_SECRET_KEY blank locally until auth is wired up
SUPERADMIN_EMAILS comma-separated superadmin emails
WORKER_API_KEY shared secret the worker sends as X-Worker-Key — generate with openssl rand -hex 32, must match worker/.env.local

Run migrations, then start the server:

Terminal window
cd backend
uv run alembic upgrade head
./scripts/backend.sh

scripts/backend.sh runs:

Terminal window
uv run uvicorn app.main:app --port 8001 --host 127.0.0.1

API docs are then at http://127.0.0.1:8001/docs.

Terminal window
cd frontend
cp .env.example .env.local
bun install
./scripts/frontend.sh # → bunx vite dev --port 5183 --host 127.0.0.1

Open http://127.0.0.1:5183/dashboard. Vite auto-loads frontend/.env.local. VITE_* vars are exposed to the browser, so only put publishable/anon keys there:

Var Local value
VITE_SUPABASE_URL https://<project-ref>.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY Supabase anon / publishable key
VITE_API_URL http://localhost:8001
VITE_POSTHOG_KEY, VITE_SENTRY_DSN, … optional analytics / error monitoring
Terminal window
cd admin
cp .env.example .env.local
bun install
./scripts/admin.sh # → bunx vite dev --host 127.0.0.1

Open http://127.0.0.1:5185/. Env vars:

Var Local value
VITE_SUPABASE_URL https://<project-ref>.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY Supabase anon / publishable key
VITE_API_URL http://localhost:8001
VITE_FRONTEND_URL http://localhost:5183 (used for invite / deep links)
Terminal window
./scripts/landing.sh # → bunx vite dev --host 127.0.0.1

Open http://127.0.0.1:5190/.

The worker counts people crossing a virtual line (YOLO + ByteTrack) and POSTs batched events to the backend. It runs against a recorded file or a live RTSP camera.

Terminal window
cd worker
uv sync

Standalone (no API/DB), straight from the worker dir:

Terminal window
uv run python run.py ../data/samples/people-walking.mp4
uv run python run.py ../data/samples/subway.mp4 --save subway_annotated.mp4

Wired to the local backend via the helper script:

Terminal window
# Recorded clip (bounded — exits at end of file)
./scripts/worker.sh data/samples/people-walking.mp4
# Live RTSP — continuous (flushes events ~every 30 frames)
./scripts/worker.sh "rtsp://user:pass@CAM_HOST:554/cam/realmonitor?channel=3&subtype=0"
# Live RTSP — bounded sanity check (stop after N frames)
./scripts/worker.sh "rtsp://user:pass@CAM_HOST:554/cam/realmonitor?channel=3&subtype=0" --max-frames 300

scripts/worker.sh loads worker/.env.local (for WORKER_API_KEY and optional overrides) and defaults to:

Var Default
API_URL http://localhost:8001
STORE_ID the seeded dev store UUID
CAMERA_ID the seeded dev camera UUID

Override any of them inline: STORE_ID=… CAMERA_ID=… API_URL=… ./scripts/worker.sh <SOURCE>.

  • Deployment — how each app ships to prod
  • Operations — migrations, monitoring, incident runbooks
  • Testing — running the test suites