Skip to content

feat(org): admin seat override and plan tier control#2943

Merged
steebchen merged 1 commit into
mainfrom
org-seats-override
Jul 8, 2026
Merged

feat(org): admin seat override and plan tier control#2943
steebchen merged 1 commit into
mainfrom
org-seats-override

Conversation

@steebchen

@steebchen steebchen commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds a nullable seats column to the organization and a Manage org dialog in the admin dashboard where we can override an org's seat limit and change its plan tier (typically free ⇄ enterprise).

The seat limit continues to fall back to the per-plan default (free/pro = 5, enterprise = 100), but an explicit seats override takes precedence everywhere it is displayed and enforced.

Changes

  • Schema: new nullable organization.seats column (null = use plan default) + migration.
  • Enforcement (apps/api/src/routes/team.ts): the add-member seat cap now uses a shared resolveSeatLimit(plan, seats) helper — seats ?? (plan === "enterprise" ? 100 : 5).
  • Display: the GET /{orgId}/members response now returns the effective seatLimit, and the Team page shows X/<seatLimit> seats used instead of a hardcoded 5.
  • Admin dashboard: new PATCH /admin/organizations/{orgId}/manage route (plan + seats), a manageOrganization server action, and a Manage org dialog on the org detail page. A Seats: N badge shows when an override is set. The change is written to the audit log as organization.manage.

Notes

  • SCIM syncs intentionally ignore the seat cap for now — automated provisioning may temporarily show e.g. 60/50 used, which is acceptable.
  • Enterprise/seat changes are admin-only (guarded by the existing admin-email middleware).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for organization seat overrides alongside plan settings.
    • Admins can now update an organization’s plan and seat limit from the organization page.
    • Team member screens now show the effective seat limit and seats used.
  • Bug Fixes

    • Member-cap checks now respect organization-specific seat overrides instead of relying on fixed plan defaults.
  • Chores

    • Organization details and admin responses now include the current seat override value.

Copilot AI review requested due to automatic review settings July 8, 2026 00:15
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a nullable seats column to the organization table for manual seat-limit overrides. Introduces a resolveSeatLimit helper used by team endpoints, exposes seats/seatLimit in API schemas and responses, adds a new admin PATCH endpoint to manage plan/seats with audit logging, and adds an admin UI dialog plus dynamic UI seat-limit display.

Changes

Manual seat limit override

Layer / File(s) Summary
Database schema and migration
packages/db/src/schema.ts, packages/db/migrations/1783469250_flowery_puppet_master.sql, packages/db/migrations/meta/_journal.json
Adds nullable seats integer column to organization table, migration file, journal entry, and new "organization.manage" audit action.
Admin API endpoints
apps/api/src/routes/admin.ts, apps/api/src/routes/organization.ts
Extends organization schemas with nullable seats, includes it in metrics/transactions responses, and adds PATCH /organizations/{orgId}/manage to validate, update plan/seats, audit-log, and return the result.
Team seat-limit resolution
apps/api/src/routes/team.ts, apps/gateway/src/lib/rate-limit.spec.ts
Adds exported resolveSeatLimit(plan, seats) used to compute seatLimit in the members list response and to enforce seat caps when adding members; updates mocked org test data with seats: null.
Team UI dynamic display
apps/ui/src/app/dashboard/[orgId]/org/team/team-client.tsx
Replaces hardcoded seat limit of 5 with dynamic data?.seatLimit ?? 5 in dialog copy and card description.
Admin UI manage dialog
ee/admin/src/app/organizations/[orgId]/manage-org-dialog.tsx, ee/admin/src/app/organizations/[orgId]/page.tsx, ee/admin/src/lib/admin-organizations.ts
Adds ManageOrgDialog component for editing plan/seat overrides, manageOrganization server action calling the manage endpoint, and wires both into the org page with a conditional seats badge.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant ManageOrgDialog
  participant manageOrganization
  participant AdminAPI
  participant Database

  Admin->>ManageOrgDialog: edit plan and seats
  ManageOrgDialog->>manageOrganization: onSave({plan, seats})
  manageOrganization->>AdminAPI: PATCH /organizations/{orgId}/manage
  AdminAPI->>Database: update plan and seats, write audit event
  Database-->>AdminAPI: updated organization
  AdminAPI-->>manageOrganization: {plan, seats}
  manageOrganization-->>ManageOrgDialog: {success}
  ManageOrgDialog->>Admin: router.refresh()
Loading
sequenceDiagram
  participant Client
  participant TeamRoute
  participant resolveSeatLimit
  participant Database

  Client->>TeamRoute: GET /members/{organizationId}
  TeamRoute->>Database: fetch organization plan and seats
  Database-->>TeamRoute: {plan, seats}
  TeamRoute->>resolveSeatLimit: resolveSeatLimit(plan, seats)
  resolveSeatLimit-->>TeamRoute: seatLimit
  TeamRoute-->>Client: {members, seatLimit}
Loading

Possibly related PRs

  • theopenco/llmgateway#1557: Both PRs modify the mocked organization data in apps/gateway/src/lib/rate-limit.spec.ts used by the rate-limit tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: admin control over organization seat overrides and plan tier updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch org-seats-override

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.

@steebchen steebchen force-pushed the org-seats-override branch from ebadfe1 to f442f5d Compare July 8, 2026 15:10

@coderabbitai coderabbitai Bot 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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/app/dashboard/[orgId]/org/team/team-client.tsx (1)

