app store: make member provisioning a mechanism, not an install-time loop
The owner installs, but a server may already have members, and a member added next month needs the same work. So the unit is (service × member) reachable from two triggers — install a service, provision existing members; add a member, provision installed services — rather than a loop inside the installer. Only handling the first works on day one and rots. No new table. A member is provisioned exactly when they hold a service_connections row: their own credential, url NULL, inheriting the instance from the owner's. That schema anticipated this before this existed, and a second record of the same fact would only be able to disagree with the first. Three outcomes, declared per catalogue entry so the installer never special-cases a service. `accounts` is fully transparent. `none` is a single-tenant daemon with nothing to do — filtered before the provisioning loop so callers can tell "nothing to do" from "did nothing", which look identical at a call site and matter when someone is asking why a member cannot see a feature. `invite` is not a weaker `accounts`, it is the correct outcome: Vaultwarden derives its encryption key from the master password, so a credential we could mint would mean a vault we could read. Transparent right up to where being transparent would be a defect. The per-service work is an interface implemented beside each sidecar rather than a switch in core — a central function growing a case per service is what would stop any of this shipping from its own repository. Implementations must be idempotent, since both triggers can fire for the same pair and a duplicate account upstream is not ours to undo. Deprovision is optional and defaults to leaving the upstream account alone: deleting an Immich user deletes their photos. Written assuming the vault's multi-user adaptation has landed. Today /api/vault is owner-only by an explicit ownerGate, so a member is refused before Vaultwarden is reached — verified, and out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -246,6 +246,49 @@ finishes the job — which is what `completedSteps` was for.
|
||||
|
||||
---
|
||||
|
||||
## Members get their own accounts
|
||||
|
||||
The owner installs, but a server may already have members — and a member added next month needs the same
|
||||
work done. So the unit is **(service × member)**, reachable from two triggers:
|
||||
|
||||
```
|
||||
install a service -> provision every member who already exists
|
||||
add a member -> provision every service already installed
|
||||
```
|
||||
|
||||
Only handling the first is the classic thing that works on day one and rots quietly. There is no new
|
||||
table: a member is provisioned for a service exactly when they hold a `service_connections` row for it —
|
||||
their own credential, `url` NULL, inheriting the instance from the owner's. That schema was built for
|
||||
this before this existed.
|
||||
|
||||
Three outcomes, declared per catalogue entry as `members`, so the installer never special-cases a
|
||||
service:
|
||||
|
||||
| | Meaning | Services |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| `accounts` | Admin API creates the user **and** mints a credential. Fully transparent — the member just finds it working. | Immich, Jellyfin, Memos, InvoiceShelf, CalDAV |
|
||||
| `invite` | The account can be created; a usable credential cannot. The member sets their own password. | Vaultwarden |
|
||||
| `none` | Single-tenant daemon, no user concept. Access is mediated by Officer alone. | Transmission, slskd, headscale, email, music, wallet, notify, vnc |
|
||||
|
||||
**`invite` is not a weaker `accounts`** — it is the correct outcome. Vaultwarden derives its encryption
|
||||
key from the master password, so a credential we could mint would mean a vault we could read. Transparent
|
||||
right up to the point where being transparent would be a defect.
|
||||
|
||||
The per-service work is an **interface implemented beside each sidecar**, never a switch in core: a
|
||||
central function growing one case per service is exactly what would stop any of this shipping from its
|
||||
own repository. Implementations must be idempotent — both triggers can fire for the same pair, and
|
||||
creating a second account upstream is not something we can undo.
|
||||
|
||||
Deprovision is deliberately optional and defaults to doing nothing upstream. Deleting a user in Immich
|
||||
deletes their photos; an app store that destroys data as a side effect of an unrelated action is worse
|
||||
than one that leaves a stale account behind.
|
||||
|
||||
**Assumed working:** the vault's own multi-user adaptation is being done separately. Today `/api/vault`
|
||||
is owner-only by an explicit `ownerGate`, so a member is refused before Vaultwarden is reached — this
|
||||
design is written as though that has landed.
|
||||
|
||||
---
|
||||
|
||||
## What Phase 0 must not foreclose
|
||||
|
||||
Three things are coming, and each one constrains a decision that looks free today.
|
||||
|
||||
Reference in New Issue
Block a user