# Soulseek — the extraction record Extracted from the platform on **2026-08-19**, the seventh plugin out, after transmission. `plugins/EXTRACTING-A-PLUGIN.md` is the runbook. ## This one was a resurrection, not a move Every other extraction so far moved working code. **Soulseek was already dead when it left**, and had been since 2026-08-13: - `src/servers/hono.ts` had both the import and the `PROTECTED_MOUNTS` entry commented out (`// plugin — switched off 2026-08-13`), so nothing mounted `/api/slskd`. - Because `api/slskd/router.ts` was therefore imported by nobody, `createSidecarProxy` never ran — so `registerProxiedPrefix('/api/slskd')` was never called and the `slskd:server` port announcement fired into a void. - `officer-slskd` was not in `ecosystem.config.cjs` at all, so the sidecar was not even started. - The permission entry had been reduced to `api: []`, which is why `assertPermissionTotality` was satisfied and the server booted cleanly. **The UI rendered perfectly and every request 404'd.** That last point is the one worth keeping. The boot check is a totality check, not a liveness check: it proves every mounted prefix has a permission, and is silent about a feature that mounts nothing at all. A screen whose panels all fail is invisible to it. There was also a live data hazard, unrelated to the routing: `src/databases/officer_db/src/schema.ts` had `export * from './soulseek/schema'` commented out, so the three tables were **not in the drizzle barrel** — `bun db:push` would have dropped `soulseek_favorites`, `soulseek_browse_snapshots` and `soulseek_browse_dirs` on any server holding them. `scripts/rebuild-soulseek-tree.ts` documented the workaround rather than the problem. Moving the schema into the plugin closes that. ## What moved | Was | Is now | | ------------------------------------------------------- | --------------- | | `src/servers/sidecar/slskd/*` (6 files) | `sidecar/*` | | `src/servers/api/slskd/router.ts` | `api/router.ts` | | `src/databases/officer_db/src/soulseek/*` (3 files) | `db/*` | | `src/workspaces/officerdev/src/apps/Soulseek/*` (20 files) | `web/*` | | `src/apps/officer-web/Screens/Dashboard/Soulseek/*` | `web/layout.ts` | | `public/slskd.png` | `icon.png` | | `scripts/rebuild-soulseek-tree.ts` | deleted | ## The prefix changed, and that is not cosmetic The mount moved from `/api/slskd` to `/api/soulseek`, because `mountPrefix` derives it from the **appName** and this plugin is `soulseek`. Three consequences, all handled here: 1. All 47 frontend call sites were repointed from `/slskd/...` to `/soulseek/...`. 2. `useServiceConnection('slskd')` became `useServiceConnection('soulseek')` — that hook builds `` `/${service}/_config` ``, so its argument is the **mount name**, not the credential key. 3. The credential key in `service_connections` stays **`'slskd'`**. It names the daemon, not the route. Point 3 is the one that will trip somebody. `service` in the database and the URL segment are now different strings on purpose, and conflating them silently reads the wrong row. ## The part that is not self-contained **The connection lives in `service_connections`, a CORE table** — the same one transmission uses. `'slskd'` is a member of the platform's `ServiceName` union, which a plugin cannot extend. Deliberate, and the table says so in its own comment: it exists for *"services the owner has exactly ONE of"*. Nobody has two Soulseek daemons, so there is no label, no `is_active` and no switcher. It also gives encryption at rest for free, which a private table would have to reimplement. The cost, plainly: **this plugin depends on a core facility it does not own.** If `service_connections` changes shape, this breaks with it. Unlike transmission, this plugin **does** declare a schema — the connection is core, but favourites and the cached browse tree are its own. ## Fixtures this broke, and what replaced them `slskd` was a canonical app-store test fixture in three files, the third extraction in a row to break them. `installer.test.ts` now builds a **literal** fixture rather than reaching into the catalogue: ```ts const noMemberConcept = { ...byId('jellyfin')!, id: 'test-single-tenant', members: 'none' as const }; ``` That test pins `planSteps`, which is installer logic. Binding it to catalogue contents is why it broke three times; no catalogue entry is both `provisioned` and `members: 'none'` any more, so there was nothing left to borrow. `catalogue.test.ts` and `members.test.ts` were re-anchored to `vnc`/`notify`/`gitea`, which are real remaining single-tenant entries. ## Not carried over `composeTemplate: 'slskd'` pointed at a template directory containing **only `assets/icon.png` and no `docker-compose.yaml`**, so the catalogue's `modes: ['existing', 'provisioned']` was already unbackable. Like photos and transmission, this plugin is **point-at-your-own-daemon only** until plugin Docker provisioning exists. The SignalR hub proxy (`/hub/*`) was a TODO in the old sidecar and never built. Live search results still arrive by polling.