feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(web): add multi-owner support with promote/demote actions - #988

Merged
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support
Mar 10, 2026
Merged

feat(web): add multi-owner support with promote/demote actions#988
brendan-kellam merged 9 commits into
mainfrom
brendan-kellam/multi-owner-support

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Allow owners to promote members to owner and demote owners to member, enabling multiple owners per organization. This is gated behind the org-management entitlement as an enterprise feature.

  • Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
  • Update leaveOrg to allow non-last owners to leave
  • Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
  • Support self-demotion (with last-owner protection)
  • Deprecate transferOwnership action

Fixes#816

Summary by CodeRabbit

  • New Features

    • Multi-owner model: promote members to owners and demote owners to members; UI and telemetry added.
  • Documentation

    • Updated roles & permissions, access settings, and license docs to reflect multi-owner behavior and managing owners.
    • Audit log action types updated to include owner promotion/demotion events.
  • Improvements

    • Prevent actions that would leave an org without an owner; clarified subscription tooltip copy.

brendan-kellamand others added 2 commits March 9, 2026 17:09
Allow owners to promote members to owner and demote owners to member,
enabling multiple owners per organization. This is gated behind the
org-management entitlement as an enterprise feature.
- Add promoteToOwner and demoteToMember server actions in ee/features/userManagement
- Update leaveOrg to allow non-last owners to leave
- Replace "Transfer ownership" UI with "Promote to owner" / "Demote to member"
- Support self-demotion (with last-owner protection)
- Deprecate transferOwnership action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport
- Update roles-and-permissions to document multiple owners and
promote/demote workflows
- Update access-settings to use plural owner references
- Add new audit events: org.member_promoted_to_owner,
org.owner_demoted_to_member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds multi-owner support: replaces single ownership transfer with promote-to-owner and demote-to-member flows; introduces backend actions, UI changes, docs updates, new telemetry events, and new error codes to enforce multi-owner rules.

Changes

