Managed Members — Rollout Runbook
How phase 1 reaches production, how a gym is switched on, and how to switch it off. The flag model is specified in behavior.md §6a; the migration in data-model.md §5.
1. Merge = migrate
Railway runs pnpm db:migrate as the API’s preDeployCommand (apps/api/railway.toml:15). Merging PR #526 to main applies 0168_managed_members to production before the new API serves traffic. There is no separate migration step, so the owner’s approval to merge is the approval to migrate (docs/runbooks/migrations.md, hard rule 2).
Before merging:
- CI green on the PR, the branch up to date with
main, and0168’s journalwhengreater than every earlier entry (migrations.md, monotonic-when). drizzle-kit generatereports no changes.- Review the SQL once more for the two data-touching steps: empty-string emails → NULL (before the partial unique index), and the form-signature backfill.
What the migration does to existing data: sets users.email = '' to NULL, relabels every historical form signature as a self-signature, and adds defaulted columns. No existing row is re-keyed.
2. Deploy with the flag off
The flag managed-members is unset in PostHog, so it is off for every org. With it off, adults see no change apart from the small ungated items: who-booked attribution on bookings, a non-training adult promoted to trains when they get a plan, and nullable-email handling.
After deploy, check:
- Sentry and Better Stack for new errors in
users,memberships,bookingsand notifications for an hour. - An existing member signs in, books and cancels; a staff member opens the member list and a member record (no Add child, no Responsible adults filter, no household section).
3. Before turning it on for a pilot gym
Not yet verified outside the review stack:
| Check | Why |
|---|---|
| Push on a real device | Notifications about a child must reach the adult’s device and open as that child. |
| Teen sign-up block through the live Clerk webhook | Sign up with an address a child record holds: expect “ask your gym to link your account” and an audit row. |
| Youth-consent gate | Create a parental_consent_youth template; a child can’t book until their adult signs it. |
| Arbox / Fizikal mirror for the pilot | The Arbox direct sync skips children (only the CSV import creates them). Confirm neither mirror fails on, or overwrites, a member with no email. |
| A real card payment for a child’s plan | The adult is charged and receives the receipt; the child’s record shows the plan. |
4. Turning it on for a gym
- The gym’s plan must include
managed_members(Pro and above). The flag alone does nothing on a lower tier. - In PostHog, enable
managed-membersfor that org: group typeorganization, key = the org id. The API evaluates withdistinctId = orgIdandgroups: { organization: orgId }; onlytruecounts. - It applies on the next request. Staff now see Add child, the Responsible adults filter and the household section; parents see Family and, once a link is confirmed, the switcher.
5. Turning it off
Switch the flag off for the org in PostHog. It takes effect immediately, with no deploy.
| Stays | Stops |
|---|---|
| Children as ordinary members: roster, bookings, plans, billing; staff manage them from the dashboard. | Staff Add child, the filter, the household section. |
| Notifications and receipts about a child still go to their adults. | Parents’ Family area, the switcher and acting for a child (403 managed_members_disabled). |
| Every link, attestation and audit row. | The youth-consent booking requirement: a child books on what an adult does. |
| The safety guards and the teen sign-up block. | Guardian signing; CSV rows becoming children. |
Turning it back on restores everything as it was.
6. What not to do
- Don’t roll the API back below this release once the migration has run. Earlier code expects
users.emailto be a string, and phone-only sign-ups now hold NULL. Use the flag to switch the feature off. - Don’t hand-write a down migration. There is none, and undoing it is clean only while no gym has children: a child has no email to restore.
7. Mobile
Mobile (taikan-mobile PR #128) needs the new shared types. After #526 merges: publish @taikan/shared, bump it in #128, let its CI go green, then build and release through the usual flow (docs/runbooks/mobile-builds.md). The mobile app reads the same household endpoint, so it follows the flag with no separate switch.