Add team-shared extra usage with admin pool and per-member caps#443
Conversation
Lets a team admin fund a shared overflow pool that any member can draw from once the team subscription bucket is exhausted, with admin-set per-member monthly caps, a member-level disable switch, a team-wide cap, and a spend dashboard. Auto-reload bills the org's existing Stripe customer instead of individual users so the team's billing stays unified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a team-scoped extra-usage system: Convex schema and server logic for a shared team credit pool with per-member controls, Stripe Checkout purchase + webhook handlers, admin API/UI to manage pool/settings/members, and client/server wiring so rate-limited usage can debit/refund an organization pool with auto-reload support. ChangesTeam Extra Usage Pool & Admin System
Sequence Diagram sequenceDiagram
participant AdminClient
participant ConvexAction as convex.teamExtraUsageActions.createTeamPurchaseSession
participant Stripe
participant ConfirmRoute as /api/team/extra-usage/confirm
participant Webhook as /api/team/extra-usage/webhook
participant ConvexMut as convex.teamExtraUsage.addTeamCredits
AdminClient->>ConvexAction: createTeamPurchaseSession(serviceKey, orgId, amount, baseUrl)
ConvexAction->>Stripe: create Checkout session (metadata: orgId, amount, type)
AdminClient->>Stripe: User completes Checkout
Stripe->>ConfirmRoute: redirect to /confirm?session_id=cs_...
ConfirmRoute->>ConvexMut: addTeamCredits(serviceKey, orgId, amount, idempotencyKey)
Stripe->>Webhook: emits checkout.session.completed
Webhook->>ConvexMut: addTeamCredits(CONVEX_SERVICE_ROLE_KEY, idempotencyKeys)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 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 `@app/api/team/extra-usage/members/`[userId]/route.ts:
- Around line 46-54: The PATCH handler currently passes unvalidated body fields
into convex.mutation(api.teamExtraUsage.updateTeamMemberUsage), causing 500s for
bad input; before calling that mutation validate req.json() output: confirm
body.monthlyLimitDollars is either undefined or a finite non-negative number (or
numeric string converted to number) and body.disabled is either undefined or a
boolean, otherwise return a 400 response; perform these checks in the same
route.ts function that computes guard.organizationId and targetUserId and only
call api.teamExtraUsage.updateTeamMemberUsage when the types/values are valid.
In `@app/api/team/extra-usage/purchase/route.ts`:
- Around line 22-27: The current amountDollars check only tests typeof number
but allows NaN/Infinity/zero/negative; update the validation in the route
handler to require Number.isFinite(amountDollars) && amountDollars > 0 and
return a 400 NextResponse.json({ error: "amountDollars must be a finite number
greater than 0" }, { status: 400 }) when it fails; locate the existing check
around the amountDollars variable and replace the condition and error message
accordingly before proceeding to checkout creation.
In `@app/api/team/extra-usage/route.ts`:
- Around line 83-93: The POST handler is sending unvalidated values to
convex.mutation (api.teamExtraUsage.updateTeamExtraUsageSettings); add runtime
validation of the parsed body from req.json() in the route.ts POST handler:
verify body.enabled and body.autoReloadEnabled are booleans, and
body.autoReloadThresholdDollars, body.autoReloadAmountDollars, and
body.monthlyCapDollars are numbers (and non-negative) before calling the
mutation with guard.organizationId; if validation fails, return a 400 response
with a clear error message instead of calling convex.mutation so invalid types
do not produce 500s.
In `@app/api/team/extra-usage/webhook/route.ts`:
- Around line 68-76: The webhook currently returns a 400 for invalid metadata
(checking organizationId and amountDollars) which causes Stripe to retry; update
the handler in route.ts so that when organizationId is missing or amountDollars
is NaN you still log the error (use the existing console.error message
referencing session.id) but return a 2xx acknowledgement via NextResponse.json
(e.g., success/ok) instead of the 400 error to stop Stripe redeliveries; keep
the rest of the flow unchanged.
- Around line 56-61: The webhook grants credits on checkout.session.completed
regardless of settlement; modify the event handling in the route's switch over
event.type to (1) check session.payment_status === "paid" before issuing credits
in the "checkout.session.completed" case and return early if not paid, and (2)
add a new case for "checkout.session.async_payment_succeeded" that performs the
same credit issuance flow for sessions with metadata.type ===
"team_extra_usage_purchase". Update the logic that reads session (currently cast
as Stripe.Checkout.Session) so both cases reuse the same credit-granting
function (e.g., grantTeamExtraUsageCredits or similar) to avoid duplication and
ensure credits are only issued when payment_status is "paid" or on
async_payment_succeeded.
In `@app/components/TeamExtraUsageSection.tsx`:
- Around line 79-100: updatePool and similar functions (updateMember) swallow
errors by catching them and not propagating the failure, so callers still treat
the operation as successful; change these functions to propagate failures
(either rethrow the caught error or return a rejected Promise/false) after
performing local cleanup/logging so callers can show error UI instead of success
toasts. Locate updatePool and updateMember in TeamExtraUsageSection.tsx and
ensure that in the catch block you do not silently absorb the error—after
logging/toasting, rethrow the error (or return Promise.reject(err)) so callers
that await updatePool/updateMember can detect failure and avoid showing success
dialogs/toasts.
In `@convex/teamExtraUsage.ts`:
- Around line 564-567: The query that builds members via
ctx.db.query("team_member_usage").withIndex("by_org", (q) =>
q.eq("organization_id", args.organizationId")).collect() returns an unbounded
collection; replace .collect() with a bounded call (e.g., .take(N)) or implement
pagination so the result is limited (or loop with cursors). Update the code
where members is assigned to use .take(...) or a paginated fetch and ensure
callers handle partial results accordingly.
In `@lib/api/chat-stream-helpers.ts`:
- Around line 875-895: The current team-path in chat-stream-helpers.ts treats a
null getTeamExtraUsageState(organizationId, userId) as an optimistic enabled
config, which can bypass admin/member hard cutoffs; change the fallback to a
conservative deny by returning undefined (or an explicit { enabled: false })
instead of an enabled object, keep the console.warn for observability, and
ensure the logic in the subscription === "team" branch (the
getTeamExtraUsageState handling and subsequent checks on
state.enabled/state.memberDisabled/state.balanceDollars/state.autoReloadEnabled)
treats null state as disallowing extra usage so overflow cannot occur when team
state is unavailable.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0f037abf-1975-4919-bc92-17a9340e5f17
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (17)
app/api/team/extra-usage/auth.tsapp/api/team/extra-usage/confirm/route.tsapp/api/team/extra-usage/members/[userId]/route.tsapp/api/team/extra-usage/purchase/route.tsapp/api/team/extra-usage/route.tsapp/api/team/extra-usage/webhook/route.tsapp/components/TeamExtraUsageSection.tsxapp/components/TeamTab.tsxconvex/schema.tsconvex/teamExtraUsage.tsconvex/teamExtraUsageActions.tslib/api/chat-handler.tslib/api/chat-stream-helpers.tslib/extra-usage.tslib/rate-limit/__tests__/refund.test.tslib/rate-limit/refund.tslib/rate-limit/token-bucket.ts
Backs the new team-pool state machine with unit tests covering deduct/refund
branches, addTeamCredits idempotency, and buildExtraUsageConfig's team
routing. Promotes the per-route requireAdminOrg helper into a shared
team-auth module and adopts it in /api/team/{members,invite,seats}, cutting
the duplicated WorkOS membership + admin-role lookups. Also moves the
TeamExtraUsageSection mount from inside the Team tab to its own "Extra
Usage" tab for team admins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Disambiguates from the new "Extra Usage" tab that team admins also see — "Members" describes the contents (member list, invites, seat management) more directly than the generic "Team". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
app/components/SettingsDialog.tsx (1)
49-60: 💤 Low valueMissing fetch cleanup could cause state updates on unmounted component.
The
useEffectfetch doesn't use anAbortControllerfor cleanup. If the dialog unmounts or subscription changes mid-flight, React will warn about setting state on an unmounted component.♻️ Suggested fix with AbortController
useEffect(() => { // Only consulted when subscription === "team"; tabs() condition gates // any other read, so a stale value after switching tier is harmless and // will be overwritten the next time the user becomes a team member. if (subscription !== "team") return; + const controller = new AbortController(); - fetch("/api/team/members") + fetch("/api/team/members", { signal: controller.signal }) .then((res) => res.json()) .then((data) => setIsTeamAdmin(data.isAdmin ?? false)) .catch(() => setIsTeamAdmin(false)); + return () => controller.abort(); }, [subscription]);🤖 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 `@app/components/SettingsDialog.tsx` around lines 49 - 60, The effect that fetches "/api/team/members" (inside the useEffect that reads/sets isTeamAdmin via setIsTeamAdmin) needs an AbortController: create a controller, pass controller.signal into fetch, and in the fetch's then/catch paths guard against setting state if the request was aborted (or check signal.aborted) so you don't call setIsTeamAdmin after unmount; return a cleanup from the effect that calls controller.abort() to cancel in-flight requests when subscription changes or the component unmounts.
🤖 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 `@app/api/team/team-auth.ts`:
- Around line 35-50: The code currently picks the first active membership from
workos.userManagement.listOrganizationMemberships (memberships.data[0]), which
is ambiguous for users in multiple orgs; update the handler that calls
listOrganizationMemberships (team-auth) to accept an organizationId parameter
from the request and use it to find the matching membership (e.g., find
membership where membership.organization.id === organizationId) and return a
400/404 if missing, or alternatively enforce/validate single-membership by
returning an explicit error if memberships.data.length > 1 and no organizationId
was provided; ensure responses and error messages reference the membership
selection requirement so callers can disambiguate.
In `@convex/__tests__/teamExtraUsage.test.ts`:
- Around line 379-381: The currentMonth computation calls new Date() multiple
times which can yield inconsistent results across UTC month boundaries; create a
single Date instance (e.g., const now = new Date()) and derive currentMonth from
that single variable instead of calling new Date() repeatedly so currentMonth is
stable (update the code that defines currentMonth to use now.getUTCFullYear()
and now.getUTCMonth()).
- Around line 42-43: The test currently sets SERVICE_KEY and mutates
process.env.CONVEX_SERVICE_ROLE_KEY at module scope which can leak into other
tests; capture the original value (e.g. const _ORIGINAL_CONVEX_SERVICE_ROLE_KEY
= process.env.CONVEX_SERVICE_ROLE_KEY), move the assignment of
process.env.CONVEX_SERVICE_ROLE_KEY = SERVICE_KEY into a beforeEach or beforeAll
block, and restore the original value in an afterEach or afterAll block (e.g.
process.env.CONVEX_SERVICE_ROLE_KEY = _ORIGINAL_CONVEX_SERVICE_ROLE_KEY) so
SERVICE_KEY and process.env.CONVEX_SERVICE_ROLE_KEY are cleaned up after the
test run.
---
Nitpick comments:
In `@app/components/SettingsDialog.tsx`:
- Around line 49-60: The effect that fetches "/api/team/members" (inside the
useEffect that reads/sets isTeamAdmin via setIsTeamAdmin) needs an
AbortController: create a controller, pass controller.signal into fetch, and in
the fetch's then/catch paths guard against setting state if the request was
aborted (or check signal.aborted) so you don't call setIsTeamAdmin after
unmount; return a cleanup from the effect that calls controller.abort() to
cancel in-flight requests when subscription changes or the component unmounts.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: fad4a6d9-d191-4b8e-a298-4840814ccc28
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (10)
app/api/team/extra-usage/members/[userId]/route.tsapp/api/team/extra-usage/purchase/route.tsapp/api/team/extra-usage/route.tsapp/api/team/invite/route.tsapp/api/team/members/route.tsapp/api/team/seats/route.tsapp/api/team/team-auth.tsapp/components/SettingsDialog.tsxconvex/__tests__/teamExtraUsage.test.tslib/api/__tests__/build-extra-usage-config.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- app/api/team/extra-usage/members/[userId]/route.ts
- app/api/team/extra-usage/purchase/route.ts
- app/api/team/extra-usage/route.ts
- Reject NaN/Infinity/non-positive amountDollars in /purchase - Ack malformed-metadata webhook events with 200 to stop Stripe retries - Gate success toasts on updatePool/updateMember actually succeeding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
app/api/team/extra-usage/webhook/route.ts (1)
56-58:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate crediting on settled payment and handle async settlement event.
Credits are issued in
checkout.session.completedonly. For delayed payment methods, settlement can complete later; this should also handlecheckout.session.async_payment_succeeded(or explicitly gate by payment state before crediting).💡 Suggested fix
switch (event.type) { - case "checkout.session.completed": { + case "checkout.session.completed": + case "checkout.session.async_payment_succeeded": { const session = event.data.object as Stripe.Checkout.Session; if (session.metadata?.type !== "team_extra_usage_purchase") { return NextResponse.json({ received: true }); } + if (event.type === "checkout.session.completed" && session.payment_status !== "paid") { + return NextResponse.json({ received: true }); + }In Stripe Checkout webhook fulfillment, should handlers process both `checkout.session.completed` and `checkout.session.async_payment_succeeded`, and avoid fulfilling when `checkout.session.completed` has `payment_status = "processing"`?Also applies to: 78-88
🤖 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 `@app/api/team/extra-usage/webhook/route.ts` around lines 56 - 58, Update the webhook switch handling so credits are only issued when the Checkout Session is settled: check both event.type values "checkout.session.completed" and "checkout.session.async_payment_succeeded" (or explicitly check session.payment_status !== "processing" inside the "checkout.session.completed" case) before calling the crediting logic; use the existing session variable (event.data.object as Stripe.Checkout.Session) to inspect payment_status and move the credit issuance into a shared helper invoked only when settled to avoid double-processing across the two event types.
🤖 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 `@app/api/team/extra-usage/purchase/route.ts`:
- Around line 19-20: Wrap the JSON parsing calls (the await req.json() on the
top-level request and the similar parse in the 53-58 block) in a try/catch that
treats JSON parse errors as client errors: catch the SyntaxError (or any JSON
parsing error) and return an HTTP 400 response with a brief message instead of
letting it bubble to the generic 500 handler; update the POST/route handler to
detect parse failures around the req.json() call(s) and return new Response(...)
with status 400 when parsing fails.
In `@app/components/TeamExtraUsageSection.tsx`:
- Around line 67-72: When load() fails you must stop relying on pool being null
to indicate loading — either set an explicit error/empty state or setPool to an
empty sentinel and update rendering; modify the catch block in load() to call
setLoading(false) (already done) and also call setPool([]) or setLoadError(true)
so the UI can detect failure, and update the render condition (the branch that
currently checks if (!pool) to show "Loading team usage…") to check if (!pool &&
loading) for the loading spinner and render an error/empty-with-retry when
(!pool && !loading). Ensure you reference and update the existing symbols
load(), pool, setPool, setLoading (or add setLoadError/getLoadError) and the
retry handler to re-run load().
- Around line 485-488: The component is rounding member.monthlyLimitDollars when
initializing limitInput which can unintentionally change decimal caps; update
the useState initializer in TeamExtraUsageSection (limitInput / setLimitInput)
to preserve the original numeric value instead of Math.round — e.g. convert
member.monthlyLimitDollars to a string directly
(String(member.monthlyLimitDollars) or member.monthlyLimitDollars.toString())
when it’s not null so opening/saving the edit dialog won’t alter decimal limits.
---
Duplicate comments:
In `@app/api/team/extra-usage/webhook/route.ts`:
- Around line 56-58: Update the webhook switch handling so credits are only
issued when the Checkout Session is settled: check both event.type values
"checkout.session.completed" and "checkout.session.async_payment_succeeded" (or
explicitly check session.payment_status !== "processing" inside the
"checkout.session.completed" case) before calling the crediting logic; use the
existing session variable (event.data.object as Stripe.Checkout.Session) to
inspect payment_status and move the credit issuance into a shared helper invoked
only when settled to avoid double-processing across the two event types.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: c1719f76-c5c3-4f52-9b40-fe3a1b4b2f17
📒 Files selected for processing (3)
app/api/team/extra-usage/purchase/route.tsapp/api/team/extra-usage/webhook/route.tsapp/components/TeamExtraUsageSection.tsx
- requireTeamOrg now uses session.organizationId instead of picking the first WorkOS membership, so multi-org users can't accidentally act on the wrong org - Snapshot/restore CONVEX_SERVICE_ROLE_KEY in the team extra usage test file so it doesn't leak into other test files in the same Jest worker Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ision - Team extra usage routes now return 400 on invalid JSON instead of bubbling up as 500 - TeamExtraUsageSection shows a retry button when the initial fetch fails instead of leaving "Loading team usage…" stuck on screen - The member limit edit dialog prefills with the exact stored value so re-saving doesn't silently round a decimal cap Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/api/team/extra-usage/route.ts (1)
32-47: ⚖️ Poor tradeoffN+1 API calls to fetch user details.
Each membership triggers a separate
getUsercall. WorkOS User Management API does not support batch getUser or listUsers by IDs, so individual calls are required. For teams with many members, consider caching user details or implementing a memoization layer to reduce repeated API calls.🤖 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 `@app/api/team/extra-usage/route.ts` around lines 32 - 47, The membership mapping is making N calls via workos.userManagement.getUser inside memberships.data.map causing an N+1 API problem; fix by introducing a user cache/memoization (e.g., a Map keyed by userId) used in the members construction so you only call workos.userManagement.getUser once per unique userId and reuse the cached User for duplicate IDs, optionally batching parallel fetches for unique IDs before mapping and then reference usageByUserId and the cached users when building the members array.
🤖 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 `@app/api/team/extra-usage/route.ts`:
- Around line 24-28: The call to
workos.userManagement.listOrganizationMemberships returns paginated results so
the current code only uses the first page (memberships.data) and drops
subsequent pages; update the logic around listOrganizationMemberships to iterate
through all pages (using the SDK pagination fields such as next_cursor/has_more
or equivalent) and accumulate every page's members into a single array (replace
usages of memberships.data with the fully aggregated memberships array),
ensuring the function that consumes memberships (e.g., the variable currently
named memberships or memberships.data) receives the complete list.
---
Nitpick comments:
In `@app/api/team/extra-usage/route.ts`:
- Around line 32-47: The membership mapping is making N calls via
workos.userManagement.getUser inside memberships.data.map causing an N+1 API
problem; fix by introducing a user cache/memoization (e.g., a Map keyed by
userId) used in the members construction so you only call
workos.userManagement.getUser once per unique userId and reuse the cached User
for duplicate IDs, optionally batching parallel fetches for unique IDs before
mapping and then reference usageByUserId and the cached users when building the
members array.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 36c1d5e5-c7d1-4453-8b14-f1e580a2192f
📒 Files selected for processing (6)
app/api/team/extra-usage/members/[userId]/route.tsapp/api/team/extra-usage/purchase/route.tsapp/api/team/extra-usage/route.tsapp/api/team/team-auth.tsapp/components/TeamExtraUsageSection.tsxconvex/__tests__/teamExtraUsage.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- app/api/team/extra-usage/members/[userId]/route.ts
- app/api/team/extra-usage/purchase/route.ts
- app/api/team/team-auth.ts
- app/components/TeamExtraUsageSection.tsx
- convex/tests/teamExtraUsage.test.ts
The route only consumed the first page of listOrganizationMemberships (default limit 10), silently dropping members from teams larger than the page size. Use the SDK's autoPagination() to fetch all pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # convex/schema.ts # lib/api/chat-handler.ts
Summary
What changed
Backend
team_extra_usage(org pool) andteam_member_usage(per-member caps + spend), keyed byorganization_id.convex/teamExtraUsage.ts(queries/mutations) andconvex/teamExtraUsageActions.ts(Stripe Checkout + auto-reload). Mirrors the per-user path but charges the org's Stripe customer.lib/rate-limit/token-bucket.ts) branches onsubscription === "team" && organizationIdfor deduct, refund, and post-stream adjustments. Three new failure messages: team pool disabled, member disabled, member cap hit.buildExtraUsageConfigreads team state for team users; their personalextra_usage_enabledflag is ignored.API routes (admin-gated via WorkOS `role.slug === "admin"`)
UI
Plan: `~/.claude/plans/create-a-plan-for-magical-moonbeam.md`.
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Tests