Managed Members — Execution
Ticket: TKN-382 Work packages: TKN-396 … TKN-414, sub-issues of TKN-382. Status: phase 1 shipped WP-00 … WP-14 and WP-10b (PR #526, taikan-mobile #128). WP-15 … WP-19 are deferred. The waves below are the plan as executed; they are kept as history.
Linear is the source of truth for work-package scope and acceptance. Each ticket carries its own full brief. This document holds only what is true across all of them: the wave order, the concurrency rules, and the constraints every package obeys. It deliberately does not restate the tickets — two copies of a work package drift within a week.
Ground rules for every package
Repeated in each ticket because these are the things that get dropped first.
- Do not run
pnpm build,pnpm lint, or any test suite mid-flight. Run verification once, at the end of your package. - Never run
pnpm db:migrate. Generate migrations, review the SQL, hand them over. Execution needs explicit owner approval. Neverdb:push. - Every user-facing string goes in
libs/shared/src/lib/i18n/dictionaries/{en,he,ru}.jsonbefore it is referenced. No hardcoded English, including in error messages you author.en.jsonis the typed canonical;he/ruare deep-merged over it, so a missing Hebrew key ships as visible English rather than failing CI — it is a shipped defect, not a caught one. - Tests use
data-testidonly. NogetByText, noqueryByText, no CSS or structural selectors. Every web test goes through a co-located*.driver.tsx; specs never importscreen.vi.mock()lives in spec files, never drivers. - Behavioural changes sit behind the PostHog flag
managed-members, followingapps/api/src/payments/services/webhook-enforcement.service.ts:30,51-55exactly: a staticFLAG_KEY,distinctId = orgId,groups: { organization: orgId }, and only=== trueproceeds.isFeatureEnabledreturnsundefinedwhen it cannot decide; treat that as off. - No
console.log/console.error/console.warnin committed code. - Do not create git commits. Stage with
git addand report that the work is ready for review. - Follow existing conventions rather than establishing a second one. Controllers:
@ApiTags+@ApiBearerAuth,@Controller('organizations/:orgId/...'), param order(params…, @CurrentUser('userId') clerkId, @Body dto), always return{ data: … }. DTOs:class-validatorat the HTTP boundary plus a Zod schema re-validated in the service. Copyapps/api/src/check-ins/check-ins.controller.tsanddto/assign-check-in.dto.ts. - Do not reach into a sibling service’s private properties.
apps/api/src/bookings/bookings.controller.ts:82doesthis.bookingsService['membershipsService']— a bug to avoid, not a pattern to copy. Inject properly. - Read
data-model.mdbefore writing schema andbehavior.mdbefore writing an endpoint.data-model.md§2 records why theparticipantsdesign was rejected; do not re-propose it.
Waves
| Wave | Packages | Concurrency |
|---|---|---|
| 0 — Decisions | — | Complete. All phase-0 blockers resolved in review; no counsel dependency remains for phase 0. |
| 1 — Schema | TKN-396 (WP-01) | Serial. |
| 2 — The gate | TKN-397 (WP-02) | Runs alone. Nothing else may touch the API while the null sweep is open. |
| 3 — Authorization | TKN-398 (WP-03) → TKN-399 (WP-04) | Serial; WP-04 depends on the resolver. |
| 4 — Domain | TKN-400 (WP-05), TKN-401 (WP-06), TKN-402 (WP-07), TKN-403 (WP-08), TKN-404 (WP-09), TKN-405 (WP-10), TKN-415 (WP-10b), TKN-406 (WP-11) | Eight concurrent. WP-10 and WP-10b depend only on WP-01/WP-02; WP-11 depends on WP-05. |
| 5 — Web | TKN-407 (WP-12), TKN-408 (WP-13), TKN-409 (WP-14), TKN-410 (WP-15), TKN-411 (WP-16) | Five concurrent. WP-13 needs WP-07 + WP-08; WP-14 needs WP-06 + WP-12; WP-15 needs WP-04 + WP-05. |
| 6 — Hardening | TKN-412 (WP-17) → TKN-413 (WP-18) → TKN-414 (WP-19) | Serial. WP-17 is a convergence point by construction. |
Two concurrency rules that are not negotiable
TKN-397 runs alone. It works a compiler error list across the whole API. A concurrent package adding code will add errors to that list, and the two agents will fight over the same files.
TKN-412 (i18n) has exactly one owner. Five web packages all add keys to en.json. Converging them into one package is the only way three agents do not overwrite each other’s dictionary edits.
The gate
Nothing past wave 2 starts until TKN-397 satisfies both:
npx nx build apiclean, with zero null-assertion escapes added. Not “the errors are gone” — gone without!or?? ''.adult-migration-parity.int.spec.tspassing: a fully-populated adult member’s every member-facing API response is byte-identical before and after the migration. Not written in phase 1: the gate was held by item 1 plus every existing suite passing on the migrated schema (data-model.md§5).
That is acceptance criterion A9, and it is the only thing standing between this feature and a silent regression for every existing paying member.
Sub-agent dispatch
The Linear ticket body is the task brief. Dispatch shape:
context:
# Goal
TKN-382 managed members, Taikan monorepo at /Users/saarku/dev/taikan.
Read docs/features/managed-members/{data-model,behavior,design-spec}.md first.
data-model.md is normative for schema; behavior.md for behaviour; design-spec.md for UI.
# Constraints
<paste "Ground rules for every package" from execution.md verbatim>
# Contract
<the exported interfaces this package provides or consumes — e.g. GuardianCapabilities
from libs/shared, GuardianAccessService.can()'s signature>
task:
<the Linear ticket body verbatim, plus: report every path:line you changed>Two dispatch rules. One integration owner per file — if two packages would touch the same file they belong in different waves or the same package; there is no third option. And siblings coordinate through the parent via write agent://<id>, never by editing each other’s files.
Deferred, with the trigger that would revive each
Recorded so they are not silently forgotten or silently re-added.
| Deferred | Trigger |
|---|---|
| Messaging for managed members | A phase-1 decision plus counsel on the safeguarding escalation path. Guardian-visible is the right default; the carve-out is that guardian visibility is wrong when the disclosure concerns the guardian. |
| Age tiering and the claim flow | Explicit product decision. No automatic age-triggered behaviour ships; selfAccessMinAge defaults to off. |
| Split billing across two households | Material volume of separately-paying divorced parents at the pilot. Shape is known — DanceStudio-Pro’s percentage-split autopay — and the unsolved part is partial-failure dunning against two payers. |
| Consolidated family invoice | Operator demand. Would make a household a real aggregate with real invariants, which is when a households table earns its place. |
| Pooled family quota across siblings | A family plan product. Boostapp’s pooled-versus-per-head switch is the reference. |
| Structured medical facts and a printable roster summary | Its own ticket. canViewHealth already exists so the permission is ready when the data is. |
attachments.organizationId | Pre-existing scoping weakness, not introduced here. Needs an org backfill across four polymorphic owner types. |