Skip to content

Let an admin lift or change one account's weekly AI cap - #261

Merged
Programcode merged 1 commit into
mainfrom
fix/per-account-weekly-cap-bypass
Oct 28, 2026
Merged

Programcode merged 1 commit into
mainfrom
fix/per-account-weekly-cap-bypass

Conversation

@Programcode

@Programcode Programcode commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Staging test accounts keep hitting the $2 weekly AI budget. This adds a per-account override, set the same way as daily_limit_override: in user_settings, by an admin or the service role only.

What changes

  • Migration 20261028120000_weekly_budget_override.sql (additive) adds two columns to public.user_settings:
    • weekly_budget_override_usd numeric (nullable, CHECK >= 0). NULL keeps the global WEEKLY_AI_BUDGET_USD ($2). A value replaces it. 0 refuses every request that week.
    • weekly_budget_unlimited boolean NOT NULL DEFAULT false. When true, the weekly check is skipped, even if an override value is set.
  • getWeeklyBudgetUsd in _shared/entitlements.ts reads both. checkRateLimit passes the result as p_weekly_cap_usd to consume_ai_quota and claim_ai_session_quota. Neither RPC changes; a NULL cap already skips the weekly check in both. peekRateLimit (the quotaCheck that Crossfire and Sparring send before a round) compares against the same per-account value, so an unlimited account can start rounds. The daily limit logic is unchanged.
  • The columns are read in their own query, not inside getSubscriptionInfo's select. If functions are deployed before the migration, that query fails, and the code logs a warning and keeps the global cap. It never lifts the cap on a failed read.

Users cannot set it

20260819000000_security_hardening.sql revoked table-level INSERT/UPDATE/DELETE on user_settings from anon and authenticated. Users can update only named preference columns (language, theme, feature_configs, custom_system_prompt, output_language, updated_at), so new columns start out server-only and no grant changes are needed. The client's user_settings writes in settings-store.ts name only preference columns. The SECURITY DEFINER functions that write user_settings (promo redemption, payment handling) list their columns explicitly and never touch these two. The pgTAP test checks the column privileges and also runs a real UPDATE as an authenticated user, which fails with 42501.

Usage screens

The plan card and Settings show only daily numbers, so they are unchanged for an overridden account. The weekly allowance shows up only as the refusal dialog and the Sparring/Crossfire pre-round message. Both now follow the account's own cap: an unlimited account never sees them, and a capped account still does.

Tests

  • Edge (entitlements.test.ts): no override sends the global cap, a value (number, numeric-as-string, 0) replaces it, unlimited sends null, a failed override read keeps the global cap, and the pre-round peek respects the override. Three of these fail on main's code.
  • pgTAP (user_settings_privileges_test.sql): authenticated has no UPDATE privilege on either column, a real self-update of each column is denied, new accounts default to no override, and a negative value is rejected.

What ran

Local only, at 682d83b2. GitHub Actions is billing-blocked, so CI failures on this PR are expected and say nothing about the code.

  • NODE_OPTIONS=--max-old-space-size=3072 npm run verify got through docs check, jargon gate, lint, typecheck, unit tests (714 passed), collaboration tests (30 passed), edge tests (498 passed) and the build. It was stopped during test:sw, so the service-worker check and the e2e smoke run did not complete.
  • Not run: the pgTAP database gate (so the new user_settings_privileges_test.sql assertions have not been run yet) and the verify-debatelab usage-limits recipe. Adam skipped them because the change is small.

Enabling it on staging (after the migration is applied there)

This lifts the cap for seed-admin and seed-pro. seed-trial and seed-expired stay on the normal $2 cap, so the weekly-limit screens can still be tested on seed-trial:

UPDATE public.user_settings
SET weekly_budget_unlimited = true, updated_at = now()
WHERE user_id IN (
  SELECT id FROM auth.users
  WHERE email IN ('seed-admin@staging.debatelab.test', 'seed-pro@staging.debatelab.test')
);

