Skip to main content

Architecture Overview

WatchLens is built around a single architectural idea, which is the hybrid registry pattern. The recommendation policy and the UI a participant sees are both registered through the same kind of plug-in interface. New policies and new UIs are first-class additions, and neither requires forking the platform.

The two registries are parallel​

ConcernRecommender registryUI registry
Built-in entriesBUILTIN_INSTANCES, Python class instances (5 baselines)BUILTIN_UIS, React presets (youtube-{desktop,tablet,mobile}, tiktok-{desktop,tablet,mobile}, none)
External / DB-drivenrecommender_registry table (kind='external_http' rows)ui_templates table (status='published' rows)
Registration entryPOST /admin/recommendersVisual or Code editor in the admin UI, then Save + Publish
Dispatcherget_recommender(key) returns BaseRecommenderInline branch in Feed.tsx and VideoWatch.tsx
Capability flagssupports_feed, supports_watchsupports_feed, supports_watch, devices
Validatoris_registered(key) and get_capability(key)BUILTIN_*_KEYS set and _is_valid_template_for_device(key)
Live propagationOne worker writes the cache, and siblings refresh on miss via DB lookupOne admin publish writes the DB, and the frontend fetches templates on dropdown

The two columns differ only in surface vocabulary. The dispatcher logic is the same. The platform tries the built-in map first, falls through to a DB lookup, self-heals the cache on miss, and either raises (or renders a "not configured" notice) if neither resolves.

Why this matters​

Both axes are user-extensible without forking the platform.

  • A new recommendation policy is one Python file (subclass BaseRecommender, register an instance), or zero code at all if the service already runs over HTTP elsewhere.
  • A new UI is one TSX file (drop in ui-presets/<key>/feed.tsx), or zero code at all if it can be composed in the visual editor.

Researchers reuse the platform. They do not fork it. That keeps studies running on different policies and UIs comparable, because the data plane underneath is unchanged.

What sits underneath both registries​

The platform is a four-container Compose stack.

  • db, PostgreSQL 15. Holds users, groups, videos, sessions, the events table (wide-table plus JSONB payload), recommender_registry, ui_templates, and the training caches.
  • backend, FastAPI plus SQLAlchemy 2.0 plus Alembic. Wires the recommender registry, the auth surface (HttpOnly cookie plus bcrypt), the events ingest, and the admin API.
  • frontend, React 18 plus TypeScript plus Vite plus Tailwind. Hosts the three UI authoring tracks plus the admin console and serves the built bundle through nginx.
  • data-nginx, a second nginx that range-serves video and thumbnail files mounted from data/.

Both the recommender and the UI track read from the same data plane. The events table is the single point of measurement. Every authoring track lands events through EventContext (5 s and 20-event batching), so 33 events across 6 categories show up identically regardless of which dispatcher branch produced the rendered page.

What is intentionally not in scope​

  • No native mobile app. Three device-tagged variants of the bundled presets cover desktop, tablet, and mobile via responsive HTML. Per-group device assignment ensures participants do not silently slide into a different variant.
  • No A/B traffic split. Allocation is by group membership at account-creation time. Mid-experiment reassignment is not modelled.
  • No real-time online learning loop. RecBole training runs on a scheduler (default 60 min) and writes pre-computed predictions into a cache. Request-time policies (HTTP recommenders) can do whatever they want internally.

Where to go next​