869-871: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Add Member button still uses hardcoded seat limit of 5.

The dialog copy and card description were updated to use data?.seatLimit ?? 5, but the button's disabled condition on line 869 still checks against a hardcoded 5. If an admin overrides the seat limit to a higher value (e.g., 100), the button will disable at 5 members, preventing further additions despite available seats. This directly contradicts the PR's goal of dynamic seat enforcement.

🐛 Proposed fix
-								<Button disabled={(data?.members.length ?? 0) >= 5}>
+								<Button disabled={(data?.members.length ?? 0) >= (data?.seatLimit ?? 5)}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/app/dashboard/`[orgId]/org/team/team-client.tsx around lines 869
- 871, The Add Member button in team-client.tsx still uses a hardcoded 5-seat
cap in its disabled logic, so it can block additions even when the org’s seat
limit is higher. Update the Button’s disabled condition to use the same dynamic
seat limit source used elsewhere in this view, namely data?.seatLimit ?? 5, so
the add-member behavior stays consistent with the dialog copy and card
description.
🧹 Nitpick comments (1)
ee/admin/src/app/organizations/[orgId]/manage-org-dialog.tsx (1)

39-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

PLAN_DEFAULT_SEATS duplicates backend plan defaults.

If the backend plan defaults change, this UI constant will be out of sync, showing incorrect placeholder text. Consider deriving these defaults from the API or a shared constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/admin/src/app/organizations/`[orgId]/manage-org-dialog.tsx around lines 39
- 43, The PLAN_DEFAULT_SEATS constant in manage-org-dialog should not hardcode
plan defaults because it can drift from backend values. Update the org
management dialog to derive default seats from the API response or a shared
plan-default source instead of the local PLAN_DEFAULT_SEATS map, and make sure
the place that renders the seat placeholder uses that shared value in the
manage-org dialog flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ee/admin/src/app/organizations/`[orgId]/manage-org-dialog.tsx:
- Around line 55-60: The form state in ManageOrgDialog is persisting across
close/reopen because the component stays mounted and only DialogContent is
conditional. Update the dialog open handling in ManageOrgDialog (especially
onOpenChange) so planValue and seatsValue are reset back to the current plan and
seats props whenever the dialog opens, ensuring unsaved edits are cleared before
the user sees the form again.

---

Outside diff comments:
In `@apps/ui/src/app/dashboard/`[orgId]/org/team/team-client.tsx:
- Around line 869-871: The Add Member button in team-client.tsx still uses a
hardcoded 5-seat cap in its disabled logic, so it can block additions even when
the org’s seat limit is higher. Update the Button’s disabled condition to use
the same dynamic seat limit source used elsewhere in this view, namely
data?.seatLimit ?? 5, so the add-member behavior stays consistent with the
dialog copy and card description.

---

Nitpick comments:
In `@ee/admin/src/app/organizations/`[orgId]/manage-org-dialog.tsx:
- Around line 39-43: The PLAN_DEFAULT_SEATS constant in manage-org-dialog should
not hardcode plan defaults because it can drift from backend values. Update the
org management dialog to derive default seats from the API response or a shared
plan-default source instead of the local PLAN_DEFAULT_SEATS map, and make sure
the place that renders the seat placeholder uses that shared value in the
manage-org dialog flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0306bd7c-ad63-4d4d-aca4-0493b3848f3d

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6b1c6 and f442f5d.

📒 Files selected for processing (12)
  • apps/api/src/routes/admin.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/team.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/ui/src/app/dashboard/[orgId]/org/team/team-client.tsx
  • ee/admin/src/app/organizations/[orgId]/manage-org-dialog.tsx
  • ee/admin/src/app/organizations/[orgId]/page.tsx
  • ee/admin/src/lib/admin-organizations.ts
  • packages/db/migrations/1783469250_flowery_puppet_master.sql
  • packages/db/migrations/meta/1783469250_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts

Comment on lines +55 to +60
const [planValue, setPlanValue] = useState<Plan>(
plan === "pro" || plan === "enterprise" ? plan : "free",
);
const [seatsValue, setSeatsValue] = useState(
seats === null ? "" : String(seats),
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Form state persists across dialog open/close cycles.

Since ManageOrgDialog stays mounted and only DialogContent is conditionally rendered, useState values survive close/reopen. If an admin changes the plan or seats, closes without saving, and reopens, they'll see their unsaved edits rather than the current org values. Consider resetting state in onOpenChange when the dialog opens.

💚 Proposed fix
 	<Dialog open={open} onOpenChange={(v) => {
 		setOpen(v);
+		if (v) {
+			setPlanValue(plan === "pro" || plan === "enterprise" ? plan : "free");
+			setSeatsValue(seats === null ? "" : String(seats));
+			setError(null);
+		}
 	}}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/admin/src/app/organizations/`[orgId]/manage-org-dialog.tsx around lines 55
- 60, The form state in ManageOrgDialog is persisting across close/reopen
because the component stays mounted and only DialogContent is conditional.
Update the dialog open handling in ManageOrgDialog (especially onOpenChange) so
planValue and seatsValue are reset back to the current plan and seats props
whenever the dialog opens, ensuring unsaved edits are cleared before the user
sees the form again.

@steebchen steebchen merged commit c2e405f into main Jul 8, 2026
17 checks passed
@steebchen steebchen deleted the org-seats-override branch July 8, 2026 15:18
Sign up for free to 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.

2 participants