7.5 KiB
Server Discovery
Area: server-directory Status: Active Last updated: 2026-07-05
Overview
Server discovery lets a signed-in user find public servers to join without knowing an exact name. It spans the signaling server (REST routes + CQRS query handlers that rank public servers) and the product client (server-directory domain API/facade plus the /dashboard landing and /servers browse page). It complements the existing free-text GET /api/servers search with two curated lists — featured and trending.
Responsibilities
- Server: rank and return public servers as featured (most-populated) and trending (most-recently-active) lists, capped per request.
- Client: fetch those lists through
ServerDirectoryFacadeand render them via the reusableapp-server-browsercomponent on/serversand/dashboard. - It does NOT own: free-text search (
GET /api/servers), join/access checks (/api/servers/:id/join), invites, or room signal-affinity. Discovery is read-only browsing; joining flows through existing paths.
Key concepts
- Featured: public servers ranked by membership count descending, ties broken by most recent
lastSeen(rankFeaturedServers). - Trending: public servers ranked by most recent
lastSeendescending, ties broken by membership count (rankTrendingServers). - Discovery limit: each route clamps
limitto[1, 50](parseDiscoveryLimit), default12.
API Endpoints
Both endpoints live in server/src/routes/servers.ts and must be registered before the parameterised /:id route, otherwise Express resolves featured/trending as a server id.
GET /api/servers/featured
- Method: GET
- Authentication: None (public discovery)
- Rate Limiting: No
- Query params:
limit(optional integer; clamped to[1, 50], default12)
GET /api/servers/trending
- Method: GET
- Authentication: None (public discovery)
- Rate Limiting: No
- Query params:
limit(optional integer; clamped to[1, 50], default12)
Response Schema (both)
{
"servers": "ServerInfo[] — enriched public servers (icon, channels, sourceId/sourceName/sourceUrl filled by the client API layer)",
"total": "number — count of servers returned",
"limit": "number — the effective clamped limit"
}
ServerInfo matches the shape returned by GET /api/servers search results, so the client normalises and renders all three lists identically.
Error Responses
- 500 Internal Server Error: query handler / persistence failure.
Server internals
- Routes delegate to CQRS query handlers
handleGetFeaturedServers/handleGetTrendingServers(server/src/cqrs/queries/handlers/), dispatched viaGetFeaturedServers/GetTrendingServersquery types. - Ranking lives in
server/src/cqrs/queries/handlers/server-ranking.util.ts(rankFeaturedServers,rankTrendingServers,loadMembershipCounts). Membership counts load in a single grouped query. - Results pass through the same
enrichServer()step as search before serialisation.
Client internals
ServerDirectoryApiService.getFeaturedServers()/getTrendingServers()call the routes through a shared privategetDiscoveryServers(path)helper and normalise intoServerInfo[].- Multi-endpoint fan-out: discovery queries every online endpoint (
getSearchableEndpoints()+forkJoin), deduplicated by server ID — mirroring free-text search. Querying only the active endpoint made the default/serversview appear empty when populated servers lived on other endpoints. - Legacy 404 fallback: when
GET /api/servers/featuredor/trendingreturns 404 (older signal servers resolve those paths as/servers/:id),fetchDiscoveryFromEndpointfalls back per-endpoint to the publicGET /api/serverslisting (fetchPublicServerListForDiscovery) instead of returning[]. Verified inserver-directory-api.service.spec.ts(including production hosts likesignal.toju.app). ServerDirectoryService→ServerDirectoryFacadeexposegetFeaturedServers()/getTrendingServers()as the domain boundary.FindServersComponent(/servers) composes Recently active (the user's saved rooms, capped at 6), Featured, and Trending sections, all rendered throughapp-server-browserwith[showMyServers]="true".DashboardComponent(/dashboard) is a single-column landing page (max-width centered, no in-page sidebars): a header greeting (no emoji), a global search withCtrl+Kfocus and localStorage-backed Recent Searches chips shown beneath it, three primary action cards (Find People →/people, Find Servers →/servers, Create Server →/create-server— one link each), and discovery panels People you might know, Popular Servers, Your Friends, and Recently Active Servers. Each list is capped at 5 (DISCOVERY_LIMIT). It loadspopularServerson init fromgetFeaturedServers(5), falling back togetTrendingServers(5)when featured is empty; reusesapp-friend-buttonfor Add andapp-user-avatarfor people rows.peopleYouMightKnowexcludes existing friends (viaFriendService.friendIds());friendslists discovered people who are friends. "See all" header links route to the matching/peopleor/serverspage (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted undermetoyou_dashboard_recent_searches.- The servers-rail top button (
servers-rail.component) is the Dashboard button (lucideLayoutDashboard,title="Dashboard"); itsgoToDashboard()handler deselects any active voice server and navigates to/dashboard. A Create a server button (lucidePlus,data-testid="server-rail-create") sits below the saved-server icons and opensapp-create-server-dialog(a Toju modal on desktop / bottom sheet on mobile) which dispatchesRoomsActions.createRoomdirectly; the dashboard //create-serverroute remains as an alternative entry point. Rail icons (h-12 w-12,md:h-11 w-11) animate their corner radius on hover and:activefor a Discord-style squircle effect. - On mobile (
ViewportService.isMobile()), discovery routes (/dashboard,/people,/servers) render their page body full-width via<ng-template #pageContent>+[ngTemplateOutlet]. The servers rail is global inapp.html(shouldShowMobileAppServersRailincore/platform/mobile-shell-layout.rules.ts) — discovery pages must not embed a second<app-servers-rail>or Swiper stack. Chat-room and DM-workspace routes keep their own embedded rail inside Swiper and hide the global shell rail (seetoju-app/AGENTS.md).
Related
- Product-client domain README:
toju-app/src/app/domains/server-directory/README.md - Full server-directory REST contract (CRUD, join, moderation): server-directory.md
- People discovery (
/people):toju-app/src/app/domains/direct-message/README.md - Mobile shell: mobile-capacitor.md
Changelog
| Date | Change |
|---|---|
| 2026-07-05 | Added multi-endpoint fan-out and 404 fallback; corrected mobile shell layout (global rail, no per-page Swiper) |
| 2025-02-14 | Initial documentation |