Discovery & Search
Explore, trending, search, games, sponsors, and creator links. All endpoints require authentication — there is no anonymous browsing.
Trending
| Method | Path | Returns |
|---|---|---|
GET | /trending/clips | Trending video clips |
GET | /trending/playpalz | Trending creators |
GET | /trending/coaches | Trending coaches |
GET | /trending/games | Trending games |
GET | /trending/searches | Popular search queries |
All five wrap their payload:
{ "success": true, "data": [ … ] }Caching
Every trending endpoint caches in Redis for 5 minutes (CACHE_TTL = 60 * 5), keyed per endpoint and, where personalised, per user. Expiry only — there is no invalidation, so a change takes up to five minutes to surface.
PlayPalz vs coaches
Both are creators; the distinction is by badge. The PlayPal badge is assigned automatically to creators, and the coach badge is assigned manually. /trending/coaches filters on the coach badge. See Domain Models.
Trending searches
Backed by the SearchLog model — every query is logged with query and createdAt, both indexed, and aggregated into the popular list.
Search
| Method | Path | Returns |
|---|---|---|
GET | /search?q=<query> | Users, clips, and games in one response |
GET | /search/users?q=<query> | Users only |
GET /search?q=valorant
{
"success": true,
"data": { "users": [ … ], "clips": [ … ], "games": [ … ] }
}An empty or whitespace-only q short-circuits to empty results without touching the database.
Note the inconsistency: /search uses the { success, data } envelope while /search/users returns a bare array.
Discover
| Method | Path | Purpose |
|---|---|---|
GET | /discover | The discover feed |
The discover query is a placeholder
export const fetchDiscoverFeed = async (authenticatedUserId, limit?, cursor?) => {
return prisma.user.findMany({ where: { NOT: { id: authenticatedUserId } } });
};It selects every user in the database except you — no ranking, no filtering, no pagination, and the limit and cursor parameters are accepted and ignored. It works at demo scale and will fall over as the user base grows.
A real implementation should filter to creators (userType = "playpal"), exclude blocked users, rank by something meaningful (recency, engagement, shared game interests), and honour the pagination parameters it already accepts. Partial
The /trending/* endpoints are considerably more developed and are what the explore screen actually uses.
Games
| Method | Path | Purpose |
|---|---|---|
GET | /games | The catalog |
PUT | /user/games | Set your favourite games |
Populated by igdb-heartbeat — roughly 300 published games at the time of writing, refreshed daily. Favourites are chosen during onboarding and feed discovery.
Sponsors
| Method | Path | Purpose |
|---|---|---|
GET | /sponsor | The currently active sponsor |
Sponsored placements rendered in discovery surfaces.
Links
Creator external links — merch stores, socials — shown on the profile, similar to Instagram's link list.
| Method | Path | Purpose |
|---|---|---|
GET | /links | Your links |
GET | /links/:linkId | One link |
POST | /links | Create |
PUT | /links/:linkId | Update |
DELETE | /links/:linkId | Delete |
A Link is { name, url } scoped to a user. Zod schemas live in apps/api/src/schemas/link.ts.
Notifications
| Method | Path | Purpose |
|---|---|---|
GET | /notifications | Cursor-paginated inbox, ?unreadOnly=true |
GET | /notifications/unread-count | Badge count |
POST | /notifications/:id/read | Mark one read |
POST | /notifications/read-all | Mark all read |
The reference implementation for cursor pagination in this codebase. Details in Notifications.