-- check
SELECT u.email, s.weekly_budget_override_usd, s.weekly_budget_unlimited
FROM public.user_settings s JOIN auth.users u ON u.id = s.user_id
WHERE u.email LIKE 'seed-%@staging.debatelab.test'
ORDER BY u.email;

The staging seed reruns after every sync. It upserts user_settings with resolution=merge-duplicates and sends only the plan columns, so the flag survives reseeds. To undo: SET weekly_budget_unlimited = false.

Deploying

  • Migration: supabase/migrations/20261028120000_weekly_budget_override.sql.
  • Edge Functions whose behavior changes: ai and ai-stream. voice also bundles entitlements.ts but only uses functions this PR doesn't touch, so redeploying it is optional. stripe-webhook and create-checkout bundle usage-limits.ts, where only a comment changed.
  • Merging to main triggers the staging sync timer (ops/staging-sync/run.sh), which applies pending migrations and deploys changed functions on staging within about 5 minutes. Production is not touched by the merge.
  • Not done here: no migration applied anywhere, no function deployed.

Not covered

export_account_data exports user_settings generically, so a user's own export will include the two new columns. Redacting them would mean replacing that whole function, which is out of scope here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Weekly AI spending limits can now be adjusted per account by administrators, or lifted entirely. Accounts without an override continue to use the default weekly limit.
    • The daily usage limit still applies when an account’s weekly limit is lifted.
  • Documentation

    • Clarified that weekly spending limits include costs from AI requests and related services.

Staging test accounts keep hitting the $2 weekly AI budget. Two new
user_settings columns, writable only by the service role, override it per
account: weekly_budget_override_usd replaces the cap and
weekly_budget_unlimited skips it. NULL and false keep the global cap. The
daily limit is unchanged.

checkRateLimit passes the account's cap to consume_ai_quota and
claim_ai_session_quota, and peekRateLimit compares against it, so the
pre-round quota check agrees with the real reservation. The columns are read
apart from getSubscriptionInfo, so functions deployed before the migration
keep working, and a failed read keeps the global cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 18:41

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 insufficient remaining quota.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between b30c53b and 682d83b.

Files selected for processing: 7
  • .claude/skills/verify-debatelab/features/usage-limits.md
  • docs/architecture.md
  • supabase/functions/_shared/entitlements.test.ts
  • supabase/functions/_shared/entitlements.ts
  • supabase/functions/_shared/usage-limits.ts
  • supabase/migrations/20261028120000_weekly_budget_override.sql
  • supabase/tests/user_settings_privileges_test.sql

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The change adds per-account weekly budget overrides and an unlimited setting. Entitlement checks resolve the account cap and apply it to quota reservations and weekly-limit peeks. The migration, tests, and documentation cover the new settings and behavior.

Changes: Weekly budget overrides

Layer / File(s) Summary
Budget settings and access restrictions
supabase/migrations/20261028120000_weekly_budget_override.sql, supabase/tests/user_settings_privileges_test.sql
The migration adds the nullable numeric override and the unlimited flag, with a constraint that rejects negative overrides. SQL tests check the defaults, constraint, and authenticated-user access restrictions.
Resolve and apply the weekly cap
supabase/functions/_shared/entitlements.ts, supabase/functions/_shared/entitlements.test.ts, supabase/functions/_shared/usage-limits.ts, docs/architecture.md, .claude/skills/verify-debatelab/features/usage-limits.md
Entitlement checks resolve the account’s cap and pass it to quota reservations. Weekly-limit peeks use the resolved cap and skip weekly denial when it is null. Tests cover cap selection and peek behavior; documentation describes the settings and a test setup caveat.

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

Sequence Diagram(s)

sequenceDiagram
  participant Entitlements
  participant user_settings
  participant QuotaRPC as Session-ledger and per-call quota RPCs
  Entitlements->>user_settings: Read weekly budget settings
  user_settings-->>Entitlements: Return override and unlimited flag
  Entitlements->>QuotaRPC: Reserve quota with resolved weekly cap
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

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

@Programcode
Programcode merged commit c06378c into main Oct 28, 2026
3 of 14 checks passed
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