Skip to content

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:

  1. 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).

  2. Child credential management. Child accounts use a parent-managed username

    • PIN/password hashed with Argon2id and stored in the platform Users table — never in Clerk (ADR 0003). The platform must provide a first-party UI for parents to create, update, and reset those credentials.
  3. 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's family_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 FamilyGroup only. All family-management operations route through the typed api-client SDK to server-side API endpoints that enforce the family_group_id scope 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:

  1. Creates a User record for the spouse with status = PENDING_APPROVAL and accountType = SPOUSE. The externalUserId field is left null — the spouse has not authenticated yet.
  2. Creates an ApprovalWorkflow record (workflow_type = SPOUSE_ADD) linking the pending user to the primary member's family group.
  3. 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 statusACTIVE, roleMEMBER
  • A FamilyGroupMember row (relationship = SPOUSE) links them to the family
  • User.familyGroupId is 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_id of 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.

RuleEnforcement point
A parent can read/write only their own FamilyGroupfamily_group_id from session; SQL WHERE family_group_id = ?
A parent can write child credentials only for children where parent_user_id = selfAPI checks parent_user_id before any credential write
Child role (credential_type = parent-managed) has no access to Family Portal endpointsRBAC guard on portal route group (ADR 0006)
Spouse-add requires a pending ApprovalWorkflow; no direct account creationWorkflow row must exist before any spouse Users insert
Child credential values are never logged or returned to the clientAPI 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) ​

ConcernFamily Portal (this ADR)Admin & Ministry Portal (ADR 0021)
ScopeOwn family group onlyAll members and all family groups
Who can use itMember, Spouse (their own family)Minister, Admin roles
Spouse-addInitiates the approval requestReviews and approves/rejects
Child-addParent submits; auto-approvedVisible in audit log; no required action
Child credential resetParent-autonomousNot surfaced (parent owns child creds)
Member directoryNot accessibleFull directory access per RBAC

Alternatives considered ​

OptionProsConsWhy not chosen
Parent-facing Family Portal, server-side scoped (chosen)Self-service within guardrails; clear authorization boundary; API-first consistent with ADR 0008Portal must be built as part of platform scope before feature Epics— chosen
Ministers manage all family changes manuallyKeeps family data in one admin surface; no parent-facing UI neededDoes not scale; parents cannot self-serve credential resets or scope changes; minister becomes a bottleneck for routine actionsRejected — violates self-service within guardrails principle
Children self-manage their own credentialsReduces parent involvementViolates parent-managed model (ADR 0007); children have no email/social login; COPPA requires verifiable parental consent on account creation and changesRejected — hard constraint from ADR 0007 and COPPA requirement
Store child credentials in ClerkUnified auth layer; single token pathClerk does not support parent-managed, no-email child credentials; passes minor PII to an external provider; breaks the COPPA boundaryRejected — ADR 0003 explicitly excludes Clerk from child accounts
Expose family management only through the Admin & Ministry PortalFewer surfaces to build; one admin UIParents would need a minister or admin to make routine changes; poor UX; contradicts the "family-not-just-a-church" modelRejected — 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_id scoped 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 ApprovalWorkflow table 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_id from session — or accepts it from the client — could let one parent overwrite another family's data. Mitigation: all family-scoped queries derive family_group_id from 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_id before 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 ApprovalWorkflow record should carry a requested_at timestamp; 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 ​

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.