Appearance
0022 — Family Portal ​
Status: Accepted (2026-06-18 — owner accepted during ADR review)
Date: 2026-06-18
ADO work item: AB#3086
Deciders: Kristopher Turner (platform owner)
Context ​
ADR 0007 locks the account and family-group identity model: every approved member belongs to exactly one FamilyGroup; the group may include the primary member, a spouse, and one or more parent-managed child sub-accounts. ADR 0008 places the member and family domain inside the platform core — it is owned by the platform and consumed by every feature, not a feature unto itself.
What neither ADR records is how a member or parent administers their own family from a UI perspective — which operations are exposed, how they are gated, and how they map to the approval workflow and credential management primitives defined in ADR 0007.
Three concerns motivate this decision:
Self-service within guardrails. Ministers cannot be the operational path for every family-management action. Parents must be able to add a child, reset a PIN, and adjust a child's allowed scope without raising a support request. At the same time, high-trust changes (adding a spouse) still require minister approval (ADR 0007).
Child credential management. Child accounts use a parent-managed username
- PIN/password hashed with Argon2id and stored in the platform
Userstable — never in Clerk (ADR 0003). The platform must provide a first-party UI for parents to create, update, and reset those credentials.
- PIN/password hashed with Argon2id and stored in the platform
Hard authorization boundary. A parent may only ever see and modify their own
FamilyGroup. The API must enforce this server-side on every request; the UI is a thin surface that presents only what the API returns for the authenticated member'sfamily_group_id. This portal is explicitly not the Admin & Ministry Portal (ADR 0021 — leadership-wide administration of all members and families); it is scoped to one family, one member.
This ADR records the surface design for the Family Portal: the capabilities it exposes, the authorization model it relies on, and the boundaries it observes. It introduces no schema changes (those are Phase 2, ADR 0007) and no code.
Decision ​
We will deliver a Family Portal as a platform-owned surface (web shell + mobile) that gives an authenticated Member or Spouse self-service access to their own
FamilyGrouponly. All family-management operations route through the typedapi-clientSDK to server-side API endpoints that enforce thefamily_group_idscope on every request. Child credential operations (create, reset PIN/password) are handled entirely within the platform — no child data is passed to Clerk or any external provider. Spouse addition and child addition are subject to the minister approval workflow defined in ADR 0007; credential reset and scope adjustment are parent-autonomous.
Portal capabilities ​
1 — Family group overview ​
The member sees their own FamilyGroup record: group name, primary member, and a list of all members (FamilyGroupMembers join table). No information about any other family is returned by the API. A Member or Spouse role may view and manage the group; the CHILD role has no access to the Family Portal.
2 — Manage own member profile ​
The authenticated user edits their own Users record: display name, contact preferences, and any profile fields defined by the platform. The API scopes the write to user_id = authenticated user; no cross-user profile writes are possible through this surface.
3 — Add spouse (approval-gated, invite-driven) ​
The primary Member submits a spouse-addition request by entering the spouse's first name, last name, and email address. The API:
- Creates a
Userrecord for the spouse withstatus = PENDING_APPROVALandaccountType = SPOUSE. TheexternalUserIdfield is left null — the spouse has not authenticated yet. - Creates an
ApprovalWorkflowrecord (workflow_type = SPOUSE_ADD) linking the pending user to the primary member's family group. - Sends a Clerk invitation email to the spouse's address so they receive a "Join Heritage Virginia" link. This step is best-effort: if the Clerk invite call fails, the workflow and pending user still exist and the minister can proceed manually.
When the spouse clicks the invite link, they sign in via Clerk (Apple or Google social login). On the first API call after sign-in (POST /auth/session), the platform resolves the Clerk session: it looks up any PENDING_APPROVAL / SPOUSE user with a matching email and no externalUserId, then links the Clerk subject to that existing record instead of creating a duplicate. The spouse's account remains PENDING_APPROVAL until a minister approves the workflow in the Admin & Ministry Portal (ADR 0021).
On approval:
- The spouse's
status→ACTIVE,role→MEMBER - A
FamilyGroupMemberrow (relationship = SPOUSE) links them to the family User.familyGroupIdis set atomically in the same transaction
The Family Portal shows the pending state until the minister resolves the workflow. Phone number is optional at submission time; it can be added by the spouse from their profile after activation.
4 — Add child sub-account (parent-autonomous after vetting) ​
The Member or Spouse submits a child-addition form. The parent provides the child's first name, last name, a chosen username (the UI pre-fills this as first.last, still editable), and an initial PIN/password. The display name is derived from first and last name server-side if not supplied. The API validates the payload (username uniqueness, PIN/password strength), hashes the credential with Argon2id, creates the Users record (credential_type = parent-managed, parent_user_id = authenticated user, email = NULL), and links it to the FamilyGroup via FamilyGroupMembers (relationship = child). Per ADR 0007, child addition by an already-approved parent is auto-approved — no minister queue entry is created. The child account is active immediately.
5 — Reset a child's PIN/password ​
The parent selects a child from their family group and submits a new PIN/password. The API verifies that:
- the authenticated user is the
parent_user_idof the target child, and - the target child belongs to the authenticated user's
family_group_id.
On success, the API hashes the new credential with Argon2id and writes it to Users. No credential value is logged or returned. The child's active session tokens (if any) are invalidated. This operation requires no minister approval.
6 — Set or adjust a child's allowed scope ​
The parent configures which platform sections a child may access — for example, which homeschool courses or modules the child is enrolled in (ADR 0015). The API stores the child's permitted scope and enforces it on every child-authenticated request server-side. The parent UI presents the available sections and current child access; changes take effect immediately without a minister queue step.
Authorization model ​
All Family Portal API endpoints enforce the following rules server-side. The api-client SDK carries the authenticated user's session token; the API resolves family_group_id from the server-side session on every request.
| Rule | Enforcement point |
|---|---|
A parent can read/write only their own FamilyGroup | family_group_id from session; SQL WHERE family_group_id = ? |
A parent can write child credentials only for children where parent_user_id = self | API checks parent_user_id before any credential write |
Child role (credential_type = parent-managed) has no access to Family Portal endpoints | RBAC guard on portal route group (ADR 0006) |
Spouse-add requires a pending ApprovalWorkflow; no direct account creation | Workflow row must exist before any spouse Users insert |
| Child credential values are never logged or returned to the client | API discards credential after hashing; response contains only success/error |
The client-side UI may use role claims to hide controls, but the API is the sole authority on every protected action (ADR 0006, ADR 0008 composition rule).
Surface delivery ​
The Family Portal is delivered on both web and mobile via the platform API. Web mounts inside the core web shell (apps/web) as an auth-gated route group. Mobile surfaces the same capabilities through apps/mobile (React Native/Expo, ADR 0002) using the same api-client calls. Both surfaces share the same shared-types contract; no feature-specific logic lives in the client.
Distinction from Admin & Ministry Portal (ADR 0021) ​
| Concern | Family Portal (this ADR) | Admin & Ministry Portal (ADR 0021) |
|---|---|---|
| Scope | Own family group only | All members and all family groups |
| Who can use it | Member, Spouse (their own family) | Minister, Admin roles |
| Spouse-add | Initiates the approval request | Reviews and approves/rejects |
| Child-add | Parent submits; auto-approved | Visible in audit log; no required action |
| Child credential reset | Parent-autonomous | Not surfaced (parent owns child creds) |
| Member directory | Not accessible | Full directory access per RBAC |
Alternatives considered ​
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Parent-facing Family Portal, server-side scoped (chosen) | Self-service within guardrails; clear authorization boundary; API-first consistent with ADR 0008 | Portal must be built as part of platform scope before feature Epics | — chosen |
| Ministers manage all family changes manually | Keeps family data in one admin surface; no parent-facing UI needed | Does not scale; parents cannot self-serve credential resets or scope changes; minister becomes a bottleneck for routine actions | Rejected — violates self-service within guardrails principle |
| Children self-manage their own credentials | Reduces parent involvement | Violates parent-managed model (ADR 0007); children have no email/social login; COPPA requires verifiable parental consent on account creation and changes | Rejected — hard constraint from ADR 0007 and COPPA requirement |
| Store child credentials in Clerk | Unified auth layer; single token path | Clerk does not support parent-managed, no-email child credentials; passes minor PII to an external provider; breaks the COPPA boundary | Rejected — ADR 0003 explicitly excludes Clerk from child accounts |
| Expose family management only through the Admin & Ministry Portal | Fewer surfaces to build; one admin UI | Parents would need a minister or admin to make routine changes; poor UX; contradicts the "family-not-just-a-church" model | Rejected — conflates administration of all families with self-service management of one's own family |
Consequences ​
Positive ​
- Parents have first-party self-service for the operations they own — adding a child, resetting a PIN, adjusting a child's scope — without depending on minister availability.
- The authorization boundary (
family_group_idscoped server-side) provides a clean, auditable guarantee that no parent can access another family's data. - Child credentials are wholly platform-owned: no minor PII touches Clerk or any external provider, keeping the COPPA consent chain clean and contained.
- The Argon2id credential path for child accounts is reused for any future parent-managed credential type; no new hashing infrastructure is introduced.
- Spouse-add flowing through the existing
ApprovalWorkflowtable means the minister's review queue is the single entry point for all high-trust membership changes, consistent with ADR 0007.
Negative / trade-offs ​
- The Family Portal is platform scope, not a later feature Epic — it must ship as part of the platform baseline before members can manage their own families. This adds to the Phase 1 build surface.
- Two approval-gated operations (spouse-add) produce a "pending" state visible in the Family Portal but resolved in the Admin & Ministry Portal. The UX must communicate that state clearly, or parents will re-submit.
- Parent-autonomous child credential resets introduce a recovery path the API must handle carefully — invalidating existing child sessions and preventing token reuse after a reset.
Risks ​
- Over-broad parent writes. A bug that fails to resolve
family_group_idfrom session — or accepts it from the client — could let one parent overwrite another family's data. Mitigation: all family-scoped queries derivefamily_group_idfrom the server-side session token; the value is never accepted as a client request parameter. - Child session invalidation on PIN reset. If active child sessions are not revoked when a parent resets the PIN, a child could continue using the old credential. Mitigation: the reset endpoint invalidates all active tokens for the child
user_idbefore writing the new hash. - Stale "pending" UX for spouse-add. If the minister takes time to review, the parent sees an unresolved pending state indefinitely. Mitigation: the
ApprovalWorkflowrecord should carry arequested_attimestamp; the portal UI displays elapsed time and the platform sends a notification when the decision is made (ADR 0013). - Orphaned child accounts on parent removal. If the parent member account is deactivated or removed, child sub-accounts must be deactivated in the same operation. Mitigation: cascade deactivation logic in the platform API (noted as a required action in ADR 0007).
References ​
- Platform strategy — Account & Family-Group model
- Platform strategy — Platform scope
- ADR 0003 — Authentication: Clerk + social login — adult auth; Clerk excluded from child accounts
- ADR 0006 — Two-plane RBAC — five canonical roles; CHILD role excluded from Family Portal
- ADR 0007 — Account & Family-Group identity + approval workflow — single
Userstable; nullable email;FamilyGroups; parent-managed credentials; Argon2id; spouse-add and child-add workflow rules - ADR 0008 — Platform composition — member & family domain is platform-owned; all surfaces consume via
api-client; no self-authorization on clients - ADR 0021 — Admin & Ministry Portal — leadership-wide admin surface; distinct from this single-family portal
- ADO: Story AB#3086 (this ADR + Family Portal design), Epic AB#3074 (Build Platform)