Skip to content

0032 — Member Directory: field visibility and role-based access ​

Status: Accepted (revised 2026-06-22 — see §Amendment: Corrected member-detail model)

Date: 2026-06-22

ADO work item: AB#3074 (Platform Epic); story AB#3693 under Members feature

Deciders: Kristopher Turner (platform owner)


Context ​

ADR 0021 (Admin & Ministry Portal, capability area 5) established that the member directory exists and that admins have elevated access compared to ordinary members. It did not define what fields exist in a member's directory record, which fields each role can see, or which fields each role can edit on behalf of another member. ADR 0006 defines the six canonical Plane-2 roles but does not specify per-feature field-level visibility rules.

The result is a gap in the API contract for GET /members/:id and PUT /members/:id — the platform build (AB#3074) cannot proceed on the member detail surface without this decision.

Additionally, the current MembersPage.tsx renders a minimal inline panel (toggleSelected) that shows only name, role badge, status badge, email, and member ID. This is insufficient for a community member directory and is not a full-page route. This ADR establishes the correct model.

Decision ​

The member detail screen shows the same information to every authenticated role who views it. All roles see the same fields for a given member — there are no inline "extra" fields that appear only for admin or ministry leaders. Role-specific management capabilities (editing, suspending, role changes, pastoral oversight) are accessed via a "Manage" button that opens a separate management screen (/app/members/:id/manage). The server enforces who may navigate to the management screen and what they may edit there.

Adult member fields (everyone sees the same set): birthday, anniversary (if the member is married and has provided it), address, phone number, and email.

Child member fields: birthday only. No contact information is shown for minors.

A member viewing their own profile at /app/profile sees all their own fields for editing regardless of their role.

Member detail fields (by member type) ​

FieldAdult memberChild member
Display name / full name
Avatar / photo
Role label (read-only)
Family group name
Relationship in family
Birthday✅ (month/day)✅ (month/day only)
Anniversary (if married)✅ if provided
Phone number
Email address
Address
Bio / personal note

All fields are read-only on the detail screen for viewers who are not the profile owner. The "Manage" button is shown only to admin and ministry_leader — it is hidden for ordinary member callers.

Manage button and management screen ​

The "Manage" button appears at the top of the detail screen for admin and ministry_leader callers. It navigates to /app/members/:id/manage, which loads MemberManagePage:

RoleManagement screen capabilities
adminEdit any member's profile fields; change account status (suspend/reactivate); change role (links to Role Assignment page at /app/admin/roles); view audit log for this member
ministry_leaderPastoral oversight — view family context, pastoral notes (TBD scope); initiate approval-queue actions where applicable

The management screen is a separate page — no management content appears inline on the detail screen. Server-side: GET /members/:id/manage returns 403 for callers whose role is not admin or ministry_leader; PUT /members/:id operations (edit, status, role) are also 403 for callers outside those roles.

Children in the directory ​

Child sub-accounts (accountType: 'child') show only their birthday on the detail screen — no contact information. Children do not appear in the general member directory listing for ordinary member callers. Children appear to:

  • The child's own family head (primary account) — on the Family page
  • ministry_leader and admin — in their full member view

A child's /app/members/:id detail page is accessible to admin, ministry_leader, and the child's own family head. It renders the kid-only field set (birthday only).

Members list ​

The GET /members list page (/app/members):

  • Sorted alphabetically by last name (A → Z)
  • Family members grouped together under their family name
  • Full-text search by name (?q= parameter)
  • Clicking a member navigates to /app/members/:id (replaces the current inline toggleSelected panel)
  • Children included for admin and ministry_leader; hidden from member-tier callers
  • Role label is not displayed in the list row; it is visible on the member detail page only.

API contract ​

typescript
// Returned for all authenticated callers viewing a member
// Server returns the same shape regardless of caller role;
// admin/ministry_leader receive no extra inline fields.
type MemberDetailView = {
  id: string;
  displayName: string;
  firstName: string;
  lastName: string;
  role: AppRole;
  roleLabel: string;           // "Community Leader" | "Ministry Leader" | … | "Member"
  familyName: string;
  relationship: 'primary' | 'spouse' | 'child';
  accountType: 'primary' | 'spouse' | 'child';
  // Adult-only fields (absent for child accounts):
  birthdayMonthDay?: string;   // "March 12" — month and day only for all callers
  anniversary?: string;        // "June 8" — present if married and provided
  phone?: string;
  email?: string;
  address?: { street: string; city: string; state: string; zip: string };
  bio?: string;
  // Manage affordance:
  canManage: boolean;          // true for admin/ministry_leader; drives "Manage" button visibility
};

The server returns canManage: true for admin and ministry_leader callers. The client renders the "Manage" button based on canManage — it does not inspect the caller's own role from a JWT claim (consistent with ADR 0006: role is never trusted from the client).

Write contract (/members/:id/manage) ​

  • PUT /members/:id with profile fields — accepted for admin on any member; accepted for member on their own record only (403 otherwise).
  • PUT /members/:id with roleadmin only; 403 for all other roles.
  • PUT /members/:id with status (suspend/reactivate) — admin only; 403 for all other roles.
  • ministry_leader has no write access to other members' profile data via this endpoint. Their write authority is the Approval Queue and pastoral-scope actions on the management screen.

Amendment: corrected member-detail model (owner, 2026-06-22) ​

The original decision used a tiered inline field model — different fields appeared inline for member, ministry_leader, and admin based on their role. The platform owner clarified (2026-06-22) that this model is wrong:

Everyone sees the same detail screen. Admin and minister do NOT get extra inline fields — they get a "Manage" button that opens a separate management screen. This keeps the detail screen simple and puts management capabilities where they belong.

Additionally, the original model exposed full birthday (including year) to pastoral/admin tiers for all members. The corrected model restricts:

  • Kids: birthday only, no other fields.
  • Adults: birthday shown as month/day only on the public detail screen to all viewers. The full year is accessible to admin on the management screen, not on the shared detail screen.

The design doc (docs/internal/design/member-directory-rbac.md) and the three role-specific mockups (member-detail-*.html) have been revised to reflect this model.

Alternatives considered ​

OptionProsConsWhy not chosen
Single detail screen + Manage button (chosen)Simple, consistent view for all roles; management capabilities scoped to a separate surfaceRequires an additional management page— chosen
Tiered inline fields (original model)Role-differentiated detail in one screenComplexity grows as roles multiply; "extra" fields visible inline feel invasiveRejected by owner 2026-06-22
All fields visible to all authenticated membersSimplest APIExposes contact info and minors' data to every memberPrivacy; rejected
Inline panel only (status quo)Already builtNo route; insufficient fields; not discoverable via URLInsufficient for directory use; rejected

Consequences ​

Positive ​

  • Every role sees the same clean detail screen — no complex conditional field rendering.
  • Admin and minister management capabilities are cleanly separated onto their own screen.
  • Children's privacy is enforced: only birthday shown, no contact information exposed.
  • The canManage flag keeps role logic server-driven; the client does not branch on role.

Negative / trade-offs ​

  • Two screens instead of one (detail + management). Mitigation: management screen is accessed via a single button; the detail screen is simple and consistent.
  • The phonePublic column from the original model is no longer needed for tier control. Phone is simply always shown for adult members. If member-controlled phone visibility is needed in the future, that is a separate ADR.
  • The inline toggleSelected panel in MembersPage.tsx must be replaced with navigation to /app/members/:id.

Risks ​

  • Management screen access control — if the /manage route is not server-guarded, a member-tier user could navigate directly to the URL. Mitigation: GET /members/:id/manage returns 403 for callers outside admin/ministry_leader; the client router hides the route unless canManage: true is set in the detail response.
  • Kid detail visibility — if a child's /members/:id route is not gated, peer members can view it. Mitigation: GET /members/:id returns 403 for member-tier callers viewing a child account that is not in their own family.

References ​

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