diff --git a/scripts/setup/machine-setup/lib/tailscale.sh b/scripts/setup/machine-setup/lib/tailscale.sh
index 3df7a3ec..916d1741 100644
--- a/scripts/setup/machine-setup/lib/tailscale.sh
+++ b/scripts/setup/machine-setup/lib/tailscale.sh
@@ -165,7 +165,7 @@ tailscale_networks_help() {
echo " before anything that can lock you out, so there is always a second"
echo " way in."
echo ""
- echo " Officer also administers it. Its Headscale app talks to headscale"
+ echo " Officer also administers it. Its Offscale plugin talks to headscale"
echo " and offscale servers alike: register as many as you run, see which"
echo " are actually up — each is probed, not remembered — and switch"
echo " between them. On whichever is active you get the nodes, the users,"
diff --git a/scripts/setup/officer-setup/lib/proxy.sh b/scripts/setup/officer-setup/lib/proxy.sh
index 69966392..4e216211 100644
--- a/scripts/setup/officer-setup/lib/proxy.sh
+++ b/scripts/setup/officer-setup/lib/proxy.sh
@@ -327,7 +327,7 @@ proxy_claim_admin() {
# Deliberately NOT written to a file. The platform's shape is that
# secrets/officer-keys.db holds ENCRYPTION KEYS, one per purpose, and the credential
# itself lives encrypted in Postgres. A third plaintext location is the pattern
- # headscale/schema.ts calls "debt to avoid copying, not a precedent to follow".
+ # the platform elsewhere calls "debt to avoid copying, not a precedent to follow".
#
# Nothing programmatic needs this after setup — only a human logging into the admin
# UI — so not storing it is a legitimate outcome rather than a gap.
diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx
index b2bbca2c..6220ee6e 100644
--- a/src/apps/officer-web/App.tsx
+++ b/src/apps/officer-web/App.tsx
@@ -95,7 +95,7 @@ export function App() {
hands the whole subtree to the plugin's own router, which react-router nests natively. */}
{installedPlugins.flatMap((plugin) => [
} />,
- // The section pair, exactly as the core screens do it (`/headscale/:section`): the plugin's
+ // The section pair, exactly as the core screens do it (`/photos/:section`): the plugin's
// panels read `useParams` themselves, so which section is open is the URL rather than state
// passed between them.
{
//
// POST rather than GET, so `EventSource` cannot be used: it sends no `Authorization` header, and these
// routes are owner-only. The client reads the body and parses frames itself, which is exactly what
-// `useCompanionLogStream` already does for the headscale container logs.
+// a plugin's own log-stream hooks already do.
// `userId` is the caller, threaded through so install can pin the new tile to THEIR dock rather than
// guessing at the owner. Only install uses it; the others take it and ignore it so the map stays one shape.
diff --git a/src/servers/app-store/availability.ts b/src/servers/app-store/availability.ts
index b44d8812..2d99a8de 100644
--- a/src/servers/app-store/availability.ts
+++ b/src/servers/app-store/availability.ts
@@ -24,9 +24,9 @@ import { CATALOGUE, type CatalogueEntry } from './catalogue';
/**
* Permission key → the sidecar that has to be installed for it to mean anything.
*
- * Includes `alsoServes`, because one sidecar can back more than one permission: Headscale serves both the
- * owner's tailnet administration and a member enrolling their own device, and only listing the first left the
- * second looking available on a machine that had no Headscale.
+ * Includes `alsoServes`, because one sidecar can back more than one permission — an owner-only admin
+ * surface and a member-facing one, say. Listing only the first left the second looking available on a
+ * machine where the sidecar was not installed at all.
*/
const PERMISSION_TO_SIDECAR = new Map(
CATALOGUE.flatMap((entry) =>
diff --git a/src/servers/app-store/catalogue.ts b/src/servers/app-store/catalogue.ts
index e6955506..2801899b 100644
--- a/src/servers/app-store/catalogue.ts
+++ b/src/servers/app-store/catalogue.ts
@@ -118,10 +118,10 @@ export type CatalogueEntry = {
/**
* Other permissions that stop working when this sidecar is absent, for availability only.
*
- * Headscale is the case: one sidecar serves both `headscale` (administering the tailnet, owner-only) and
- * `vpn` (a member enrolling their own device). With only `permission` to go on, `vpn` was never subtracted,
- * so the Permissions screen offered it on a machine with no Headscale at all — a grant that would have
- * produced a refusal the owner could not account for.
+ * The case that produced it: one sidecar served both an owner-only admin permission and a member-facing
+ * one. With only `permission` to go on, the second was never subtracted, so the Permissions screen offered
+ * it on a machine where that sidecar was not installed — a grant that would have produced a refusal the
+ * owner could not account for. (It was the tailnet, which is a plugin now, but the shape is general.)
*/
alsoServes?: string[];
/**
diff --git a/src/servers/permissions/registry.ts b/src/servers/permissions/registry.ts
index b54926fd..9d2598d7 100644
--- a/src/servers/permissions/registry.ts
+++ b/src/servers/permissions/registry.ts
@@ -243,8 +243,9 @@ const CORE_REGISTRY: Permission[] = [
// `${invite.base}/api/v1/enroll/claim`, which is a different component entirely and does not involve
// Officer. Confirmed against the mobile monorepo and the companion repo before removal.
//
- // What this does cost: `headscale` is `admin`, so with `vpn` gone no member-grantable headscale surface
- // remains. Reintroduce one here if members ever need to enrol their own devices through Officer.
+ // What this cost: the tailnet's own permission was `admin`, so with `vpn` gone no member-grantable
+ // tailnet surface remained. That permission has since left with the offscale plugin, which declares its
+ // own — so a member-facing enrolment surface would be the PLUGIN's to add, not this file's.
// Core, not app — and this was a real defect, not a preference. `/api/dashboards` is not a feature, it is
// the per-user key-value store where EVERY workspace screen keeps its layout (`screens/files`,
// `ws-layout-*`, panel config). `WorkspaceView` renders nothing until that store has loaded, so gating it
diff --git a/src/workspaces/officerdev/src/apps/Photos/shared.ts b/src/workspaces/officerdev/src/apps/Photos/shared.ts
index 21590a64..e14922a4 100644
--- a/src/workspaces/officerdev/src/apps/Photos/shared.ts
+++ b/src/workspaces/officerdev/src/apps/Photos/shared.ts
@@ -1,6 +1,6 @@
// Shared types, constants and pure helpers for the /photos workspace panels.
//
-// Unlike the offscale plugin's panels — whose sidecar reshapes Headscale's API into Officer's own types — the
+// Unlike a plugin whose sidecar reshapes an upstream API into Officer's own types — offscale does — the
// photos sidecar is a deliberate pass-through, so these types mirror Immich's wire shapes directly. They were
// generated from the LIVE instance's OpenAPI document (`GET /api/spec.json`, Immich 3.0.3), not from memory:
// several of them changed shape in 3.0 (`isArchived` became `visibility`, and the timeline bucket response