Files
Toju/agents-docs/features/server-discovery.md
T
2026-07-14 00:41:05 +02:00

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 ServerDirectoryFacade and render them via the reusable app-server-browser component on /servers and /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 lastSeen descending, ties broken by membership count (rankTrendingServers).
  • Discovery limit: each route clamps limit to [1, 50] (parseDiscoveryLimit), default 12.

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], default 12)

GET /api/servers/trending

  • Method: GET
  • Authentication: None (public discovery)
  • Rate Limiting: No
  • Query params: limit (optional integer; clamped to [1, 50], default 12)

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 via GetFeaturedServers / GetTrendingServers query 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 private getDiscoveryServers(path) helper and normalise into ServerInfo[].
  • 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 /servers view appear empty when populated servers lived on other endpoints.
  • Legacy 404 fallback: when GET /api/servers/featured or /trending returns 404 (older signal servers resolve those paths as /servers/:id), fetchDiscoveryFromEndpoint falls back per-endpoint to the public GET /api/servers listing (fetchPublicServerListForDiscovery) instead of returning []. Verified in server-directory-api.service.spec.ts (including production hosts like signal.toju.app).
  • ServerDirectoryServiceServerDirectoryFacade expose getFeaturedServers() / getTrendingServers() as the domain boundary.
  • FindServersComponent (/servers) composes Recently active (the user's saved rooms, capped at 6), Featured, and Trending sections, all rendered through app-server-browser with [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 with Ctrl+K focus 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 loads popularServers on init from getFeaturedServers(5), falling back to getTrendingServers(5) when featured is empty; reuses app-friend-button for Add and app-user-avatar for people rows. peopleYouMightKnow excludes existing friends (via FriendService.friendIds()); friends lists discovered people who are friends. "See all" header links route to the matching /people or /servers page (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted under metoyou_dashboard_recent_searches.
  • The servers-rail top button (servers-rail.component) is the Dashboard button (lucideLayoutDashboard, title="Dashboard"); its goToDashboard() 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 opens app-create-server-dialog (a Toju modal on desktop / bottom sheet on mobile) which dispatches RoomsActions.createRoom directly; the dashboard / /create-server route remains as an alternative entry point. Rail icons (h-12 w-12, md:h-11 w-11) animate their corner radius on hover and :active for 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 in app.html (shouldShowMobileAppServersRail in core/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 (see toju-app/AGENTS.md).
  • 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