# Discovery — Browse, Search & Home — Plan > **Status:** Planned > **Owner:** Platform team > **Feature ID:** `discovery` > **Godot version:** 4.6.2 stable > **Backend:** FastAPI + SQLAlchemy (async) + PostgreSQL + Redis ## 1. Overview This document breaks the **Discovery** feature into user stories and right-sized implementation tasks. The feature delivers: - **Home / discover page** — a default listing of experiences a player lands on. - **Search** — find worlds by name and by tag. - **Categories** — a single category per world plus free-form tags, with filtering. - **Simple sorting** — newest, most-played, and trending. Trending starts as a basic **recent-activity heuristic** (weighted recent visits); no real recommendation model for MVP. Without this, nothing else built (experiences, creator content) is actually reachable by a real user. Discovery is the front door. ## 2. Why this matters The platform's value is *reachable* content. Worlds and creator assets already exist in the data model (`World`, `Asset`), but there is currently: - **No public listing** of worlds (`/api/worlds` requires auth and returns only PUBLIC worlds or your own worlds, ordered by `created_at` only). - **No search** by name or tag. - **No category/tag metadata** on worlds at all. - **No play-count or recency signal** to power "most-played" or "trending". - **No client UI** for browsing. Discovery closes that gap with a small, dependency-free metadata layer plus a read-optimized listing endpoint. ## 3. Existing constraints (do not re-build) - **Auth:** JWT bearer tokens; `get_current_user` / `CurrentUser` dependency in `app/dependencies.py`. Discovery **listing** must work both for anonymous visitors and authenticated users, so it needs an *optional* auth dependency (not `CurrentUser`, which returns 401). - **DB:** async SQLAlchemy + Alembic; migration head is `0002_add_date_of_birth_to_users`. - **Models:** `World` (id, name, description, owner_id, visibility, created_at, updated_at). `User`, `Asset` (owner_id, is_public) exist. No tags/categories/ play-count fields yet. - **Routers:** `worlds.py` already owns CRUD under `/api/worlds`. Discovery adds a new read-focused router rather than overloading it. - **Redis:** configured (`REDIS_URL`) but unused; **not required** for discovery MVP (Postgres queries are sufficient at this scale). - **Client:** Godot 4.6.2, minimal `main_menu.gd`; no networking client, no discovery UI. ## 4. Key design decisions | Concern | Decision | |---|---| | Listing transport | REST (`GET /api/discovery/worlds`) — no WebSocket needed for browse | | Anonymous browsing | Listing/read endpoints accept **optional** auth; visibility filtering treats anonymous users as "PUBLIC only" | | Categories | Single `category` string column on `worlds` (indexed). Free-form for MVP; a distinct-categories list is derived with a `SELECT DISTINCT` | | Tags | PostgreSQL `ARRAY(String)` column on `worlds` with a GIN index for `@>` tag containment search | | Play count | `play_count` integer (default 0) on `worlds`, incremented atomically on visit | | Recency signal | `last_played_at` timestamp on `worlds` + append-only `world_visits` table for the trending heuristic | | Trending (MVP) | Weighted score over recent visits: `sum(decay_weight(age))` over the last N days, no ML | | Sorting | `newest` (created_at desc), `most_played` (play_count desc), `trending` (score desc) | | Pagination | `skip`/`limit` (limit capped at 100), consistent with existing routers | | Visibility | Discovery only surfaces `PUBLIC` worlds; never leaks private/friends_only worlds | ## 5. User stories ### US-1 Home / discover feed As a player, I can open a home/discover page and see a default list of experiences without searching. **Acceptance criteria** - Returns PUBLIC worlds only, newest-first by default. - Paginated with a stable total count. - Works anonymously and authenticated. - Each result includes id, name, description, category, tags, owner, play_count, and created_at. ### US-2 Search As a player, I can search experiences by name and by tag. **Acceptance criteria** - Case-insensitive substring search on `name`. - Tag search matches worlds containing *any* of the requested tags. - Search combines with category filter and sort. ### US-3 Categories & sorting As a player, I can filter by category and sort by newest, most-played, or trending. **Acceptance criteria** - `category` filter matches exactly (case-insensitive). - Distinct category list is available for UI dropdowns. - `newest`, `most_played`, and `trending` sorts are supported. - `most_played` uses `play_count`; `trending` uses the recent-visits heuristic. ### US-4 Visit tracking As a player, opening a world records a visit so counts and trending stay fresh. **Acceptance criteria** - `POST /api/worlds/{id}/visit` increments `play_count` and updates `last_played_at`. - Appends a `world_visits` row (world_id, user_id or anonymous marker, visited_at). - Idempotent per request; rate-limitable later. ### US-5 Godot client As a player, I can browse, search, filter, and sort experiences from the game UI. **Acceptance criteria** - An HTTP API client handles the discovery endpoints (JWT + anonymous). - A discover/home screen lists experiences with search, category filter, and sort controls. - Results render name, category, tags, play count, and description. ## 6. Task breakdown Right-sized to 1–3 files / one coherent piece of functionality each. | # | Task | Story | Depends on | |---|------|-------|-----------| | 1 | World discovery metadata + visits model & Alembic migration | US-2, US-3, US-4 | — | | 2 | World visit tracking endpoint (play count + last played + visit log) | US-4 | 1 | | 3 | Browse + search endpoint (name/tag/category, newest + most-played) | US-1, US-2, US-3 | 1, 2 | | 4 | Trending sort (recent-activity heuristic over `world_visits`) | US-3 | 3 | | 5 | Godot discovery API client (HTTP + optional JWT) | US-5 | 3 | | 6 | Godot discover/home UI (feed, search, category filter, sort) | US-5 | 4, 5 | ## 7. Phasing - **Phase 1 (data + core read API):** tasks 1–3 (metadata, visit tracking, browse/search). - **Phase 2 (trending):** task 4 — the only non-trivial query; isolated so it can be tuned without touching the browse surface. - **Phase 3 (client):** tasks 5–6, each landing after its backend dependency. ## 8. Out of scope for this feature - Real recommendation models / personalization (collaborative filtering, ML). - Full-text search (Postgres FTS); MVP uses ILIKE substring match. - Faceted search with multiple simultaneous category/tag AND-semantics. - Featured/editorial curation and admin-curated carousels. - Search result relevance scoring beyond the three explicit sorts. - Analytics dashboards; `world_visits` is append-only signal, not a BI surface.