Skip to content

Team Management

Lets a Team / Enterprise owner share their package allowance with members under per-member caps. Built on the per-product entitlement model (migrations 195–198); this is issue #1347.

Model

  • The team’s allowance is the OWNER’s package. The owner holds team / enterprise in account_packages (from the live self-serve checkout). The shared pool counter is the owner’s entitlement_usage row for the quota group β€” so the team allowance always tracks the owner’s live subscription (a cancel/downgrade flows through automatically). There is no duplicate package record on the team.
  • team_member_quotas β€” per-member, per-group cap. A missing row or NULL limit = unlimited within the pool. Caps may oversubscribe the pool (sum of caps is not constrained); they are independent per-member lids.
  • team_member_usage β€” per-member, per-group consumption this period (the member-cap source + the per-member figure the dashboard renders). Resets on the same boundary as the pool.
  • billing_packages.team_pool β€” TRUE for team / enterprise. Only a team-capable owner package engages the team path.

Request-time resolution (migration 200)

consume_entitlement / entitlement_status / refund_entitlement consult resolve_team_consumption(user, group). A member consumes from the team pool when all hold:

  1. profiles.credit_source = 'team:<uuid>' (the member is enrolled), AND
  2. they are the team owner or an active member (joined_at set), AND
  3. the owner holds an active, team_pool package whose entitlement for the group is quota or unlimited.

Otherwise the existing personal path runs unchanged.

In the team path (quota groups only β€” remediation), both ceilings are hard caps: the pool (owner’s entitlement_usage vs the package limit) and the member cap (team_member_usage vs team_member_quotas). Either blocks. Unlimited groups (paid-plan audits) are pass-through, exactly like personal.

API (/api/teams, both entry points)

  • GET /:teamId/entitlement β€” dashboard: pool + per-member caps & usage.
  • PUT /:teamId/members/:userId/quota { quotaGroup, limit } β€” set a cap (limit: null = unlimited within pool). Metered groups only; capping an unlimited group is rejected. Admin+.
  • DELETE /:teamId/members/:userId/quota?quotaGroup=… β€” clear a cap. Admin+.
  • PUT /:teamId/members/:userId/pool { enrolled } β€” enroll/unenroll a member by pointing their credit_source at the team (or back to personal). A member only draws on the pool while enrolled β€” this is the activation switch. Admin+.

UI: the Allowance tab in the team dashboard (apps/web/src/app/teams/[teamId]/TeamAllowancePanel.tsx).

Legacy

The cents-based team pool (teams.credit_balance, team_members.credit_quota / credits_used, deduct_team_credits, team_credit_transactions) is left in place (additive migrations) but is no longer the entitlement gate. Removing it is a separate cleanup.

Tests

  • supabase/tests/team_entitlement.test.sql β€” pool ceiling, member hard cap, oversubscribe, owner-consumes, idempotency, exhaustion, refund (both counters), period reset, and the four personal-path fallbacks.
  • workers/api/src/__tests__/routes/teams.test.ts β€” the four endpoints.