Repository navigation
Team administration & offboarding — roles, pending-invite status, seats, leave/remove #72
Copy link
Copy link
Closed
Labels
designUI/UX design & interactionUI/UX design & interactionenhancementNew feature or requestNew feature or requestteam-uxTeam & collaboration UX flows (sessions, vaults, invites, presence)Team & collaboration UX flows (sessions, vaults, invites, presence)
Description
Part of the team-ux initiative (siblings: #65–#69).
Relates: #67 (pending-invite management overlaps the inbox), #68 (roles & unified panels), #71 (hub is the likely home for admin entry).
Problem
The inviter/admin surface (MembersPage.tsx) is dense and under-designed, and the offboarding side is essentially absent.
Administration (inviter side):
- Roles — assigning/changing roles (owner/manager/editor/member/connect-only) is present but not clearly explained; no per-role "what can this person do" surface.
- Pending invites — status is thin: revoke is a hover-
xonPendingInviteCard; there's no clear resend, no "invited but not yet accepted" clarity, no expiry. - Seats / paywall — the seats usage bar → HTTP 402 →
BuySeatsModalpath works but is transactional and easy to hit as a wall; the "why is my invite blocked" moment isn't well handled. - Two-step member removal + undo exists but isn't a coherent, reassuring flow.
Offboarding (missing):
- A member leaving a team — no clear action or consequence explanation.
- An owner removing a member — what visibly happens to that person's access and keys (revocation, re-wrap) is neither surfaced to the admin nor to the removed user.
Proposed reflow
- Clear role model: show what each role grants at the point of assignment; make changes obviously reversible.
- First-class pending-invite management: status (invited / accepted / expired), resend and revoke as explicit actions, consistent with the receive-side inbox (Unified team notification inbox — invites, shared sessions & control requests through the bell + toasts #67).
- Seat UX that isn't a wall: surface seat usage before the invite is blocked; make the buy-more path calm and explain what a seat is.
- Explicit offboarding: "Leave team" and "Remove member" as honest flows that state the access/key consequences, with confirmation.
Decisions (brainstorm 2026-07-24)
- Filed as one issue (per brainstorm) since it's all the same admin surface; may split into "member/role/seat administration" and "offboarding" if it grows.
- Role set and panel unification are coordinated with Explicit "Share" verb for vaults — a real share sheet, and unify the two invite panels #68 (don't diverge role vocabularies).
Still open
- Whether admin lives in the Team hub (Team hub + first-time mental model (end silent team creation; teammates directory) #71), stays per-vault in
MembersPage, or both. - Key-revocation UX + what the removed user sees.
- Seat model presentation (per-user? explain trial seat-clamp behavior).
- Split vs. keep-as-one decision.
Metadata
Metadata
Assignees
Labels
designUI/UX design & interactionUI/UX design & interactionenhancementNew feature or requestNew feature or requestteam-uxTeam & collaboration UX flows (sessions, vaults, invites, presence)Team & collaboration UX flows (sessions, vaults, invites, presence)
Activity
kipavy commentedon Sep 7, 2026
Audit 2026-09-07 — much of this issue is stale, and two real bugs sit underneath it
Re-read the code against this issue's claims. A lot shipped since the 2026-07-24 brainstorm, so the issue as written now points at the wrong things. Full design:
docs/superpowers/specs/2026-09-07-team-admin-offboarding-design.md.Already built — these parts of the issue are no longer true
RoleChiphas a hover tooltip listing granted permission labels (src/components/members/MembersPage.tsx:43); all 17 permissions carry a label + description, translated (src/components/settings/sections/RolesSection.tsx:33);TeamRolesPanelis reachable from the Members toolbar and Settings › Vaults.historyStore(MembersPage.tsx:392,:445).SeatsMeterrenders inInvitePanelabove the search field, andisAtSeatLimitshort-circuits before the request (MembersPage.tsx:720,:803) — so the 402 is not normally reached. (Except under a trial; see below.)expires_atalready on the wire (server/src/routes/teams.rs:1376). The client just never renders it.expires_at = now() + INTERVAL '7 days'(teams.rs:466,:1277,:1329). A resend button needs no new endpoint.remove_memberdoesif auth.0 != user_id { require MANAGE_MEMBERS }(teams.rs:504), so self-removal passes; the owner is blocked separately. Leaving isDELETE /v1/teams/{id}/members/{me}— this is client UI only, not a server feature.team_vault_keysrow, revokes session grants, pushesmembership_changed(teams.rs:545-563); the removed client evicts the in-memory key, unlinks the vault and clears every team store slice (src/services/sync.ts:882-926).Also stale: "stays per-vault in
MembersPage". It is already a top-level nav page (src/components/layout/MainPanel.tsx:97), so the open question about the hub does not block anything here.Two bugs found while auditing
1. Team secrets survive removal in the departed member's OS keychain.
fetchTeamDatawrites every team secret — connection passwords, private keys, passphrases — into the local keychain (src/services/teamVaultSync.ts:388). The only function that removes them from disk,_clearTeamStores(teamVaultSync.ts:499-524), is called exclusively fromfetchTeamData's own error/403 branches. The removal path calls the plain in-memoryclearTeamConnections/clearTeamKeys/… store methods instead (sync.ts:918-925,src/stores/connectionStore.ts:75).The ordering makes it unrecoverable: the wipe needs the object IDs, and the code says so — "Wipe secrets from disk before clearing in-memory state so we still have the IDs."
onTeamRemovedclears memory first, so a later 403 finds empty arrays and deletes nothing. A removed member keeps the team's plaintext passwords and private keys indefinitely.2. The seat pre-check is wrong under a trial.
/subscription-inforeturns the raw purchasedseat_count(server/src/routes/billing.rs:519-523), not the trial-clamped cap the invite path enforces viaowner_seat_cap(teams.rs:49,seats.min(10)). A trial owner with 25 purchased seats sees "4 of 25 used, 21 available", the pre-check passes, and the server rejects at 10 with a 402 — the wall this issue complains about, in the one case nothing explains it.Key rotation is not the fix
Removal deletes the wrapped key but never rotates the team DEK. Rotating it would not help, because the departing member held the plaintext:
Rotation protects only blobs written after rotation, which a removed member cannot fetch anyway. Its real value is bounding blast radius if the encrypted blob leaks from a backup or stale replica — defense in depth, and filed separately with that rationale rather than as offboarding.
What this issue becomes
onTeamRemoved, before clearing memory.InboxKind: "membershipEnded"plus a toast, with a "Review affected credentials" action. Not raised when someone leaves voluntarily.teamOffboardingservice. Bulk kick currently has no confirmation at all (MembersPage.tsx:1261) while single removal is two-step; the unification fixes that as a side effect.teams.rs:1404), return astatus, and give the card a sent date, an expiry, and explicit Resend/Revoke buttons.effective_seatscomputed by the existingowner_seat_cap(fixes bug 2), explain the trial clamp, and say that seats count distinct people across every team you own.RoleChiptooltip and attach it to the toggle chips too.Plus decomposing
MembersPage.tsx(1629 lines, eight components), which the above touches throughout, behind a before/after pixel diff.kipavy commentedon Sep 8, 2026
#225 implements gaps 1, 2, 4, 5, 6 and 8 from the audit above, plus the
MembersPagedecomposition. Paired with VoltiusApp/server#13 for the invite status.Still open on this issue after #225 merges:
One correction to the audit: it said removal was written three times. It is four —
VaultsSection.tsxhad its own copy with a two-step confirm and no undo at all.devhas since deleted that panel entirely (#221), so #225 takes that deletion rather than fixing it.feat(teams): offboarding, invite lifecycle, and the keychain leak on …
kipavy commentedon Sep 10, 2026
All remaining scope shipped in #232 (fix #219) — trial-clamp copy and account-wide-scope explanation are already in
SeatsMeter.tsxondev(members.invite.seatsTrialClamp/seatsScope, translated en/fr/ru/zh), just not yet in a tagged release (latest is v0.32.1, which predates it).Closing as fixed, ships with the next release.