Skip to content

Unified cross-org membership management for parent organizations (Phase 1: Navigation Hub) #5495

Description

@jrf0110

Summary

Parent organizations currently have no unified way to view or manage member roles across their child (sub-)organizations — admins must open each child org one at a time. This issue tracks Phase 1 of a phased plan (full design process/rationale in the linked discussion below): a unified navigation hub, built almost entirely from existing, already-trusted components, plus two independent bug fixes to existing membership code.

Phase 2 (a dedicated atomic bulk-write API) is explicitly not part of this issue — it's evidence-gated behind telemetry from Phase 1 usage and will be scoped separately if/when the data justifies it. See "Non-goals" below.

Background / design process

This PRD was produced via an adversarial multi-agent design process: three independent designs were explored (a first-class "membership matrix" bulk API, a declarative group/role-template propagation model, and a federated navigation-hub model), compared by an impartial analysis pass, stress-tested by an adversarial critique of the top-ranked design, and reconciled into the phased plan below. The navigation-hub approach won Phase 1 because it reuses battle-tested UI with zero new privileged write surface; the matrix-API approach's core ideas are preserved as an evidence-gated Phase 2 rather than discarded.

Scope (Phase 1)

1. Unified people directory

  • Extend organizations.subOrganizations.people (apps/web/src/routers/organizations/organization-sub-organizations-router.ts) with one additive field per membership/invitation/parent-membership entry: canManageMemberships: boolean, computed via the existing batched getOrganizationsAccessRoles helper (apps/web/src/routers/organizations/utils.ts). This is a UX hint only — every mutation still re-checks real authorization server-side.
  • Update PeopleSection (apps/web/src/app/(app)/organizations/[id]/sub-organizations/SubOrganizationSections.tsx) to add a row-selection checkbox column and turn membership/invitation badges into clickable affordances when canManageMemberships is true. No inline-editable cells — every action is a navigation trigger (opens a drawer or wizard), never an onChange-fired mutation inside a table cell.

2. Context drawer

  • New component MemberManagementDrawer that mounts the existing, unmodified OrganizationAdminMembers component (apps/web/src/components/organizations/OrganizationMembersCard.tsx) and OrganizationAdminContextProvider, scoped to a specific child org id, opened from a directory row. Drawer state lives in the URL query string. Closing the drawer invalidates the parent's subOrganizations.people query.

3. Guided bulk-action wizards

  • "Add people to a child org": select people → pick one target child org → preview (role picker, auto-skip existing members/invitees) → sequential execute via the existing organizations.members.invite mutation → per-row results summary with retry-failed.
  • "Remove people from a child org": pick target org → select people with an existing membership/invitation there → preview with an authority pre-check and irreversibility warning → sequential execute via existing organizations.members.remove / deleteInvite → shared results-summary component.
  • No new tRPC procedures for either wizard.

4. Two independent bug fixes (ship regardless of the above)

  • addUserToOrganization (apps/web/src/lib/organizations/organizations.ts) is missing the lockOrganizationMembershipMutation advisory lock that removeUserFromOrganization and addSsoUserToOrganization already take — add it, closing a race condition on concurrent membership writes for the same (organization_id, kilo_user_id) pair.
  • setChildMemberships's removal loop (apps/web/src/routers/organizations/organization-members-router.ts) has no role filter — it will silently strip a child-org owner/billing_manager membership if omitted from the request, today prevented only client-side. Add a server-side role !== 'member' guard before removing. Note: organization-members-router.test.ts (around lines 624–657) currently has a test asserting the vulnerable behavior as correct ('removes unselected child organization memberships regardless of role') — this must be rewritten to assert the fixed contract.

5. Telemetry (gates Phase 2)

  • Emit events for wizard runs, repeat-run-within-15-minutes, large selections (≥15 people), drawer opens, and partial-failure rate. Observe for ~6 weeks across enough parent orgs to be meaningful before deciding whether Phase 2 (dedicated bulk-write backend) is warranted.

Non-goals for this issue

  • No new membershipMatrix/reconcile tRPC procedures or spreadsheet-grid UI (deferred to an evidence-gated Phase 2).
  • No cross-org atomic "move" operation (remove from A + add to B is two independent wizard runs in Phase 1 — a known, accepted gap).
  • No declarative/recurring role-template mechanism (out of scope for both phases as currently planned).

Testing

  • Unit: wizard row-eligibility filtering, sequential-execution/partial-failure reducer, addUserToOrganization lock-acquisition, setChildMemberships role-filter predicate.
  • Integration: rewrite the existing setChildMemberships removal test to assert owner/billing_manager rows survive when omitted; add canManageMemberships coverage to organization-sub-organizations-router.test.ts (inherited parent access vs. plain member).
  • Component: badge-vs-button rendering by canManageMemberships; drawer context-nesting safety (renders child-scoped role, not an outer parent-scoped one).
  • E2E: drawer role-edit round-trip, add-wizard with an auto-skipped existing member, remove-wizard with a forced per-row failure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions