| 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 |
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 entitlementUntil 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:
lib/access.js and the billing Lambda's PROMO_ENDS both hold it. Movethem together or the client and server disagree about who has paid for what.
take money.
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.
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.
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.