Service Registry
Service catalog, per-user URL resolution, multi-network service definitions, and the admin UI for managing the service inventory
Service Registry
The service registry is a catalog of all homelab services across both networks. It maps services to URLs, widgets, and categories. User assignment is handled separately on the user management page — the registry itself is a pure catalog of what services exist.
Architecture
The registry lives at src/lib/service-registry.ts with KV persistence at key data:service-registry. Built-in service definitions ship with the code and merge with any custom entries stored in KV. An in-memory cache with a 60-second TTL prevents repeated KV reads.
Service Definition Type
interface ServiceDef {
id: string; // 'plex-bogie', 'grafana-mm'
label: string; // 'Plex (bogie)'
icon: string; // Font Awesome class
category: 'media' | 'storage' | 'monitoring' | 'tools';
network: NetworkId; // 'milky-way' | 'andromeda'
url: string; // Primary access URL (CF tunnel)
widgetId: string; // Maps to widget-registry ID
users: string[]; // Emails granted access (empty = admin-only)
restartable?: boolean; // User can trigger container restart
containerId?: string; // Docker container name for restart API
embedUrl?: string; // For iframe-embeddable services (Grafana kiosk mode)
}
Resolution Logic
- Admin — gets all services, no filtering
- Member — gets services where:
- Their email appears in the service’s
users[]array, OR - Their
auth.services[]includes the servicewidgetIdorid
- Their email appears in the service’s
- Demo — gets no services (no URLs resolved)
Key functions:
| Function | Returns | Purpose |
|---|---|---|
getServicesForUser(auth) | ServiceDef[] | All services the user can access |
buildServiceUrlMap(auth) | Record<string, string> | Widget ID → URL map for dashboard |
getRestartableServices(auth) | ServiceDef[] | Services the user can restart |
getServiceUrl(auth, widgetId) | string | null | Single URL lookup by widget |
Admin UI (/admin/services)
The service registry admin page at /admin/services displays all services in a sortable table.
Table Columns
| Column | Description |
|---|---|
| Label | Service display name |
| Widget ID | Maps to widget-registry entry |
| Category | media, monitoring, storage, or tools |
| Network | milky-way or andromeda |
| URL | Primary access URL |
| Actions | Edit / Delete buttons |
Category Color Coding
Table rows have a color-coded left border by category:
- Media — pink border
- Monitoring — cyan border
- Storage — green border
- Tools — purple border
Add/Edit Service
The modal form collects:
- Label — display name
- Service ID — unique identifier (auto-suggested from label on create)
- Widget ID — select populated from the widget registry at
src/lib/widget-registry.ts - Category — media, monitoring, storage, or tools
- Network — milky-way or andromeda
- URL — primary access URL
- Icon — Font Awesome icon class
- Embed URL — optional iframe URL
- Container ID — optional Docker container name for restart
- Restartable — toggle for user-initiated restarts
- Helper text — descriptions explain each field’s purpose
User Assignment
User-to-service assignment is done on the user edit form at /admin/users, not on the service form. When editing a user, the Services & Features section shows checkboxes for all available services (deduplicated by widget ID from the registry). This keeps the service registry as a pure catalog.
Built-in Services
Twelve services ship by default across two networks:
Andromeda Network (Andromeda)
| Service ID | Widget | URL | Restartable |
|---|---|---|---|
plex-daniel | plex | plex.Arcturus-Prime.com | No |
plex-bogie | plex | plex-bogie.Arcturus-Prime.com | Yes |
plex-mauve | plex | plex.Arcturus-Prime.com | No |
rutorrent-bogie | rutorrent | bogie.Arcturus-Prime.com | Yes |
audiobookshelf | audiobookshelf | audiobooks.Arcturus-Prime.com | No |
tautulli-daniel | tautulli | tautulli.Arcturus-Prime.com | No |
tautulli-bogie | tautulli | tautulli-bogie.Arcturus-Prime.com | No |
grafana-mm | grafana-embed | grafana-mm.Arcturus-Prime.com | No |
speedtest-mm | speedtest | speedtest-mm.Arcturus-Prime.com | No |
unraid-dashboard | storage-overview | (proxy) | No |
Milky Way Network (Milky Way)
| Service ID | Widget | URL | Restartable |
|---|---|---|---|
rutorrent-daniel | rutorrent | argonaut.Arcturus-Prime.com | No |
rutorrent-mauve | rutorrent | mauve.Arcturus-Prime.com | No |
Admin API
Manage services at /api/admin/services. All endpoints require admin authentication.
| Method | Action | Body |
|---|---|---|
GET | List all services (built-in + custom) | — |
POST | Create or update a service | ServiceDef JSON |
DELETE | Remove a custom service | { "id": "service-id" } |
Built-in services cannot be deleted but can be overridden by posting a service with the same ID.
Service Restart
Members can restart services marked restartable: true through the /api/user/service-restart endpoint.
Safety rules:
- User must have access to the service (registry check)
- Service must have
restartable: trueand acontainerId - For non-admins: the container must be currently running — if an admin deliberately stopped a container, the restart is blocked with “Service is currently disabled by admin”
- Admins bypass the running-state check
The restart calls the mm-Arcturus-Prime proxy to issue POST /api/containers/{containerId}/restart on Meridian-Host.
Dashboard Integration
The dashboard frontmatter resolves URLs at render time:
const { serviceUrls, serviceEmbedUrls } = await buildServiceUrlMap(auth);
const restartableServices = await getRestartableServices(auth);
Widget links use serviceUrls[widgetId] instead of hardcoded URLs. Restart buttons appear next to restartable services with visual feedback (spinner → checkmark/error).