Managed Members — Risks, Blindspots and Open Questions
Ticket: TKN-382
Status: revision 2, aligned with data-model.md revision 2. A managed member is a users row with kind = 'managed', no email and no Clerk identity; the one new table is guardianships. Risk and question identifiers are stable across revisions — withdrawn items keep their number so the review page and Linear tickets still resolve.
Three sections: risks we have identified and must mitigate, blindspots not in the original framing, and open questions requiring a decision from you, the operator, or counsel before or during build.
1. Risk register
Severity is impact × likelihood, judged against a real pilot with a swim network.
R-01 — Withdrawn in revision 2
Was: memberships.userId becoming nullable. Revision 2 does not touch memberships.userId — the child has their own users row — so the risk no longer exists. Its successor is R-18.
R-18 — Every read of users.email must now handle null — High / Certain
Numbered R-18 so existing identifiers stay stable; it is the lead risk of revision 2.
users.email is today varchar(255) NOT NULL UNIQUE (libs/db/src/lib/schema/users.ts:22). A managed member has no email. Every code path that reads it must now handle null: email sends, Clerk sync, exports, the importer’s matching logic (apps/api/src/import/import.service.ts:629-630), lead conversion, receipts. This is the real cost of reusing users as the person table, and it is stated rather than hidden (data-model.md §2, §6).
Mitigation, and why it is strong: make the column nullable in the Drizzle schema. The regenerated row type is email: string | null, so npx nx build api enumerates every unsafe read as a compile error. The type system performs the audit; no reviewer has to be diligent. It is cheaper than revision 1’s version of the same mechanism, because one column changes instead of five tables. TKN-397 (WP-02) is gated on a clean type-check plus the parity spec (R-17), not on inspection.
Two rules for working the compiler’s list:
- Never silence a null with
!or?? ''. An empty-string email is a silent delivery failure or, worse, a cross-member leak if it is used as a matching key — two managed members would both “match”''. - A managed member with no email is a legitimate terminal state for a send, not an error. Skip and record; do not throw. Notification fan-out to guardians is the correct path (TKN-404 (WP-09)).
R-19 — Notifications to a managed member fail silently, and the compiler cannot catch it — High / Certain
New in revision 2. It is the price of the decision in R-18’s mitigation, and it must not be discovered in production.
R-18’s safety net has a hole. Revision 1 made memberships.userId nullable, so a delivery call keyed on it broke at compile time. Revision 2 leaves memberships.userId NOT NULL (libs/db/src/lib/schema/memberships.ts:29) because the child has their own users row — so every one of these calls receives a genuine uuid, succeeds, and reaches nobody. No type error, no exception, no log.
Email is not safe either, and the reason is the finding in R-22: strictNullChecks is off in apps/api, so sendBookingEmail’s user: { email: string } signature (apps/api/src/bookings/bookings.service.ts:1304) accepts a null without complaint. Under the differential gate it does surface — it is one of the 71 measured sites. Push notifications and the payment gateway are unsafe under either setting, because they key on a non-null uuid. Verified sites:
| Site | What silently reaches nobody |
|---|---|
apps/api/src/bookings/bookings.service.ts:646 | Booking confirmation push |
| same file, the delayed T-30 reminder job | Session reminder |
apps/api/src/forms/forms.service.ts:1437 | ”You have a form to sign” push — keyed on instance.assigneeUserId, which for a child’s waiver is the child. The one person who cannot sign it is the only person told about it. |
apps/api/src/forms/forms.service.ts:2531 | Form-to-sign push, staff-issued path |
apps/api/src/check-ins/check-in-scheduler.service.ts:150 | Check-in reminder, keyed on assigneeUserId |
apps/api/src/subscriptions/subscriptions.service.ts:1858,2390 | customerEmail: sub.membership.user?.email — already optional-chained, so the gateway receives undefined and the receipt has no address. For a child’s subscription this must resolve to the payer, not the member. |
The forms row is the sharpest: it is exactly the guardian-signing flow, and the failure mode is a waiver that never gets signed because nobody was told, which then blocks the child’s booking through the forms gate.
Mitigation: the list above is enumerated in TKN-404 (WP-09) and the payment row in TKN-403 (WP-08). Each site resolves its recipients through one helper — resolveNotificationRecipients(subjectUserId) — which returns the subject when they are an account holder and the guardians holding canReceiveNotifications when they are managed, and which returns an empty array only for a genuinely unreachable member, logging it as a delivery gap rather than returning silently. An integration test asserts that a booking for a managed member produces a notification to the guardian and zero attempts against the child.
Note the scope correction: the uuid-keyed push sites are genuinely invisible to any compiler setting, because a managed member has a valid users.id. The email-keyed sites are a different class — invisible by default, but enumerable, and now enumerated. Only the first group needs hand-auditing.
R-22 — strictNullChecks is off in apps/api, so none of the above is compiler-enforced — Critical / Certain
Found while implementing TKN-396. It invalidated the mechanism the plan’s gate was built on, and it is the single most consequential correction in this document.
The plan assumed relaxing users.email to nullable would make tsc enumerate every site needing attention — R-18’s whole safety net, and TKN-397’s reason to exist. It does not.
$ npx tsc --showConfig -p apps/api/tsconfig.app.json
strict: <unset → false>
strictNullChecks: <unset → false>With strictNullChecks off, string | null is assignable to string. Measured after making the column nullable: npx nx typecheck api reports zero errors, and a probe asserting (u: UserRecord): string => u.email compiles — which is impossible if the type is nullable. Two independent green signals, both meaningless:
| Signal | Why it proves nothing |
|---|---|
nx build api | @nx/js:swc with skipTypeCheck: true. Transpile only — completes in 1.7s cold. Cannot fail on a type error at all. |
nx typecheck api | Real tsc --noEmit, but inherits strictNullChecks: false. Will never flag a null-email site. |
So the entire sweep would have shipped with a green build and a green typecheck. Note apps/web already sets "strict": true (apps/web/tsconfig.json), so the gap is apps/api alone.
Mitigation: a differential strict-null gate, scripts/strict-null-gate.sh, committed with recorded baselines. It runs tsc --strictNullChecks over the existing project config and fails when the set of error sites grows. Sets rather than counts, so an unrelated fix that removes a baseline error cannot mask a new one.
| sites | |
|---|---|
main baseline, api | 186 |
main baseline, web | 0 (genuinely clean — already strict) |
introduced by making email nullable | 71 |
The 71 are concentrated exactly where this document predicted: memberships (20), notifications (10), subscriptions (8), payments (7). The full list is on TKN-397. That the whole repo is only 263 is itself useful: adopting strictNullChecks in apps/api is a tractable separate project, not a multi-thousand-error wall.
Two sub-findings worth keeping, because both are the same false-negative shape as the bug above and both appeared while building the gate: resolving a non-existent project file makes tsc exit before emitting diagnostics (reads as zero errors), and grep exiting 1 on no matches makes a genuinely clean app fail identically to a broken invocation under set -o pipefail. Clean must be distinguishable from broken; the script now detects both explicitly rather than inferring from an exit status.
R-20 — Identity matchers fall through to phone, and a child carries the guardian’s number — Medium / Certain if unguarded
New in revision 2. Two corrections are recorded here rather than quietly dropped: my original hypothesis about the failure mode was wrong, and my first severity rating was overstated.
I expected the danger to be an email Map/Set/ON CONFLICT coercing NULL to '' and collapsing two managed members. That does not exist — every email-keyed collection derives its keys from invitations.email (NOT NULL) or a users.email typed string, so it surfaces as a compile error instead. Verified absent.
The real cross-person merges are phone-keyed, and they are worse, because a child normally carries the guardian’s phone number. With no email, every identity matcher falls through to the phone clause and finds the guardian. This is the exact shape of the Fizikal breach — phone-as-identity plus a shared family number (research.md §1).
| Site | What happens |
|---|---|
organization-leads.service.ts:2834-2880 → :4628-4672 | A desk-created managed member emits MEMBERSHIP_ACTIVATED, matches the guardian’s lead on phone alone (first row, limit 1, strict flag off by default at :4470-4483), flips it to converted, exits the guardian’s nurture enrolments, and credits the conversion to the child. isPhoneSharedInOrg:4580-4609 does not save us: it needs ≥2 distinct named memberships and continues on a nameless row. |
arbox-member-mirror.service.ts:607-630, :183-260 | Every active managed member is auto-queued for Arbox user.create with no role filter. Phone-only probe links the child to the guardian’s Arbox user, then runUserUpdate:399-430 overwrites the guardian’s Arbox name, email and phone with the child’s. The unique(org,type,arboxId) index makes a second child fail rather than merge, hiding the first bad link. |
automation-dispatcher.service.ts:731-743 | recipientBudgetKey = sha256(email ?? phone ?? id). NULL email falls to phone, so a guardian and all their children share one daily budget of 3 sends, silently suppressed. |
external-payment-matcher.service.ts:145-195 | A child sharing the phone makes the guardian’s phone match ambiguous, so previously auto-matched payments silently fall to manual review. |
Mitigation: one guard, four places. Graded Medium rather than Critical because users.kind = 'managed' plus a guardianships row is a clear, cheap discriminator, and every one of those call sites already loads the users row — organization-leads.service.ts:2833-2837 and automation-dispatcher.service.ts:611-614 both do a users.findFirst with an explicit columns list, arbox-member-mirror.service.ts:677 already computes eligible from three clauses, and member-account-status.ts:35-41 has users joined by contract. So each fix is adding kind to a select and one early return or one &&.
There is no sweep here. The inventory’s value is the list of places to put the guard, not the difficulty of any one of them. TKN-415 (WP-10b), Medium, Wave 4 — not a gate on anything.
Lead identity merging and Arbox direct sync are descoped outright for managed members rather than fixed, which is both cheaper and more correct: a child genuinely should not be auto-converted from a guardian’s lead or mirrored into Arbox as its own user.
Worth keeping a guard-rail test against reintroducing phone-only matching, because it reads as a reasonable fallback.
R-21 — An email on a login-less row is an account-claim key — High / Certain
Direct consequence of the decision that a managed member may hold an email.
users.service.ts:101 and memberships.service.ts:1719-1721 treat an email on a clerkId IS NULL row as a claim key: whoever signs up with that address inherits the row, along with the member’s entire history. That mechanism is correct and load-bearing for imported adults. It becomes a takeover surface the moment a managed member carries a real email, because nobody is defending that address.
Related, and why the migration order matters: users.service.ts:92-97 writes email: '' for a Clerk user with no address, and '' already occurs in practice. Under a partial unique index WHERE email IS NOT NULL, '' is non-null and unique — so the second email-less Clerk user fails hard, and eq(users.email, '') AND clerkId IS NULL would link any '' row to an unrelated Clerk identity.
Mitigation: linking a login goes through a token that names the row, never an email match (data-model.md §3.9). Either suppress the email-claim path for kind = 'managed' or require the token — not both open. And normalise '' → NULL before creating the index, changing findOrCreateFromClerk in the same commit or the next webhook undoes it. On TKN-396, verified by TKN-415.
R-02 — The weak getMemberDetail endpoint already violates the acceptance criterion — Critical / Certain
MembershipsService.getMemberDetail (apps/api/src/memberships/memberships.service.ts:1181-1205) gates only on requireMembership(orgId, clerkId) — “caller belongs to this org, any role”. Any plain member can GET /organizations/:orgId/members/:anyMembershipId and receive another member’s full profile: masked national ID, emergency contact, coaching signals, account status.
A4 says “an unrelated adult cannot access that record.” This is already false today for adults, and children make it worse. Fixing it is in scope, not adjacent — TKN-399 (WP-04). getMemberStats (memberships.service.ts:3328) needs the same review.
R-03 — Staff role checks are inconsistent across three idioms — High / Certain
PermissionsService.enforce (the intended gate), isStaffRole(role), and ~45 raw inline comparisons coexist. PermissionsService’s own doc comment says it was built to replace the scattered checks; that migration is visibly incomplete.
Guardianship adds a fourth authorization axis. If it is introduced as another call-site idiom, the combinatorics become unreviewable and a missed check is a data breach involving a child. Mitigation: one GuardianAccessService resolving can(actorUserId, subjectUserId, capability, orgId) + one guard decorator (TKN-398 (WP-03)), with a test that fails if any route scoped to a managed member lacks the decorator. Do not migrate the existing 45 call sites in this ticket — that is a separate refactor — but do not add to them either.
R-04 — Vendor mirror will fail for exactly the customer who motivated the feature — High / Likely
bookings.service.book() enqueues outbox rows mirroring bookings to Fizikal/Arbox. A managed member has no email, and those vendors may require one. The pilot customer runs Arbox and Fizikal.
Mitigation: test against a real vendor sandbox before the pilot, not after (TKN-414 (WP-19)). If the vendor rejects emailless members, options are a per-vendor synthetic routing address (held in external_entity_map, never in users — R-18’s rule against fake emails applies), or disabling mirror for managed members with an explicit operator warning. Do not discover this during the pilot. Note external_entity_map.taikanId is already nullable for vendor stub rows — precedent exists.
R-05 — Health data pushes the tenant into Israel’s HIGH security regime — High / Likely
Medical information is special-sensitivity by statute. A database holding it sits at medium level, escalating to high if authorised users exceed 100. The pilot has ~150–170 staff. High level mandates a documented risk survey, penetration tests every 18 months and quarterly security-event review. Fines run at NIS 4/person, doubled to NIS 8 for special-sensitivity data.
Mitigation: minimise which records carry health data and how many staff accounts can read it. canViewHealth separability is an economic control, not only an ethical one. Consider isolating health declarations behind their own access boundary so the high-level regime does not swallow the entire platform. Needs an explicit decision — Q-12.
R-06 — Guardian-visible messaging collides with mandatory reporting — Critical / Possible — deferred out of phase 0
Israeli law imposes a reporting duty on anyone responsible for a minor, and plausibly a professional duty on staff of an institution where a minor is present. A swim school holding a child under contract is plausibly “responsible for the minor”.
Guardian-visible-by-default is the right safeguarding posture until the disclosure concerns the guardian. Then it is exactly wrong.
Phase 0 status: there is no messaging for managed members in phase 0 at all (Q-05, resolved), so there is no thread to get wrong. conversations.subjectUserId moves to a later phase. Notification routing to guardians stays — that is fan-out, not messaging, and it carries no disclosure channel. The risk is deferred, not dissolved.
Mitigation, for the phase that introduces messaging: (a) never promise confidentiality — UI copy must say communications may be disclosed where law requires; (b) a staff-only safeguarding escalation path from any channel; (c) retention must not auto-purge below the limitation period; (d) do not build a private coach↔minor DM. A managed member has no login, so this is structurally impossible — a genuine safety property we should state deliberately rather than enjoy by accident. It becomes live the moment a teen claims a login. Counsel’s answer on the escalation path is a precondition for that phase.
R-07 — showOnMinisite defaults to true on every membership row — Low / Latent
Revision 1 rated this High / Certain and said children would land on a public marketing page. That was overstated, and it is wrong: there is no PII leak today.
What is true: memberships.showOnMinisite is NOT NULL DEFAULT true (libs/db/src/lib/schema/memberships.ts:42). What revision 1 missed: the minisite only reads it for coaches. The public query filters eq(memberships.role, 'coach') and exposes only firstName/lastName (apps/api/src/organizations/minisites.service.ts:699-708), and the toggle endpoint accepts entity === 'coach' only (:1137-1143). A member — adult or child — is never exposed.
The residual concern is latent: the flag defaults true on every membership row, and exactly one role filter stands between it and exposure. A future “our members” section built on the same flag would publish everyone by default, children included.
Mitigation: managed members default to false anyway, as defence in depth (TKN-405 (WP-10)), and any future non-coach minisite section must treat the flag as opt-in rather than inherit its default.
R-08 — Progress photos of minors — Critical / Possible → hard-blocked
progress_photos keys on membershipId and holds body photographs. For children this is the highest-severity surface in the feature. Israeli privacy law makes photographing in the private domain, and using a likeness for profit, distinct infringements with statutory damages up to ~NIS 67,000 without proof of damage, doubled if intentional. Poolside and changing-room contexts sit near the most sensitive provisions.
Decision: progress photos are hard-blocked for managed members. No org toggle, no consent path, no staff override. The endpoint returns 403 for any managed member, whoever calls it, and a test asserts exactly that (TKN-405 (WP-10)). The upside is negligible and the downside is a front-page story.
Why not a setting, even a default-off one: a setting which can be switched on will be switched on by someone who does not carry the consequence. The operator admin who flips it is not the one sued, and not the child. The dual-key consent pattern (P14) remains the reference if this is ever reopened, but reopening it is a product decision with counsel in the room, not a toggle.
R-09 — participation = 'guardian_only' must be filtered everywhere — Medium / Likely
Guardians who do not train hold membership rows. Every roster count, MRR calculation, capacity report, churn metric and platform-billing seat count must filter participation = 'trains'. Missing one silently corrupts business metrics and possibly what we charge the operator.
Mitigation: grep-and-test sweep as a named QA item (WP-10); a test asserting that adding a guardian_only membership changes no analytics figure.
R-10 — Registration waves multiply under guardian booking — Medium / Likely
Israeli kids’ classes open registration in synchronised weekly waves. Boostapp has public reviews of load failure at exactly this moment, repeating weekly. A guardian with three children now issues three bookings in the same second, and our booking path is SERIALIZABLE with retry.
Mitigation: load-test the multi-child path specifically (TKN-413 (WP-18)). Consider a household-scoped queue or a single multi-member booking endpoint. Do not let three children mean three serialisable retry storms against one session row.
R-11 — Withdrawn in revision 2
Was: member semantic search would not find children. A managed member is a users row, so users.nameEmbedding and its HNSW index find them untouched.
R-12 — Credits survive the guardian who paid for them — High / Likely
Entitlement attaches to the child (per the ticket). A guardian who bought a 10-session pack and is then revoked loses what they paid for. Correct for the child, commercially explosive for the operator.
Decided (Q-08): entitlement stays with the child, the operator gets a refund/transfer tool, and the revocation confirmation screen states plainly what happens to what the guardian paid for. Silence here produces a chargeback.
R-13 — Import may mis-link a child to a stranger — High / Possible
The importer must resolve a guardian from incumbent data. Surname matching would link two unrelated families sharing a common Israeli surname. Mitigation: match only on exact email or phone; never on name similarity, and never on a null or empty email (R-18). Unresolvable rows go to a staff reconciliation queue (TKN-406 (WP-11)), never auto-linked and never dropped. The current behavior — silently dropping emailless rows (import.service.ts:629-630) — is at least safe; a bad guess is worse.
R-14 — attachments has no organizationId — Low / Certain (pre-existing)
Org scoping is inherited transitively through a polymorphic ownerId. Revision 2 adds no owner type — a managed member is a users row and uses the existing ones — but children’s documents now flow through this weak pattern. Not introduced by this ticket and not fixed here; backfilling org across four owner types is its own change. Recorded so it is not forgotten.
R-15 — Clerk user.deleted can orphan a child — Medium / Possible
The webhook soft-deletes the users row and never rethrows (Clerk retries non-2xx). If that user was the sole active primary guardian, the child is orphaned silently. Mitigation: the webhook must detect the orphan condition and raise a staff queue item. An orphaned managed member is an incident state (behavior.md §7.4), never a valid one.
R-16 — Adoption re-keys the national ID and must sever the trail — Low / Rare, Critical if mishandled
Adoption by court order reissues both name and ת.ז., deliberately severing the link to the biological parents. This directly contradicts our otherwise-correct rule that guardianships are never hard-deleted.
Mitigation: a narrow, platform-admin-only identity re-key path that purges prior guardian history for that managed member and leaves no discoverable trail — including in audit_events, which is otherwise append-only. Rare, but getting it wrong discloses an adoption to a biological parent. Needs legal input. Q-13.
R-17 — Migration parity is asserted, not proven, unless the test is real — High / Possible
“Existing adult memberships unaffected” is easy to claim and hard to verify; a smoke test will pass while a subtle field changes shape.
Much cheaper in revision 2. No existing row is re-keyed and no existing column changes meaning. The relaxation is users.email becoming nullable; the only existing rows it touches are empty-string emails, which the migration sets to NULL. The parity proof now guards a narrow surface rather than a re-keying of memberships and member_profiles.
Mitigation (planned): an adult-migration-parity.int.spec.ts snapshotting every member-facing API response for a fully-populated adult member before and after the migration. Phase 1 did not write it; the migration was gated instead by the clean strict-null type-check (R-18) and every existing suite passing on the migrated schema (data-model.md §5).
2. Blindspots — not in the original framing
Things the goal statement did not mention that the model must survive.
B-01 — The physical handover is the highest-risk moment, and it is a legal one
Israeli pool-safety regulations model a supervision handoff: responsibility sits with the escorting adult, transfers to the swim teacher for the lesson, and returns after — except while the child is learning to swim under a teacher’s supervision. Swim England’s Wavepower codifies the non-collection ladder in detail: call the emergency numbers, ask the child for other contacts, after 20 minutes contact police or children’s social care, never leave a child alone unless over 16 and with prior guardian agreement, two adults remain until collection, refuse release to an intoxicated collector.
TKN-388 owns this. TKN-382’s obligation is not to foreclose it: canAuthorizeEscorts exists, and escort authority is deliberately not the same thing as guardianship. Worth noting there is no Israeli statute governing collection from a private leisure facility — which means the platform’s authorised-collector register is the control, not a record of one.
And nobody in the category has built it. iClassPro, Jackrabbit, The Studio Director, DanceStudio-Pro, Kicksite and SwimTopia are all verified absent — no pickup roster, no check-out step, no signature at collection (research.md §2a). iClassPro’s own documentation gap here was assessed as “the single biggest child-safety gap in its documented model”. SafeSplash solves it by policy instead of software: parents must remain on site for the whole lesson.
So TKN-388 is not catch-up work — it is an unoccupied position in a category where the legal anchoring already exists and no vendor has responded to it. Worth saying out loud when sequencing it.
B-02 — Staff must be police-cleared before being assigned to a session with minors
An employer may not engage an adult for work at a gym or sports club where minors train without police confirmation, and employment may not continue past 30 days without it; the certificate must be less than a year old at hiring.
So staff qualification and clearance are bookable constraints, not HR attributes. The same applies to the requirement that a qualified minors-training instructor be present whenever minors train. This lands on TKN-313 and on scheduling, but only TKN-382 knows that a session contains minors. Publishing a minors session without a cleared, qualified instructor should be refusable.
B-03 — Gamification aimed at children is unlawful in Israel, not merely tasteless
Marketing to minors may not exploit impressionability or trust, may not encourage children to pester their parents to buy, and may not make them feel deprived if they do not. Combined with the under-18 marketing consent rule, this constrains streak mechanics, scarcity copy, social comparison and any Spotter-generated upsell visible to a child. Taikan’s brand reserves mint for “PR, streak and today” — that visual language must not become a persuasion mechanic in a child’s view.
B-04 — The operator’s own waiver is written in the wrong voice, and that is the demo
עולם המים’s children’s course terms are in the first person of an adult customer — «הלקוח מצהיר כי מצבו הבריאותי מאפשר השתתפות» — with no guardian clause and photo consent handled by verbally asking the photographer. This is simultaneously the clearest thing TKN-382 fixes for them and a live compliance exposure of theirs. It is the most persuasive single artifact for the next conversation.
B-05 — Two health artefacts with different statutory cadences, not one
A gym health declaration (2-year validity, 1-year where a medical certificate was triggered, 2-year retention) and a Sports Law seasonal clearance for competing athletes (questionnaire every season, physical every year) are different objects with different clocks. Both attach to the child. member_profiles.medicalCert is a boolean and cannot express either.
B-06 — The payer needs a membership row even if they never enter the building
Receipts, invoices, dunning and debt collection target the payer. A grandparent who pays but never trains and never books still needs to exist in the org as a billable, contactable, auditable party. participation = 'guardian_only' covers it — but the tax-invoice name is a separate field from the guardian’s display name, and Arbox ships exactly that (“the invoice-name field allows issuing documents in the parent’s name or another payer’s name”). Small, real, and an accountant will ask. Now in the model: subscriptions.invoiceName (Q-15, data-model.md §3.6).
Added from research: SafeSplash requires a non-parent payer to execute a third-party payer agreement, and — the part we had not considered — liability falls back to the original enrolling customer if the third party defaults. So a payer who is not a guardian needs (a) their own accepted agreement and (b) a recorded fallback-liable party. Without the fallback, a grandparent’s failed card leaves the operator with a debt and nobody to pursue. Now in the model: member_payment_methods.fallbackLiableUserId (data-model.md §3.6).
B-07 — Multi-child check-in at the desk
A guardian arriving with two children checks in both. The kiosk and self-check-in flows are single-member today. Two children means two queue positions at a front desk that is already the bottleneck at class changeover.
B-08 — Sibling capacity contention has no good answer, only a legible one
Two children, one remaining seat. Waitlist position is per member, so a household gets no block reservation. That is the right call — otherwise families crowd out individuals — but it will read as a bug unless the UI explains it at the moment of collision. The same per-member rule governs quotas: siblings do not consume each other’s allowance, and a pooled family quota is deferred.
B-09 — A child ages out of a recurring enrollment mid-term
Age-band eligibility is TKN-383, but birthDate becomes load-bearing here. A birthday must not silently terminate a recurring enrollment or a paid month.
B-10 — Israel’s identity system makes this easier than elsewhere, and the design should exploit it
Every Israeli resident receives an immutable ת.ז. at birth registration; the physical document comes at 16. A child is therefore uniquely and stably identifiable with no email, no phone and no login — and both major incumbents already collect it. Using ת.ז. as the natural-person key for children, while reserving email and phone strictly for account holders, structurally prevents the shared-phone collision that produced Fizikal’s account-takeover breach.
B-11 — The AI layer should probably not touch children’s records at all in phase 0
The Israeli PPA’s AI guideline names information about minors as a high-risk factor, expects a privacy impact assessment, and requires bot disclosure. AI Act Art. 5(1)(b) prohibits exploiting age-based vulnerability to distort behaviour. TKN-384 and TKN-390 are both agent features aimed at exactly this journey. Recommendation: exclude managed members from AI training and inference-heavy features by default until a PIA exists. This is a constraint on adjacent tickets that only this one can see.
The constraint still stands in revision 2, and it is now opt-out rather than structural. Revision 1 kept children in a separate table, so an AI feature reading users excluded them by accident. A managed member is now a users row, so any feature that reads users or memberships includes children unless it filters kind = 'managed' explicitly.
B-12 — Tier gating shapes the commercial story
PRODUCT.md states platform tiers gate features. It is the feature that unlocks an entire vertical (swim schools, gymnastics, martial arts kids’ programs), which argues for making it broadly available; it is also substantial work, which argues for Pro. Decided: Pro (Q-14).
B-13 — Erasure at majority must not cascade into the guardian’s records
At 18 the former child can erase data given on their behalf as a minor, including data a guardian supplied. Our schema links their history to guardianship rows, payment records and — once messaging ships — shared conversations. Erasure must be surgical, must respect the statutory two-year health-declaration retention it conflicts with, and must not delete the guardian’s own billing history.
B-14 — Retention rules conflict across jurisdictions and cannot be a constant
COPPA forbids indefinite retention of children’s data and requires a published retention policy. Israeli law mandates two-year retention of health declarations. These are directly opposed. Retention must be per-tenant policy from the start; retrofitting it later means a migration across every child record.
3. Open questions
Revision 2 resolves eleven of the fifteen. Still open: Q-03, Q-11, Q-12, Q-13. Q-03 changes the access resolver’s core predicate and must be answered before WP-03 ships; the other three do not block phase 0 schema work.
| # | Question | Who decides | Why it blocks | Recommendation / decision |
|---|---|---|---|---|
| Q-01 | What do we call a guardian in Hebrew? אפוטרופוס is legally correct but clinical; הורה is warm but wrong for a grandparent or legal guardian. | You + the operator | Names leak into 40+ i18n keys, the attestation text and the legal forms. Renaming later is a migration of copy and of signed artefacts. | RESOLVED — אחראי in member-facing copy (English UI: “responsible adult”; code: guardian). אפוטרופוס retained in legal documents and the staff record, where the statutory term is right. Rationale in data-model.md §2. |
| Q-02 | Does the pilot need split billing across two households? | You + the operator | It is the one billing shape we excluded, and it is genuinely hard: partial payment failure, two dunning tracks against one entitlement, who sees whose card. Retrofitting it touches every payment path. | RESOLVED — excluded from phase 0. The implementation shape is known for when it returns: DanceStudio-Pro ships it as a percentage split of Auto-Pay across the main payer’s card and a contact’s card, explicitly for divorced parents (research.md §2a). Their documented limits are our opening — the second card is studio-entered rather than self-service, payment plans cannot use it, and failed-split behaviour is unanswered in their own support thread. If the operator reports separate-paying divorced parents at material volume in their 8–12 centres, it is its own ticket sized as “percentage split on payerUserId + partial-failure dunning”, not a stretch goal. |
| Q-03 | What happens to a guardian’s access to history after revocation? | You + counsel | Affects the access service’s core predicate. | Open. Recommendation: ex-guardian retains their own payment receipts (they need them for tax) and loses all of the managed member’s data from the revocation timestamp. Never retroactive erasure of what they legitimately saw. |
| Q-04 | Is selfAccessMinAge 14, 16 or 18? | You | Determines whether the claim flow is a phase-0 surface or dormant. Amilia ships 14 with its own wallet; Swim England uses 16 for self-release; Israeli law knows only 18. | RESOLVED — no automatic age-triggered access changes. selfAccessMinAge is an org setting, default off. Claiming a login is guardian-initiated, with guardian veto. No majority cron, no automatic capability downgrade on claim, no automatic grant expiry at 18. |
| Q-05 | Can a coach and a minor ever communicate privately, and what happens when a child discloses harm? | Counsel, first | Mandatory-reporting duties may make guardian-visible messaging wrong in the one case that matters most. | RESOLVED for phase 0 — no messaging for managed members at all. Notification routing to guardians stays (fan-out, not messaging); conversations.subjectUserId moves to a later phase. This removes the counsel dependency from phase 0. Counsel’s answer on the escalation path (R-06) remains a precondition for whichever phase introduces messaging. |
| Q-06 | Is guardianship global or per-organization by default? | You | Affects the unique indexes and the TKN-386 network story. | RESOLVED — global, with an optional org-scoped grant (guardianships.organizationId nullable). A parent is a parent at every branch. |
| Q-07 | Does a second guardian’s staff-approval request require notifying the primary, and can the primary veto? | You + operator | It is the abuse-vector control. | RESOLVED — agreed. Notify always; primary can object (objectionLodged), staff decide. Staff are the arbiter because they hold the paperwork. |
| Q-08 | Who owns credits bought by a guardian who is later revoked? | You + operator | Commercial exposure; chargeback risk. | RESOLVED — agreed. Entitlement stays with the child; operator gets a transfer/refund tool; revocation screen says so plainly. |
| Q-09 | Do we support a member who is both managed and a guardian — a 17-year-old who brings a younger sibling? | You | Affects the “guardian must hold a login” invariant. | RESOLVED — minimum guardian age is 21, not 18, enforced on the grant. A 17-year-old sibling cannot be a guardian with or without a login; a managed member can never be one, because guardianUserId must have kind = 'account'. Escort nomination is a separate authority and belongs to TKN-388. |
| Q-10 | Must guardian identity be verified for the first guardian if the org enables gate hardware or escort release? | You + operator | Self-assertion is fine for booking a lesson and thin for releasing a child. EDPB notes self-declaration “has little robustness”. | RESOLVED — agreed. Self-assertion for phase 0. Revisit with TKN-388, where the stakes change. |
| Q-11 | Are foster carers and temporary custodians eligible as guardians? | You + counsel | Loma Linda excludes temporary custody from proxy access entirely. Israeli gym law defines “parent” to include anyone with lawful custody. | Open. Recommendation: the gym-law definition is broader and governs here. relationship = 'legal_guardian' with an evidence reference. Confirm with counsel. |
| Q-12 | Do we isolate health data behind its own access boundary? | You | Drives Israel’s security-level classification and the fine multiplier for the whole platform, not just this feature. | Open. Recommendation: worth doing, and cheaper now than later. Needs a real estimate before committing. |
| Q-13 | How do we handle the adoption identity re-key? | Counsel | Conflicts with append-only audit and never-delete-guardianship. | Open. Recommendation: platform-admin-only path, purges prior guardian history for that managed member, no discoverable trail. Rare; get it right once. |
| Q-14 | Which platform tier gates managed members? | You | Changes rollout and the commercial pitch. | RESOLVED — Pro. It is substantial work and it unlocks a vertical; Lite operators are single-location adult gyms. |
| Q-15 | Do we need the tax-invoice-name field (B-06)? | You + operator | An accountant will ask; Arbox already ships it. | RESOLVED — yes. subscriptions.invoiceName, one nullable column (data-model.md §3.6). |
Questions that genuinely need a lawyer, not a decision
None of these blocks phase 0 any more. Item 1 was the one that did, through Q-05; taking messaging out of phase 0 removed the dependency. Each remains a precondition for the later work named beside it.
- Is a swim school “responsible for a minor” for mandatory-reporting purposes, and are its coaches within the professional duty? (Q-05 — before messaging for managed members ships)
- Is Taikan a controller or a processor for children’s health declarations, and does Taikan itself cross the Israeli DPO threshold?
- What lawful basis covers health declarations in the EU, given that a gym is not a health professional bound by professional secrecy and so Art. 9(2)(h) appears unavailable?
- Is self-asserted guardianship sufficient under Israeli law for signing a liability waiver on a minor’s behalf, and what does the operator’s insurer require?
- Does the adoption re-key obligation extend to purging append-only audit records? (Q-13)
- For a US expansion, is Taikan a COPPA “operator” or a service provider to the gym as operator, and does the payment-notification consent method bind to the gym’s transaction or ours?