Managed Members — Children and Dependent Accounts
Ticket: TKN-382 · Status: phase 1 implemented (PR #526 API/web, taikan-mobile PR #128), behind the PostHog flag managed-members, Pro tier and above. One migration: libs/db/drizzle/0168_managed_members.sql.
Work packages: TKN-396 … TKN-414 , 19 sub-issues of TKN-382
Review page: TKN-382 Plan Review (Notion, block-level comments)
Origin: Discovery and Product Brief, 2026-09-30 — עולם המים, an 8–12-site Israeli swim network, ~150 staff, currently on Arbox + Fizikal + a clinic-derived system.
What this is
A model in which a responsible adult (אחראי) manages a member who has no email and no login. It separates five roles that Taikan currently collapses into one row.
| Role | Who | Where it lives |
|---|---|---|
| Managed member | the person who trains, with no login | users.kind = 'managed' |
| Account holder | the person who holds the login | users + Clerk |
| Guardian / אחראי | an adult authorised to act for someone they are not | guardianships |
| Payer | the party financially liable | subscriptions.payerUserId |
| Actor | the human who performed a given action | bookings.bookedByUserId, audit_events.actorCapacity |
Revision 2: no new person entity
Revision 1 proposed a participants table with a participant_identity view to coalesce it against users. That was rejected in review, and the objection was right.
users is already a person table. clerkId is already nullable; login-less “shell users” already exist, carry memberships, form instances and legal consents, and have their own scrub lifecycle (apps/api/src/memberships/memberships.service.ts:1587-1601); and users holds name, birthDate, gender, phone, emergency contact and national ID. The only thing making it an “account” table is email NOT NULL UNIQUE.
So a managed member is a users row with no email, no Clerk identity, and kind = 'managed'. The tell that revision 1 was wrong: it needed a database view whose only purpose was to paper over having two person tables.
What that collapses:
| Revision 1 | Revision 2 |
|---|---|
memberships.participantId added, userId nullable, unique constraint swapped | No change beyond one additive participation column |
member_profiles re-keyed | No change |
form_instances.assigneeUserId relaxed and backfilled | No change; only signer columns added |
Member search repointed off users.nameEmbedding | No change |
| A coalescing view plus a single write path to stop two tables drifting | Deleted |
| 11 migrations | 6 migrations (later squashed into one, 0168_managed_members) |
Cross-branch identity survives for free: one child is one users row with one membership per branch, exactly as an adult is today. data-model.md §2 records the full reasoning so this is not re-proposed in six months.
Documents
| Document | Normative for | Read it when |
|---|---|---|
data-model.md | Schema. The decision and why revision 1 was wrong, guardianships, the migration, the one real risk. | Before touching the database or writing a query. |
behavior.md | Behaviour. Vocabulary, 13 capabilities, establishment, coverage, product flows, the feature flag, edge cases, acceptance criteria. | Before implementing any endpoint. |
design-spec.md | UI. Acting context and the five surfaces. | Before building any screen. |
code-map.md | Where every part lives: API, schema, shared, web, mobile, tests. | To find the code. |
qa-plan.md | Scenarios to verify, and which were verified for phase 1. | Before a release or a pilot. |
rollout-runbook.md | Migration, deploy, turning the flag on per gym, and turning it off. | Before production. |
research.md | Competitor analysis, 15-pattern catalogue, compliance constraint map. | To understand why a decision was made, or before overriding one. |
risks.md | Risks, blindspots, resolved and open questions. | At every wave boundary. |
execution.md | How the Linear work packages map to waves, and the rules every package obeys. Historical plan. | To start work or hand a slice to an agent. |
Phase 1: what shipped, what didn’t
Shipped (WP-00 … WP-14, WP-10b):
- Managed members as
usersrows (kind = 'managed', no email, no login),guardianshipswith 13 capabilities,memberships.participation(trains/guardian_only). - Staff-only enrolment. Staff add a child from Members → Add child or from an adult’s record; the adult confirms the link in the app (typed name, attestation; 21+ checked from their birth date, asked for on the form if missing). Parents cannot create children.
- View as child on web and mobile (switcher, delegation not impersonation), booking and cancelling for a child with attribution, guardian form signing, youth parental consent, child plans paid by the adult.
- Notifications about a child route to their confirmed adults; the child is never contacted.
- Staff surfaces: the household section on member records, the Responsible adults filter, Message {guardian} about {child}.
- Safety guards: no progress photos, no promotional sends, no minisite exposure, analytics and lead matching skip children; the teen sign-up block.
- CSV import creates children with pending links.
Not built yet: a dashboard screen for a second adult, capability edits, revocation and suppression (the API exists, WP-15); the org policy settings in behavior.md §6 (WP-16); a teen claiming a login (§5.2); an import matching-queue screen; Arbox direct sync for children; WP-17 … WP-19.
How each scenario works end to end, from the staff, parent and child side: behavior.md §3.5.
Reviewed decisions
- No new person entity. A managed member is a
usersrow withkind = 'managed'. - Guardianship is global by default, with an optional org-scoped grant. It is not a membership — a membership relates a person to an organization, a guardianship relates a person to another person — but authority from both must resolve through one service.
- Per-child subscription on one payment method. Split billing across households and consolidated invoicing are out of phase 0.
- Minimum guardian age 21. Stricter than the market, and cheap.
- No automatic age-triggered access changes. No majority cron.
selfAccessMinAgeis an org setting defaulting to off; claiming a login is guardian-initiated with guardian veto. - No messaging for managed members in phase 0. This removes the legal blocker from phase 0 entirely.
- Progress photos hard-blocked for managed members: no toggle, no consent path, no override.
- Hebrew term: אחראי. אפוטרופוס is retained only in legal documents and the staff record.
- Tier: Pro.
Where the risk is
One package. Making users.email nullable breaks every path that assumes a member has an address — and that is the mitigation, not the danger: the regenerated Drizzle type makes npx nx build api enumerate every unsafe read as a compile error. TKN-397 is gated on a clean type-check plus a parity proof that every existing adult member’s data reads back byte-identical.
Two rules while working that list: never silence a null with ! or ?? '' (an empty-string email is a silent delivery failure, or a cross-member leak if used as a matching key), and a managed member with no email is a legitimate terminal state for a send rather than an error.
In scope because it is already broken
MembershipsService.getMemberDetail (apps/api/src/memberships/memberships.service.ts:1181-1205) gates only on org membership, so any plain member can read any other member’s full PII — masked national ID, emergency contact, coaching signals — by changing a membership ID in the URL. This ticket’s own acceptance criterion A4 is already false for adults. TKN-399.
Two things worth knowing before the next customer conversation
We are behind, not ahead. Boostapp already ships a first-class לקוח קטין requiring neither email nor phone, with multiple children per parent, guardian signing and guardian payment. And our roster importer silently discards every emailless row (apps/api/src/import/import.service.ts:629-630), so the pilot customer cannot migrate to Taikan at all today. TKN-406 is the business unlock.
The differentiators are specific. No Israeli incumbent supports multiple guardians with different rights; Boostapp fuses guardian and payer into a single לקוח משלם; none has an age-derived minor status or an 18-year transition; none records which guardian signed a waiver. And the pilot customer’s own children’s waiver is written in the first person of an adult participant — «הלקוח מצהיר כי מצבו הבריאותי מאפשר השתתפות» — with no guardian clause and photo consent handled by verbally asking the photographer. That artefact is the most persuasive single thing this feature fixes.
Related tickets
| Ticket | Relationship |
|---|---|
| TKN-383 | Level and age eligibility. Consumes users.birthDate, which this ticket guarantees non-null for managed members. |
| TKN-385 | Make-up booking. Depends on entitlement attaching to the child. The pilot customer’s two plan tiers differ only by make-up entitlement. |
| TKN-388 | Escort authorisation. Seam: guardianships.canAuthorizeEscorts. No product in the category has built this — an unoccupied position, not catch-up work. |
| TKN-313 | Staff scopes. Picks up the finding that police clearance and minors-instructor qualification are bookable constraints, not HR attributes. |
| TKN-384 · TKN-390 | AI surfaces. Constrained by this ticket: exclude managed members from AI training and inference-heavy features until a privacy impact assessment exists. |
| TKN-386 | Network topology. One child at two branches is one users row with two memberships, which keeps this open. |