Multi-Tenant Desktop — Koder Linux Workspaces
Architectural direction for layering a workspace abstraction over the single-user Koder Linux desktop, so a single installed system can host multiple isolated user contexts (work / personal / per-client) switchable from the kolide-shell panel — without falling back to the much heavier "second Linux user account + relogin" pattern. Each workspace owns its own per-user storage tree, Koder ID binding, apps catalogue, and kolide-shell settings. Apps stay multi-tenant by default (`policies/multi-tenant-by-default.kmd`) so this RFC is the desktop-side surface that finally makes the policy visible to the end user.
RFC — Multi-Tenant Desktop (Koder Linux Workspaces)
Version: 0.1.0 — Draft Status: Proposed (20260518) Owner: Koder Linux WG (infra/linux/distro + infra/linux/kolide) Discussion ticket: infra/linux/distro #019
*cope.*This RFC governs only the desktop notion of "multi- tenant" — a single OS user (the live install's
koderaccount) housing N isolated workspace contexts switchable at runtime. Multi- OS-user (two Linux accounts on the same install) is what Debian already does and stays out of scope.Servers, Flow tenants, and any backend isolation are governed by
specs/multi-tenancy/contract.kmd. This RFC reads that spec as a hard upstream contract and only adds the desktop-shell affordances.
D1 — Why now
Three forcing functions converged:
policies/multi-tenant-by-default.kmdratified that every KoderschemaAPIstorage carries
koder_user_idfrom commit 1. Apps on the desktop already plumb this through (e.g.products/horizontal /dekslice 1+2 of #100). What's missing is the user-facing handle: today the user sees one global state and has to trust that the storage layer separates per-user — but they have no visible way to switch to another identity without logging out of the whole desktop.infra/linux/kolide #008shipped session-wide Koder ID OAuth(libsecret
backed). The tokenstore schema (dev.koder.kolide .token) is keyed by{component, user_id}— already tenancy-aware, but only stores one entry. Trivial to extend to N.- Field requests: lawyers + consultants who service multiple
clients want one device with strict per-client isolation, not N devices and not N Linux accounts.
D2 — Definition
A *orkspace*in Koder Linux is:
- A persistent, named context (
work,personal,client-acme). - Owned by exactly one Koder ID identity (one
koder_user_id). - Backed by its own state tree, its own apps catalogue, its own
kolide-shell settings, its own notification history, its own Koder Drive root.
- Switchable in O(100 ms) from the kolide-shell panel without
closing apps in other workspaces (apps are scoped to one workspace and pause/hide when their workspace is inactive).
A workspace is *ot*
- A separate Linux user account (that's
useradd, not a Kolide concept). - A virtual machine (no kernel isolation; one kernel, one
compositor, one D-Bus).
- A federation primitive (cross-device sync is a separate RFC).
D3 — Storage layout
Perworkspace XDG dirs land under a workspacescoped root:
$HOME/
└── .config/koder/workspaces/
├── <workspace-id>/ # uuid v4
│ ├── meta.toml # name, koder_user_id, created_at, icon
│ ├── XDG_CONFIG_HOME/ # $HOME/.config/koder/workspaces/<id>/XDG_CONFIG_HOME
│ ├── XDG_DATA_HOME/ # similar
│ ├── XDG_CACHE_HOME/ # similar
│ ├── XDG_STATE_HOME/ # similar
│ └── kolide/ # kolide-shell per-workspace state
│ ├── settings.json
│ ├── notifications.jsonl
│ └── first-run-apps.json
└── active # symlink → <workspace-id> (current)The kolide-services apply_setting and notifications_history_* helpers (already file-rooted at $HOME/.config/kolide/) become file-rooted at ~/.config/koder/workspaces/active/kolide/ — the symlink swap is the entirety of "switch workspace" at the storage layer.
Apps (Drive, Dek, Kortex, etc.) read XDG_*_HOME from environment. kolideservices overrides those perspawn so an app launched from inside workspace acme sees XDG_CONFIG_HOME=~/.config/koder/
workspaces/<acme-uuid>/XDG_CONFIG_HOME instead of the OS default.
D4 — Koder ID binding
libsecret schema extends from {component, user_id} to {component, user_id, workspace_id}. Each workspace has its own access+refresh token pair under its own workspace_id attribute. Login from QS in workspace A places a token there; switching to workspace B finds B's token (or none, kicking the OAuth flow).
org.koder.Kolide.Auth.StateChanged signal payload gains a fourth field: workspace_id. Consumers that don't care (legacy panels) read the first three fields and ignore.
D5 — UI surface in kolide-shell
- *anel badge*(kolide #008) gains a small workspace icon overlay
on the avatar. Click opens a dropdown:
` ┌─────────────────────────────────┐ │ • work (rpm@koder.dev) │ ← active │ personal (rpm@gmail.com) │ │ client-acme (acme@…) │ ├─────────────────────────────────┤ │ + New workspace… │ │ Manage workspaces… │ └─────────────────────────────────┘
`
- *uick Settings*grows a new "Workspaces" section with the same
dropdown plus per-workspace toggles.
- *ock screen*asks for the active workspace's identity, not
the OS user.
D6 — Performance budget
- Switching workspaces (no relaunch) ≤ *00 ms*wall-clock from
click to panel state visible.
- App pause
ondeactivate uses SIGSTOP / cgroup freezer; resume onreactivate. Wakelocks are tracked per-workspace to preserve battery on laptops.
D7 — Migration plan
Single-workspace systems (every install today) get a "default" workspace transparently:
- On first kolide-services boot after this lands, if
~/.config/koder/workspaces/doesn't exist, create one workspace nameddefaultand migrate `~.configkolide