Cohort / File(s)Summary
Documentation
docs/docs/configuration/audit-logs.mdx, docs/docs/configuration/auth/access-settings.mdx, docs/docs/configuration/auth/roles-and-permissions.mdx, docs/docs/license-key.mdx
Replaced single-owner wording with multi-owner language, added "Managing owners" docs and images, updated audit action types and feature availability row.
Server: user-management (new/updated actions)
packages/web/src/ee/features/userManagement/actions.ts, packages/web/src/features/userManagement/actions.ts
Added promoteToOwner and demoteToMember with auth, entitlement, role checks, Prisma updates, and audit logging; added removeMemberFromOrg and leaveOrg with last-owner checks and optional billing seat decrement.
Server: removed legacy methods
packages/web/src/actions.ts
Removed legacy ownership-transfer, remove-member, and leave-org functions and removed decrementOrgSeatCount import from this file.
Frontend: members UI
packages/web/src/app/[domain]/settings/members/components/membersList.tsx, packages/web/src/app/[domain]/settings/members/page.tsx
Replaced transfer ownership UI with Promote/Demote flows; added hasOrgManagement prop gating; updated dialogs, tooltips, telemetry events, enablement rules, and copy to prevent demoting the last owner.
Telemetry & Errors
packages/web/src/lib/posthogEvents.ts, packages/web/src/lib/errorCodes.ts
Added PostHog events for promote/demote success/fail; removed OWNER_CANNOT_LEAVE_ORG; added LAST_OWNER_CANNOT_BE_DEMOTED and LAST_OWNER_CANNOT_BE_REMOVED.
Billing UI copy
packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
Minor tooltip copy change from singular to plural "owner(s)".
Changelog
CHANGELOG.md
Added Unreleased entry: multi-owner support with promote/demote actions.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client UI
participant Server as Server Actions
participant Auth as Auth & Entitlements
participant DB as Database
participant Audit as Audit Service
Client->>Server: promoteToOwner(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: fetch member, ensure not already OWNER
DB-->>Server: member record
Server->>DB: update role -> OWNER
DB-->>Server: update result
Server->>Audit: record promotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Client->>Server: demoteToMember(memberId)
Server->>Auth: withAuthV2 + withMinimumOrgRole
Auth-->>Server: authenticated & role verified
Server->>Auth: hasEntitlement('org-management')
Auth-->>Server: entitlement granted
Server->>DB: verify target is OWNER and owners.count > 1
DB-->>Server: validation result
Server->>DB: update role -> MEMBER
DB-->>Server: update result
Server->>Audit: record demotion audit entry
Audit-->>Server: logged
Server-->>Client: { success: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main feature addition: multi-owner support with promote/demote actions, which is the primary objective of this changeset.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan-kellam/multi-owner-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

This comment has been minimized.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/[domain]/settings/members/components/membersList.tsx (1)

247-257: Consider disabling "Demote to member" for the last owner in the UI.

The "Leave organization" option at line 272 is correctly disabled when ownerCount <= 1, but the "Demote to member" option doesn't have similar protection. While the server-side action will correctly reject demoting the last owner with LAST_OWNER_CANNOT_BE_DEMOTED, users will see the option, click it, go through the confirmation dialog, and only then receive an error.

For consistency with the "Leave" behavior, consider disabling this option in the UI:

💡 Proposed UX improvement
- {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (+ {hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role === OrgRole.OWNER && (ownerCount > 1 || member.id !== currentUserId) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
setMemberToDemote(member);
setIsDemoteDialogOpen(true);
}}
>
Demote to member
</DropdownMenuItem>
)}

Alternatively, you could use the disabled prop similar to line 272 if you want to keep the option visible but non-interactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx
around lines 247 - 257, The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/docs/configuration/audit-logs.mdx`:
- Around line 147-150: The doc contains duplicate audit action rows for
org.ownership_transfer_failed and org.ownership_transferred; remove the repeated
entries from the block that currently lists `org.ownership_transfer_failed` and
`org.ownership_transferred` so only the new entries
`org.member_promoted_to_owner` and `org.owner_demoted_to_member` remain,
ensuring the audit actions table no longer repeats
`org.ownership_transfer_failed` and `org.ownership_transferred`.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 30: Replace the em dash in the sentence "The last remaining owner of an
organization cannot be demoted — at least one owner must exist at all times."
with alternative punctuation per docs guidelines; for example change it to two
sentences ("...cannot be demoted. At least one owner..."), or use a comma or
parentheses ("..., at least one owner..." or "...(at least one owner...)") so
the .mdx file no longer contains an em dash.
In `@packages/web/src/ee/features/userManagement/actions.ts`:
- Around line 111-136: The owner-count check and subsequent role update
(prisma.userToOrg.count -> ownerCount and prisma.userToOrg.update) are
vulnerable to a TOCTOU race; wrap the count+check+update in a single atomic
transaction and acquire a row-level lock first (e.g., SELECT ... FOR UPDATE on
the UserToOrg or Org row via prisma.$queryRaw or lock the org's membership rows)
so you re-check ownerCount inside the same transaction before performing the
update; apply the same pattern to the leaveOrg flow where the same
count-then-update logic exists.
---
Nitpick comments:
In `@packages/web/src/app/`[domain]/settings/members/components/membersList.tsx:
- Around line 247-257: The "Demote to member" action currently renders whenever
hasOrgManagement && currentUserRole === OrgRole.OWNER && member.role ===
OrgRole.OWNER; update this to guard against demoting the last owner by including
ownerCount > 1 in that condition (or render the DropdownMenuItem with
disabled={ownerCount <= 1}) and keep the existing onClick behavior tied to
setMemberToDemote and setIsDemoteDialogOpen; this mirrors the "Leave
organization" behavior and prevents showing/allowing the demote flow for the
final owner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61f8da61-9e37-4f13-900a-d175adbe2995

📥 Commits

Reviewing files that changed from the base of the PR and between 5dea84f and 3c87196.

📒 Files selected for processing (10)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/access-settings.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/app/[domain]/settings/members/page.tsx
  • packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts

Comment threaddocs/docs/configuration/audit-logs.mdx Outdated
Comment threaddocs/docs/configuration/auth/roles-and-permissions.mdx Outdated
Comment threadpackages/web/src/ee/features/userManagement/actions.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/docs/configuration/auth/roles-and-permissions.mdx (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Replace the em dash in this sentence.

This line still uses , which violates the docs style rules.

📝 Suggested fix
-To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted — at least one owner must exist at all times.+To demote an owner, click the action menu next to their name and select **Demote to member**. Owners can also demote themselves to step down from the role. The last remaining owner of an organization cannot be demoted. At least one owner must exist at all times.

As per coding guidelines: "When writing or editing .mdx files in docs/, do NOT use em dashes (). Use periods to break sentences, commas, or parentheses instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/docs/configuration/auth/roles-and-permissions.mdx` at line 38, The
sentence "The last remaining owner of an organization cannot be demoted — at
least one owner must exist at all times." uses an em dash; replace the em dash
with a period (or comma/parentheses) to follow docs style: e.g., split into two
sentences "The last remaining owner of an organization cannot be demoted. At
least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/web/src/features/userManagement/actions.ts`:
- Line 5: The membership deletion currently calls decrementOrgSeatCount() inside
the DB transaction and "throw result" which leaks non-ServiceError exceptions;
move the external billing call (decrementOrgSeatCount) out of the transaction so
the DB delete and local seat-count update are committed first and then call
decrementOrgSeatCount in a post-commit/retriable job; when propagating failures
from the billing call wrap them into the ServiceErrorException (or a
ServiceError) instead of rethrowing the raw result so sew() can unwrap them
correctly (refer to decrementOrgSeatCount, sew, ServiceError and
ServiceErrorException in your changes).
- Around line 14-48: The mutation removeMemberFromOrg currently deletes a
userToOrg row without protecting the "last owner" invariant; fix it by
performing the owner-check inside the same prisma.$transaction that does the
delete: inside the transaction (use the tx instance) first fetch the
targetMember (or check targetMember.role) and if targetMember.role ===
OrgRole.OWNER then call tx.userToOrg.count({ where: { orgId: org.id, role:
OrgRole.OWNER } }) and if that count <= 1 abort (return or throw a ServiceError
indicating the org cannot be left without an owner) instead of deleting; only
proceed to tx.userToOrg.delete and then call decrementOrgSeatCount(tx) afterward
if not aborted. Apply the same in the related code path referenced (the 52-87
block / leaveOrg logic) to ensure the invariant is enforced inside the
transactional write.
---
Duplicate comments:
In `@docs/docs/configuration/auth/roles-and-permissions.mdx`:
- Line 38: The sentence "The last remaining owner of an organization cannot be
demoted — at least one owner must exist at all times." uses an em dash; replace
the em dash with a period (or comma/parentheses) to follow docs style: e.g.,
split into two sentences "The last remaining owner of an organization cannot be
demoted. At least one owner must exist at all times." Update the text in the
roles-and-permissions paragraph accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7cd23b-479b-4ae5-84f5-f05141a327c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c87196 and e71de80.

⛔ Files ignored due to path filters (4)
  • docs/images/demote_to_member.png is excluded by !**/*.png
  • docs/images/managing_owners.png is excluded by !**/*.png
  • docs/images/owner_leave_org.png is excluded by !**/*.png
  • docs/images/promote_to_owner.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/docs/configuration/audit-logs.mdx
  • docs/docs/configuration/auth/roles-and-permissions.mdx
  • packages/web/src/actions.ts
  • packages/web/src/app/[domain]/settings/members/components/membersList.tsx
  • packages/web/src/features/userManagement/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/configuration/audit-logs.mdx

Comment threadpackages/web/src/features/userManagement/actions.ts
Comment threadpackages/web/src/features/userManagement/actions.ts
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Support multiple owners/admins and introduce RBAC or group-based management

1 participant

@brendan-kellam