Skip to content

Engineering overview

Metrica turns existing store CCTV into live footfall, dwell-time, and peak-hours analytics. No new hardware, no face recognition, no personal identity storage — only anonymized IN/OUT counts derived from people crossing a virtual line at the store entrance.

The platform is a multi-tenant SaaS. A store owner logs in, sees their stores’ live occupancy and historical KPIs, and (via the admin app) has cameras provisioned and counting lines calibrated for them.

Layer Tech Responsibility
CV worker Python · YOLO (yolo26n / yolo11n) + ByteTrack · OpenCV Reads an RTSP/video stream, detects and tracks people, counts line crossings, POSTs IN/OUT events to the backend
Backend FastAPI · SQLAlchemy (async) · PostgreSQL · Alembic Ingests events, enforces tenancy, computes KPIs on the fly, serves the dashboard and admin APIs
Frontend React 19 · Vite · TanStack Router · shadcn/ui Owner-facing dashboard — live occupancy + historical charts
Admin React · Vite · shadcn/ui Internal app — provision stores/cameras, assign owners, calibrate lines, monitor camera health
Auth Supabase Auth (ES256 JWT) Identity and login; issues the Bearer tokens the backend verifies
Database Neon Postgres — eu-central-1 (Frankfurt) Application data (users, stores, cameras, events)
Hosting FastAPI Cloud Runs the backend API

A standalone Python process (worker/run.py), one per camera. Its loop:

  1. model.track(...) runs YOLO with the ByteTrack tracker, filtered to the person class only (classes=[0], persist=True to keep tracker IDs across frames).
  2. For each tracked person, the bounding-box center is fed to a pure LineCounter that tracks which side of the virtual line the person is on and emits IN or OUT when they cross it.
  3. Events are buffered and flushed to the backend roughly every 30 frames (near-real-time), alongside a periodic camera heartbeat and occasional frame snapshots used for line calibration.

The counting line can come from the CLI (calibration), from the backend camera config, or default to mid-frame height. See architecture for details.

A FastAPI app (backend/app/main.py) exposing routers for auth, events, live, dashboard, cameras, and admin. It enforces two trust boundaries:

  • Owners authenticate with a Supabase JWT (verified against the project JWKS). Every read is scoped to stores they own.
  • The worker authenticates with a shared X-Worker-Key header on ingestion endpoints (POST /events, snapshot, heartbeat, camera config).

KPIs are computed on the fly from raw events — there is no metrics table yet. See the data model for what is stored versus derived.

Two React/Vite apps. The dashboard shows live occupancy (GET /live/{store_id}) and period KPIs (GET /dashboard/{store_id}?period=day|week|month). The admin app drives the superadmin-only /admin/* endpoints: create stores and cameras, assign an owner by email (which sends an invite), and monitor camera health via last_seen_at / snapshot_at.

Camera (RTSP) ─▶ CV worker ─▶ POST /events ─▶ Backend ─▶ Postgres (events)
(YOLO+ByteTrack, (X-Worker-Key) (validate,
LineCounter) persist)
Owner dashboard ◀── GET /live, GET /dashboard ◀──────────┘
(React) (Supabase JWT, KPIs computed from events)
  1. The worker detects a person crossing the entrance line and produces an IN or OUT event carrying a UTC timestamp, the store/camera IDs, and the ephemeral ByteTrack tracker_id.
  2. Buffered events are POSTed to /events. The backend validates and inserts one events row per crossing.
  3. When the owner opens the dashboard, the backend counts events (only from cameras flagged is_counting_line) and derives occupancy and dwell time in real time using Little’s Law.
  • Architecture — component responsibilities, request flow, the event/session/metric model, and the edge-vs-cloud split.
  • Data model — entities, columns, and relationships.