Skip to content

feat(admin): grant/revoke comp Pro without faking a Stripe subscription - #225

Merged
brianorwhatever merged 1 commit into
mainfrom
chore/admin-grants
Aug 21, 2026
Merged

feat(admin): grant/revoke comp Pro without faking a Stripe subscription#225
brianorwhatever merged 1 commit into
mainfrom
chore/admin-grants

Conversation

@brianorwhatever

Copy link
Copy Markdown
Contributor

Adds convex/adminGrants.ts, the two internal mutations used to comp raedugas@gmail.com after they hit the free-plan list cap.

Merge this to stop prod drifting

These functions are already live in production but are not in main. I deployed them directly to run the grant; the commit missed #224's merge window. Convex deploys are declarative, so the next push to main touching convex/** will push main's function set and delete adminGrants from prod.

The grant itself survives either way — it's a patch to the user's row, not a function — but revokeProByEmail would silently disappear. The file here is byte-identical to what's deployed (verified by checksum).

Why not a subscriptions row

Writing one by hand is the obvious move and a trap. stripeCustomerId is required, and both createCheckoutSession and createPortalSession pass that value straight to the Stripe API:

if (existingSub) customerId = existingSub.stripeCustomerId;   // billingActions.ts

await stripe.billingPortal.sessions.create({ customer: sub.stripeCustomerId,});

An invented id means the user can never actually pay, and their billing portal 500s — the opposite of a favour.

referralProUntil is already the app's comp lever, honoured by getUserPlan (billing.ts:57) and by assertListQuota (lists.ts:84), and it leaves Stripe untouched.

Guards

This writes real user billing state, so:

  • Email must match exactly one user. The schema doesn't make email unique; granting to the wrong one of several is worse than refusing and making the caller name an id.
  • until must be in the future, rather than silently granting nothing.
  • Both return the previous value, so a grant can be undone by hand if revoke isn't enough.

internalMutation, so CLI-only and unreachable from any client.

Verification

Guards, happy path, and getUserPlan flipping to "pro" were all exercised on dev first, and the dev grant reverted before touching prod. 169 tests pass, tsc -b clean.

Usage

npx convex run --prod adminGrants:grantProByEmail '{"email":"…","until":4942965835900}'
npx convex run --prod adminGrants:revokeProByEmail '{"email":"…"}'

🤖 Generated with Claude Code

Needed to comp a user who hit the free-plan list cap. Writing a subscriptions
row by hand would have been the obvious move and is a trap: stripeCustomerId is
required, and both createCheckoutSession and createPortalSession pass that value
straight to the Stripe API. An invented id means the user can never actually pay
and their billing portal 500s — the opposite of a favour.

referralProUntil is already the app's comp lever, honoured by getUserPlan
(billing.ts) and by assertListQuota (lists.ts), and it leaves Stripe alone.
These two internalMutations just make it operable from the CLI.

Guards, because this writes real user billing state: email must match exactly
one user (the schema does not make email unique, and granting to the wrong one
of several is worse than refusing), and `until` must be in the future rather
than silently granting nothing. Both return the previous value so a grant can
be undone by hand if revoke is not enough.

Verified on dev first — guards, happy path, and getUserPlan flipping to "pro" —
then reverted the dev grant before touching prod.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brianorwhatever
brianorwhatever merged commit fed3290 into main Aug 21, 2026
3 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.

1 participant