Skip to content

Remove the Classroom tier; entitle owner accounts at the database layer - #44

Merged
mapzimus merged 1 commit into
masterfrom
claude/tappymaps-revamp-audit-4gm8pj
Aug 23, 2026
Merged

Remove the Classroom tier; entitle owner accounts at the database layer#44
mapzimus merged 1 commit into
masterfrom
claude/tappymaps-revamp-audit-4gm8pj

Conversation

@mapzimus

@mapzimus mapzimus commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Two commits. The second is the substantive one.

1. Owner accounts were entitled in the UI but not in the database (dd0d331)

Follow-up to #43, which shipped the schema v2 RLS policies. ADMIN_EMAILS in index.html grants Pro client-side only, so once RLS started deciding cloud-sync and gallery writes from user_subscriptions.tier, Postgres stopped agreeing with the UI — an owner account would see the paid tools, click one, and get a denial. The one real subscription row is max@mapparatus.org on the grandfathered $5/mo price with current_period_end back in May, so both owner accounts were free-tier as far as the database was concerned.

supabase/migrations/20260822_app_admins.sql adds an app_admins table with RLS enabled and no policies at all — no client, anon or authenticated, can read or write it; only SECURITY DEFINER functions and the service role see it. It is seeded from auth.users by email, and active_subscription_tier() reads it, so every existing policy picks the grant up with no policy edits.

This is an owner-access convenience, not a security boundary — verify-subscription remains the authoritative paid check.

2. Remove the Classroom tier (2687acd)

Tappymaps is not a school product. The $12/mo Classroom tier — class codes, printable worksheet packs, a /class/:code route — framed it as one, so it comes out in full rather than hiding behind a flag.

Checked before removing anything destructive: production held 0 rows in classroom_codes and 0 subscriptions at tier classroom. Nobody loses access and no paying customer is affected.

App (index.html, −497 lines)

  • Deleted the Classroom tools + upsell blocks in the Create Upgrade panel, their CSS, and the upgrade modal's Classroom button.
  • Deleted the class-code / worksheet subsystem: code mint / copy / join, the classroom_lookup RPC call, exportWorksheetPack, Modes.ClassJoin and its route, and wireClassroomUI.
  • Collapsed entitlements to one paid tier — dropped appState.classroomUnlocked, isClassroom(), startClassroomCheckout(), updateClassroomUI(). Owner accounts auto-grant pro.
  • Rebuilt /pricing as Free vs Pro (was Pro vs Classroom-for-teachers) and dropped the "Have a class code?" join box.
  • Dropped "high-school math teacher" from the About bio.

Billing handlers

  • create-checkout no longer resolves a classroom plan or reads STRIPE_CLASSROOM_MONTHLY_PRICE_ID.
  • verify-subscription drops isClassroom from its payload; tier is pro | free.
  • webhook's tierForPrice always resolves 'pro'. The indirection stays so a future second tier is a one-line change rather than a migration.

Databasesupabase/migrations/20260822_remove_classroom.sql, applied to production

  • Dropped classroom_lookup(text) and classroom_codes (taking its two RLS policies with it).
  • Redefined active_subscription_tier() so admins get 'pro', and has_subscription() without the classroom-implies-pro special case.
  • Re-asserted the schema v2 grant posture on both redefined functions.

Verified by JWT impersonation after applying:

max@mapparatus.org     tier=pro     has('pro')=true   has('classroom')=false
mhowe.gis@gmail.com    tier=pro     has('pro')=true   has('classroom')=false
random authenticated   tier=(none)  has('pro')=false  has('classroom')=false

classroom_codes dropped: true    classroom_lookup dropped: true
stray classroom rows: 0          policies mentioning classroom: 0

Guardrails. checkout-plan-smoke now asserts that classroom / teacher / worksheet / class-code wording cannot reappear in index.html or the checkout handler, and CLAUDE.md records the constraint so a later session does not rebuild it.

Map subject matter is deliberately untouched: a Census Bachelor's Degree+ dataset, or a GeoDraft backlog category like "high school graduation rate", is ordinary demographic data rather than school framing. None of those backlog categories are wired into the app today.

Verification

  • npm run validate — both blocks pass; Block 1 unchanged at 34,471 chars
  • node --test test/*.test.mjs — 18/18
  • node scripts/api-subscription-test.mjs — 16/16
  • node scripts/checkout-plan-smoke.mjs — pass, including the new negative guards
  • node scripts/extract-data.mjs --check — all modules current
  • npm run smoke — all checks pass, zero console errors
  • /pricing and the Create Upgrade panel inspected in a real browser

Note for whoever touches billing next

Every write to user_subscriptions must keep setting tier. The database cannot read Stripe price IDs, so a row written without it locks a paying customer out at the RLS layer even though checkout succeeded.

…e UI

Schema v2 moved entitlement enforcement into RLS, but the client has granted
Pro + Classroom to ADMIN_EMAILS since long before that. The two layers now
disagreed: the owner accounts would see a Classroom UI while Postgres refused
their cloud-sync writes and class-code creation. A user being told they have a
feature and then silently denied it is worse than either answer alone.

Surfaced by the one live subscription row — it belongs to max@mapparatus.org,
on the grandfathered $5/mo price, with its period ended 3 months 19 days ago.
Entitled in the UI, past grace in the database.

app_admins mirrors ADMIN_EMAILS by user id, seeded from auth.users by email so
it cannot drift onto the wrong account. RLS is enabled with no policies at all,
which denies every client — only the service role and SECURITY DEFINER
functions can read it.

Verified by impersonating each account's JWT claims:

  max@mapparatus.org    -> classroom  (admin; subscription expired 3mo ago)
  mhowe.gis@gmail.com   -> classroom  (admin; no subscription row at all)
  random authenticated  -> null
  anonymous             -> null

so the allowlist entitles exactly the two intended accounts and nobody else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mapparatus Ready Ready Preview Aug 22, 2026 6:40pm
tappymaps Ready Ready Preview Aug 22, 2026 6:40pm

@mapzimus
mapzimus marked this pull request as ready for review August 23, 2026 02:22
@mapzimus
mapzimus merged commit 007221d into master Aug 23, 2026
6 checks passed
@mapzimus mapzimus changed the title fix(db): entitle owner accounts at the database layer, not just in the UI Remove the Classroom tier; entitle owner accounts at the database layer Aug 23, 2026
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