Plans, access and learners
The plans
| plan | price | what it is |
|---|---|---|
| Curious | free | an account, plus Class 1 of every subject, forever |
| Band | $9/mo | one stage — a single tier |
| Family | $18/mo | every subject, up to 4 learner profiles, parent dashboard |
| Schools & districts | talk to us | rostering, teacher and admin dashboards, cohort analytics, invoice billing |
Who can see what, right now
Access is decided by hasAccess() in lib/access.js, in this order:
anonymous → no
signed in, in promo → yes, everything, free
signed in, admin → yes
otherwise → only with a live Stripe entitlementThe launch promo ends 2026-09-15
Until then, every signed-in account has full access at no cost. After that date the third and fourth rules take over and unpaid accounts fall back to the Curious plan: Class 1 of every subject.
Two things to know about that date:
- It is duplicated —
lib/access.jsand the billing Lambda'sPROMO_ENDSboth hold it. Move
them together or the client and server disagree about who has paid for what.
- Charging is gated on both sides. Until the gates flip, the system fails shut: it will not
take money.
"Live" is broader than "active"
A subscription counts as live when Stripe reports active, trialing or past_due. Matching only active locked trialing subscribers out and cut past_due ones off mid-dunning — money taken, access denied. past_due is deliberately included so a failed retry does not eject a paying customer.
Learner profiles
A Family plan holds up to 4 learner profiles in one household. Each has its own name, avatar, progress, mastery and streak — switching learner switches all of it.
Profiles are managed from 13 · Account and settings; the parent-facing rollup is 11 · Parent view.
What is *not* an access control
user.plan. It is a client-side field, it is editable in the browser, and it was once settable from a query string. Nothing gates on it and nothing should — the entitlement written by the Stripe webhook is the only server-authoritative record of who has paid.