Skip to content

fix(beta): grant free premium on conflict so the first ~500 users are unlocked#99

Merged
miquelmatoses merged 1 commit into
mainfrom
fix/beta-grant-on-conflict
Jun 17, 2026
Merged

fix(beta): grant free premium on conflict so the first ~500 users are unlocked#99
miquelmatoses merged 1 commit into
mainfrom
fix/beta-grant-on-conflict

Conversation

@miquelmatoses

Copy link
Copy Markdown
Collaborator

Problem (the reported symptom)

A registered account is sent to the Stripe payment step for Full Moon instead of having it unlocked, even though beta slots remain.

Root cause (clear defect, not an undecided policy)

The "first ~500 users free" grant was builtensure_profile (api/main.py), BETA_TOTAL = 500, the public /beta slots endpoint, and ADR 0012 all assume it is live. But it never fired:

  • The grant ran only on ensure_profile's fresh-INSERT branch.
  • The registration handlers in api/auth.py create the profiles row first (with no premium/beta).
  • So by the time ensure_profile runs (/me/profile, /me/results, /results) the row already exists and only ON CONFLICT DO UPDATE SET email executed → every user stayed premium = false.
  • FullMoonPage.jsx reads profile.premium; false → paywall.

This is a wiring defect against intended behaviour, so a fix is appropriate. (The enforcement side — require_premium — and the grant's sunset/expiry remain genuinely open in ADR 0012; this PR does not decide them.)

Fix

Grant on the conflict branch too: set premium/is_beta when a slot remains AND the row is not already premium or beta.

  • Unlocks new registrations and retro-grants the users who were defectively denied, on their next authenticated request (/me/profile runs ensure_profile then reads premium in the same request).
  • Never revokes an existing grant or paid premium (OR with the current value).
  • A paid customer is never relabelled beta (grant requires NOT premium).
  • The 500 cap is enforced by the remaining-slots subquery.

Semantic note for ADR 0012: because is_beta was ~0 in prod (the grant had never run), this shifts "first 500 to register" to "first 500 to make an authenticated request after the fix." Bounded and consistent with the early-adopter intent; flagged in the ADR.

Migration 029 (idempotent, no-op on prod)

profiles.is_beta is read/written by ensure_profile and /beta and cited by ADR 0012, but was created directly on production and never had a migration. 029 records it (add column if not exists … default false) so the schema is reproducible. Not strictly required for the fix (prod already has the column); apply via the sanctioned apply-migrations.yml at your convenience.

Tests

New api/tests/test_beta_grant.py: regression guard that the conflict branch grants premium+is_beta (not email-only), plus a truth-table test of the grant boolean (grant eligible, cap exhausted, already-beta unchanged, paid never revoked/relabelled). No DB in CI, so the SQL boolean is mirrored in Python. Full backend suite green.

ADR 0012 Context/Consequences corrected (they described the grant as live).

🤖 Generated with Claude Code

…ocked

The 'first ~500 users get Full Moon free' beta grant was built in
ensure_profile (BETA_TOTAL=500) but never fired. The grant ran only on
the fresh-INSERT branch, yet the registration handlers in api/auth.py
create the profiles row first, so ensure_profile always hit ON CONFLICT
DO UPDATE SET email — leaving every user on premium=false and routing
them to the Stripe paywall (FullMoonPage.jsx reads profile.premium).

Grant on the conflict branch too: set premium/is_beta when a slot
remains AND the row is not already premium or beta. This unlocks new
registrations and retro-grants the users who were defectively denied on
their next authenticated request (/me/profile runs ensure_profile then
reads premium in the same request). Existing grants and paid premium are
never revoked (OR with current value); a paid user is never relabelled
beta (grant requires NOT premium); the 500 cap is enforced by the
remaining-slots subquery.

Also add migration 029 to record profiles.is_beta in version control: the
column is read/written by ensure_profile and the /beta endpoint and cited
by ADR 0012, but was created directly on prod and had no migration.
Idempotent, no-op on prod. ADR 0012 corrected to note the grant was dead
(its Context/Consequences had described it as live).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@miquelmatoses miquelmatoses merged commit 188dca1 into main Jun 17, 2026
7 checks passed
@miquelmatoses miquelmatoses deleted the fix/beta-grant-on-conflict branch June 17, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant