# Server Directory Domain Manages the list of server endpoints the client can connect to, health-checking them, resolving API URLs, and providing server CRUD, search, invites, and moderation. This is the central domain that other domains (auth, chat, attachment) depend on for knowing where the backend is. ## Module map ``` server-directory/ ├── application/ │ ├── facades/ │ │ └── server-directory.facade.ts Thin domain boundary, delegates to ServerDirectoryService │ └── services/ │ ├── server-directory.service.ts Orchestrator: server CRUD, search, health, invites, moderation │ └── server-endpoint-state.service.ts Signal-based endpoint list, reconciliation with defaults, localStorage persistence │ ├── domain/ │ ├── constants/ │ │ └── server-directory.constants.ts CLIENT_UPDATE_REQUIRED_MESSAGE │ ├── logic/ │ │ ├── room-signal-source.logic.ts Room → signal-source selector resolution │ │ ├── room-signal-source.logic.spec.ts Unit tests │ │ └── server-endpoint-defaults.logic.ts Default endpoint templates, URL sanitisation, reconciliation helpers │ └── models/ │ └── server-directory.model.ts ServerEndpoint, ServerInfo, ServerJoinAccessResponse, invite/ban/kick types │ ├── infrastructure/ │ ├── constants/ │ │ └── server-directory.infrastructure.constants.ts Health-check timeout, localStorage keys │ └── services/ │ ├── server-directory-api.service.ts HTTP client for all server API calls │ ├── server-endpoint-compatibility.service.ts Semantic version comparison for client/server compatibility │ ├── server-endpoint-health.service.ts Health probe (GET /api/health with 5 s timeout, fallback to /api/servers) │ └── server-endpoint-storage.service.ts localStorage read/write for endpoint list and removed-default tracking │ ├── feature/ │ ├── invite/ Invite creation and resolution UI │ ├── server-search/ Server search/browse panel │ └── settings/ Server endpoint management settings │ └── index.ts Barrel exports ``` ## Layer composition The facade is a thin pass-through that delegates to `ServerDirectoryService`. The service delegates HTTP work to the API service and endpoint state to the state service. Health probing combines the health service and compatibility service. Storage is accessed only through the state service. ```mermaid graph TD Facade[ServerDirectoryFacade] Service[ServerDirectoryService] State[ServerEndpointStateService] API[ServerDirectoryApiService] Health[ServerEndpointHealthService] Compat[ServerEndpointCompatibilityService] Storage[ServerEndpointStorageService] Defaults[server-endpoint-defaults.logic] Models[server-directory.model] Facade --> Service Service --> API Service --> State Service --> Health Service --> Compat API --> State State --> Storage State --> Defaults Health --> Compat click Facade "application/facades/server-directory.facade.ts" "Thin domain boundary" _blank click Service "application/services/server-directory.service.ts" "Orchestrator" _blank click State "application/services/server-endpoint-state.service.ts" "Signal-based endpoint state" _blank click API "infrastructure/services/server-directory-api.service.ts" "HTTP client for server API" _blank click Health "infrastructure/services/server-endpoint-health.service.ts" "Health probe" _blank click Compat "infrastructure/services/server-endpoint-compatibility.service.ts" "Version compatibility" _blank click Storage "infrastructure/services/server-endpoint-storage.service.ts" "localStorage persistence" _blank click Defaults "domain/logic/server-endpoint-defaults.logic.ts" "Default endpoint templates" _blank click Models "domain/models/server-directory.model.ts" "Domain types" _blank ``` ## Endpoint lifecycle On startup, `ServerEndpointStateService` loads endpoints from localStorage, reconciles them with the configured defaults from the environment, and ensures at least one endpoint is active. Configured default endpoints are treated as active by default unless the user explicitly disabled or removed them. ```mermaid stateDiagram-v2 [*] --> Load: constructor Load --> HasStored: localStorage has endpoints Load --> InitDefaults: no stored endpoints InitDefaults --> Ready: save default endpoints HasStored --> Reconcile: compare stored vs defaults Reconcile --> Ready: merge, ensure active Ready --> HealthCheck: facade.testAllServers() state HealthCheck { [*] --> Probing Probing --> Online: /api/health 200 OK Probing --> Incompatible: version mismatch Probing --> Offline: request failed } ``` ## Health probing The facade exposes `testServer(endpointId)` and `testAllServers()`. Both delegate through the service to `ServerEndpointHealthService.probeEndpoint()`, which: 1. Sends `GET /api/health` with a 5-second timeout 2. Reads the response's `serverVersion` and stable `serverInstanceId` 3. Checks the reported version against the client version via `ServerEndpointCompatibilityService` 4. If versions are incompatible, the endpoint is marked `incompatible` and deactivated 5. If `/api/health` fails, falls back to `GET /api/servers` as a basic liveness check 6. Updates the endpoint's status, latency, and version info in the state service `serverInstanceId` lets the client detect when multiple configured URLs point at the same backend. `ServerEndpointStateService.resolveCanonicalEndpoint()` prefers one canonical endpoint per backend instance so REST calls, WebSocket routing, and room fallback logic do not treat same-instance aliases as different signaling clusters. Room signaling now waits for that initial health sweep before the first saved-room reconnect attempt. That avoids a cold-start race where alias endpoints could open separate WebSocket managers before `serverInstanceId` had been learned. ```mermaid sequenceDiagram participant Facade participant Health as HealthService participant Compat as CompatibilityService participant API as Server Facade->>Health: probeEndpoint(endpoint, clientVersion) Health->>API: GET /api/health (5s timeout) alt 200 OK API-->>Health: { serverVersion, serverInstanceId } Health->>Compat: evaluateServerVersion(serverVersion, clientVersion) Compat-->>Health: { isCompatible, serverVersion } Health-->>Facade: online / incompatible + latency + versions else Request failed Health->>API: GET /api/servers (fallback) alt 200 OK API-->>Health: servers list Health-->>Facade: online + latency else Also failed Health-->>Facade: offline end end Facade->>Facade: updateServerStatus(id, status, latency, versions) ``` ## Server search The facade's `searchServers(query)` method supports two modes controlled by a `searchAllServers` flag: - **Single endpoint**: searches only the active server's API - **All endpoints**: fans out the query to every online active endpoint via `forkJoin`, then deduplicates results by server ID The API service normalises every `ServerInfo` response, filling in `sourceId`, `sourceName`, and `sourceUrl` so the UI knows which endpoint each server came from. That search fan-out is discovery only. Once a room is created or joined, the room keeps an authoritative signal-server affinity via its `sourceId` / `sourceUrl`. The join response can repair stale saved metadata, and reconnect logic now retries that authoritative endpoint first before probing any other configured endpoints. Fallback stays temporary. If the authoritative endpoint is unavailable, the client can probe other active compatible endpoints as a last resort for the current session, but it does not rewrite the room's saved affinity to that fallback endpoint. ## Server-owned room metadata `ServerInfo` also carries the server-owned `channels` list for each room. Register and update calls persist this channel metadata on the server, and search or hydration responses return the normalised channel list so text and voice channel topology survives reloads, reconnects, and fresh joins. The renderer may cache room data locally, but channel creation, rename, and removal must round-trip through the server-directory API instead of being treated as client-only state. Server-side normalisation deduplicates channel names within each channel type, so a text `general` channel and a voice `General` channel can coexist while duplicate voice-to-voice or text-to-text names are still rejected. ## Default endpoint management Default servers are configured in the environment file. The state service builds `DefaultEndpointTemplate` objects from the configuration and uses them during reconciliation: - Stored endpoints are matched to defaults by `defaultKey` or URL - Missing defaults are added unless the user explicitly removed them (tracked in a separate localStorage key) - Default endpoints stay active by default unless the user explicitly disabled them (tracked separately from the endpoint payload) - `restoreDefaultServers()` re-adds any removed defaults and clears the removal tracking - The primary default URL is used as a fallback when no endpoint is resolved Saved rooms can also self-heal their endpoint metadata. If a room has missing or stale source information, the client now searches the configured endpoints for that room, restores the correct source mapping, and persists the repair locally. URL sanitisation strips trailing slashes and `/api` suffixes. Protocol-less URLs get `http` or `https` based on the current page protocol. ## Server administration The facade provides methods for server registration, updates, and unregistration. These map directly to the API service's HTTP calls: | Method | HTTP | Endpoint | |---|---|---| | `registerServer` | POST | `/api/servers` | | `updateServer` | PUT | `/api/servers/:id` | | `unregisterServer` | DELETE | `/api/servers/:id` | ## Invites and moderation | Method | Purpose | |---|---| | `createInvite(serverId, request)` | Creates a time-limited invite link | | `getInvite(inviteId)` | Resolves invite metadata | | `requestServerAccess(request)` | Joins a server (via membership, password, invite, or public access) | | `kickServerMember(serverId, request)` | Removes a user from the server | | `banServerMember(serverId, request)` | Bans a user with optional reason and expiry | | `unbanServerMember(serverId, request)` | Lifts a ban | ## Persistence All endpoint state is persisted to localStorage under two keys: | Key | Contents | |---|---| | `metoyou_server_endpoints` | Full `ServerEndpoint[]` array | | `metoyou_removed_default_server_keys` | Set of default endpoint keys the user explicitly removed | The storage service handles JSON serialisation and defensive parsing. Invalid data falls back to empty state rather than throwing.