---
id: ARCH-CRM-02
title: 'Households and Membership Coverage'
product_area: crm
revision: 1
status: proposed
source:
  artifact_url: https://claude.ai/artifact/XWNMFcPbBtfmpmGoXx7MzN
  artifact_title: 'Households and Membership Coverage'
  library_group: 'CRM and people'
  library_status: 'Decided in part'
  artifact_version: 4
  markdown_source: 'services-platform:docs/proposals/households@ee02d1a554fa9543feff4cba3805c0c99c6e50fc'
  converted_at: 2026-10-23
  converted_by: 'Forge 2.0 artifact strategy session'
contents:
  - n: 1
    title: 'Households, Constituent Relationships, and Membership Coverage: Research and Architecture Recommendation'
    anchor: '1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation'
  - n: 2
    title: '01. Executive Summary'
    anchor: '2-01-executive-summary'
  - n: 3
    title: '02. The Architectural Problem and Why It Matters'
    anchor: '3-02-the-architectural-problem-and-why-it-matters'
  - n: 4
    title: '03. Rhythm 1.0 Functionality and Lessons Learned'
    anchor: '4-03-rhythm-1-0-functionality-and-lessons-learned'
  - n: 5
    title: '04. Rhythm 2.0 Design Constraints: The Existing Architecture, Assessed'
    anchor: '5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed'
  - n: 6
    title: '05. Competitive Research'
    anchor: '6-05-competitive-research'
  - n: 7
    title: '06. Cross-Industry Requirements and Use Cases'
    anchor: '7-06-cross-industry-requirements-and-use-cases'
  - n: 8
    title: '07. Alternative Architecture Models'
    anchor: '8-07-alternative-architecture-models'
  - n: 9
    title: '08. Tradeoff and Decision Matrix'
    anchor: '9-08-tradeoff-and-decision-matrix'
  - n: 10
    title: '09. Recommended Domain Model'
    anchor: '10-09-recommended-domain-model'
  - n: 11
    title: '10. Proposed Data Model and Service Boundaries'
    anchor: '11-10-proposed-data-model-and-service-boundaries'
  - n: 12
    title: '11. Membership Coverage and Benefit Inheritance Strategy'
    anchor: '12-11-membership-coverage-and-benefit-inheritance-strategy'
  - n: 13
    title: '12. Migration and Implementation Implications'
    anchor: '13-12-migration-and-implementation-implications'
  - n: 14
    title: '13. Risks and Unresolved Questions'
    anchor: '14-13-risks-and-unresolved-questions'
  - n: 15
    title: '14. Architecture Decisions Requiring Approval'
    anchor: '15-14-architecture-decisions-requiring-approval'
  - n: 16
    title: 'A1. Household Lifecycle Rules'
    anchor: '16-a1-household-lifecycle-rules'
  - n: 17
    title: 'A2. Household 360 and the Console Experience'
    anchor: '17-a2-household-360-and-the-console-experience'
  - n: 18
    title: 'A3. Cross-Application Integration'
    anchor: '18-a3-cross-application-integration'
  - n: 19
    title: 'A4. Security, Privacy and Authorization'
    anchor: '19-a4-security-privacy-and-authorization'
  - n: 20
    title: 'A5. Testing Requirements'
    anchor: '20-a5-testing-requirements'
  - n: 21
    title: 'R1 — Rhythm 2.0 CRM macroservice: what a Households / Relationships / Family-membership proposal can build on'
    anchor: '21-r1-rhythm-2-0-crm-macroservice-what-a-households-relationships-family-membership-proposal-can-build-on'
  - n: 22
    title: 'Rhythm 2.0 Membership: what exists today, read for the household-membership proposal'
    anchor: '22-rhythm-2-0-membership-what-exists-today-read-for-the-household-membership-proposal'
  - n: 23
    title: 'R3. How the money-handling and engagement applications identify the person or organization'
    anchor: '23-r3-how-the-money-handling-and-engagement-applications-identify-the-person-or-organization'
  - n: 24
    title: 'R4 — Platform primitives for Households, person-to-person relationships and family memberships'
    anchor: '24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships'
  - n: 25
    title: 'R5 — Console and portal architecture for Households (research report)'
    anchor: '25-r5-console-and-portal-architecture-for-households-research-report'
  - n: 26
    title: 'R6 — Rhythm 1.0 precedent for households, families and person-to-person links'
    anchor: '26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links'
  - n: 27
    title: 'R7 — Rhythm 1.0 relationships, membership flowdown and eligibility, precisely'
    anchor: '27-r7-rhythm-1-0-relationships-membership-flowdown-and-eligibility-precisely'
  - n: 28
    title: 'W1 — How Blackbaud products model households, family relationships and family memberships'
    anchor: '28-w1-how-blackbaud-products-model-households-family-relationships-and-family-memberships'
  - n: 29
    title: 'W2 — How NetForum and iMIS model households, family relationships and family memberships'
    anchor: '29-w2-how-netforum-and-imis-model-households-family-relationships-and-family-memberships'
  - n: 30
    title: 'Households in nonprofit and association software: Salesforce NPSP, Salesforce Nonprofit Cloud, Tessitura, and eight smaller CRMs'
    anchor: '30-households-in-nonprofit-and-association-software-salesforce-npsp-salesforce-nonprofit-cloud-tessitura-and-eight-smaller-crms'
  - n: 31
    title: 'W4 — Association-management platforms: households, constituent relationships and membership coverage'
    anchor: '31-w4-association-management-platforms-households-constituent-relationships-and-membership-coverage'
  - n: 32
    title: 'W5 — Museum, zoo, aquarium, garden and cultural-institution membership platforms'
    anchor: '32-w5-museum-zoo-aquarium-garden-and-cultural-institution-membership-platforms'
  - n: 33
    title: 'W6 — Church management and community/alumni platforms: households, relationships, giving units, guardian authorization'
    anchor: '33-w6-church-management-and-community-alumni-platforms-households-relationships-giving-units-guardian-authorization'
decisions: []
modules: []
superseded_by:
---

# Households and Membership Coverage

## What This Record Is

Proposes how Rhythm 2.0 CRM should record households (the group an organization mails and recognizes as one unit), person-to-person relationships such as spouse or guardian, and who a membership covers and why, for museums, churches, fundraising shops and family clubs. Andrew ruled all nine decisions on 2026-10-21 (six as recommended, three changed: CRM-owned relationship tables, a household may hold a membership, implementation deferred until a signed customer needs it); nothing is built.

(Summary from the Rhythm 2.0 Architecture Library card; the parts below are the proposal's own text, converted verbatim.)

## Contents

1. [Households, Constituent Relationships, and Membership Coverage: Research and Architecture Recommendation](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation)
2. [01. Executive Summary](#2-01-executive-summary)
3. [02. The Architectural Problem and Why It Matters](#3-02-the-architectural-problem-and-why-it-matters)
4. [03. Rhythm 1.0 Functionality and Lessons Learned](#4-03-rhythm-1-0-functionality-and-lessons-learned)
5. [04. Rhythm 2.0 Design Constraints: The Existing Architecture, Assessed](#5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed)
6. [05. Competitive Research](#6-05-competitive-research)
7. [06. Cross-Industry Requirements and Use Cases](#7-06-cross-industry-requirements-and-use-cases)
8. [07. Alternative Architecture Models](#8-07-alternative-architecture-models)
9. [08. Tradeoff and Decision Matrix](#9-08-tradeoff-and-decision-matrix)
10. [09. Recommended Domain Model](#10-09-recommended-domain-model)
11. [10. Proposed Data Model and Service Boundaries](#11-10-proposed-data-model-and-service-boundaries)
12. [11. Membership Coverage and Benefit Inheritance Strategy](#12-11-membership-coverage-and-benefit-inheritance-strategy)
13. [12. Migration and Implementation Implications](#13-12-migration-and-implementation-implications)
14. [13. Risks and Unresolved Questions](#14-13-risks-and-unresolved-questions)
15. [14. Architecture Decisions Requiring Approval](#15-14-architecture-decisions-requiring-approval)
16. [A1. Household Lifecycle Rules](#16-a1-household-lifecycle-rules)
17. [A2. Household 360 and the Console Experience](#17-a2-household-360-and-the-console-experience)
18. [A3. Cross-Application Integration](#18-a3-cross-application-integration)
19. [A4. Security, Privacy and Authorization](#19-a4-security-privacy-and-authorization)
20. [A5. Testing Requirements](#20-a5-testing-requirements)
21. [R1 — Rhythm 2.0 CRM macroservice: what a Households / Relationships / Family-membership proposal can build on](#21-r1-rhythm-2-0-crm-macroservice-what-a-households-relationships-family-membership-proposal-can-build-on)
22. [Rhythm 2.0 Membership: what exists today, read for the household-membership proposal](#22-rhythm-2-0-membership-what-exists-today-read-for-the-household-membership-proposal)
23. [R3. How the money-handling and engagement applications identify the person or organization](#23-r3-how-the-money-handling-and-engagement-applications-identify-the-person-or-organization)
24. [R4 — Platform primitives for Households, person-to-person relationships and family memberships](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships)
25. [R5 — Console and portal architecture for Households (research report)](#25-r5-console-and-portal-architecture-for-households-research-report)
26. [R6 — Rhythm 1.0 precedent for households, families and person-to-person links](#26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links)
27. [R7 — Rhythm 1.0 relationships, membership flowdown and eligibility, precisely](#27-r7-rhythm-1-0-relationships-membership-flowdown-and-eligibility-precisely)
28. [W1 — How Blackbaud products model households, family relationships and family memberships](#28-w1-how-blackbaud-products-model-households-family-relationships-and-family-memberships)
29. [W2 — How NetForum and iMIS model households, family relationships and family memberships](#29-w2-how-netforum-and-imis-model-households-family-relationships-and-family-memberships)
30. [Households in nonprofit and association software: Salesforce NPSP, Salesforce Nonprofit Cloud, Tessitura, and eight smaller CRMs](#30-households-in-nonprofit-and-association-software-salesforce-npsp-salesforce-nonprofit-cloud-tessitura-and-eight-smaller-crms)
31. [W4 — Association-management platforms: households, constituent relationships and membership coverage](#31-w4-association-management-platforms-households-constituent-relationships-and-membership-coverage)
32. [W5 — Museum, zoo, aquarium, garden and cultural-institution membership platforms](#32-w5-museum-zoo-aquarium-garden-and-cultural-institution-membership-platforms)
33. [W6 — Church management and community/alumni platforms: households, relationships, giving units, guardian authorization](#33-w6-church-management-and-community-alumni-platforms-households-relationships-giving-units-guardian-authorization)

## 1. Households, Constituent Relationships, and Membership Coverage: Research and Architecture Recommendation

## Households, Constituent Relationships, and Membership Coverage: Research and Architecture Recommendation

Status: **proposed 2026-10-21; all nine decisions ruled by Andrew the same day (chapter 14 "Rulings"); implementation
deferred until a signed customer needs it (HC-D8); documentation only; nothing built, no code, migration, API, UI or
Linear task changed.** Branch `households-architecture` of services-platform (the precedent of the Commerce, Accounting,
CRM and record-lifecycle proposals). It is a standalone proposal: the CRM architecture (RHYTHM-CRM-ARCH-001, branch
`crm-architecture`) and the Membership architecture are not modified by it; once Andrew rules, each gets a pointer and
the CRM roadmap's CRM-M21 is re-planned as chapter [12 §6](#13-12-migration-and-implementation-implications) describes.

Cite it as **RHYTHM-HC-ARCH-001** with chapter and section ("RHYTHM-HC-ARCH-001 §11 3").

**Labels used throughout.** **Verified** (read in code, DDL, a named test, vendor documentation, a published API or
database reference, or open source), **Decided** (Andrew, dated), **Planned** (a proposal or milestone record, not
built), **Absent**, **Reported** (forums, implementers, customers), **Inferred** (our reasoning), **Unavailable**
(searched, not found).

### The recommendation in three lines, and the rulings

1. Relationships between people: restore them. Recommended on Platform associations; **ruled: CRM-owned tables** (HC-D1;
   DDL in 10 §2a).
2. The mailing and recognition unit: a narrow first-class household record, never an organization, never a money party.
   Recommended never a membership holder; **ruled: a household may also hold a membership** (HC-D3; note in 11 §2).
3. Membership coverage: explicit beneficiary rows with a basis, produced by designation or by package rules; one
   `/entitlements` answer that says why. Ruled as recommended (HC-D4). Implementation is deferred until a signed
   customer needs it (HC-D8).

### The documents

| #   | Document                                                                                                  | Brief section                                                                                                     |
| --- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 01  | [Executive summary](#2-01-executive-summary)                                                              | 1                                                                                                                 |
| 02  | [The architectural problem and why it matters](#3-02-the-architectural-problem-and-why-it-matters)                           | 2                                                                                                                 |
| 03  | [Rhythm 1.0 functionality and lessons learned](#4-03-rhythm-1-0-functionality-and-lessons-learned)                  | 3                                                                                                                 |
| 04  | [Rhythm 2.0 design constraints: the existing architecture, assessed](#5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed)   | 4                                                                                                                 |
| 05  | [Competitive research, with the evidence table and sources](#6-05-competitive-research)                   | 5                                                                                                                 |
| 06  | [Cross-industry requirements and use cases](#7-06-cross-industry-requirements-and-use-cases)              | 6                                                                                                                 |
| 07  | [Alternative architecture models](#8-07-alternative-architecture-models)                                  | 7                                                                                                                 |
| 08  | [Tradeoff and decision matrix](#9-08-tradeoff-and-decision-matrix)                                        | 8                                                                                                                 |
| 09  | [Recommended domain model](#10-09-recommended-domain-model)                                                | 9                                                                                                                 |
| 10  | [Proposed data model and service boundaries](#11-10-proposed-data-model-and-service-boundaries)                     | 10                                                                                                                |
| 11  | [Membership coverage and benefit inheritance strategy](#12-11-membership-coverage-and-benefit-inheritance-strategy) | 11                                                                                                                |
| 12  | [Migration and implementation implications](#13-12-migration-and-implementation-implications)                           | 12                                                                                                                |
| 13  | [Risks and unresolved questions](#14-13-risks-and-unresolved-questions)                                    | 13                                                                                                                |
| 14  | [Architecture decisions requiring approval](#15-14-architecture-decisions-requiring-approval)                           | 14                                                                                                                |
| A1  | [Household lifecycle rules](#16-a1-household-lifecycle-rules)                                                        | relationship and flow-down rules, life events                                                                     |
| A2  | [Household 360 and the console experience](#17-a2-household-360-and-the-console-experience)                                      | optional customer-facing workflows                                                                                |
| A3  | [Cross-application integration](#18-a3-cross-application-integration)                                                  | APIs, queries, Finance, Events, Marketing, Fundraising, Data Engine                                               |
| A4  | [Security, privacy and authorization](#19-a4-security-privacy-and-authorization)                               | guardian and manager authority, minors                                                                            |
| A5  | [Testing requirements](#20-a5-testing-requirements)                                                        | testing requirements                                                                                              |
| —   | [evidence/](evidence/)                                                                                    | w1–w6 competitive reports with every URL and evidence label; r1–r7 code investigations with `path:line` citations |

### How this document was produced

- Nine code and proposal investigations (r1–r7, two of them read 1.0 through the GitHub CLI) at services-crm
  `origin/master` `eee6a5f4`, services-platform `0614c54cf`, services-membership `fcbddf2`, services-accounting
  `17cca211`, services-commerce `bd62910a`, react-console `13e4bcee9`, rhythm-web `5ecc208`, and the Membership,
  Fundraising, Marketing, Billing, Events, custom-objects, record-lifecycle and mobile proposals.
- Six competitive reports over forty-one products, primary documentation first (published database and API references,
  open source), with every claim labelled and every unfetchable page listed.
- An earlier draft of this work recommended the household as a membership holder and was written to be folded into the
  CRM architecture; Andrew's follow-up brief of 2026-10-21 reopened the question across five approaches, forbade a
  name-coupled or protected organization type, and asked for a standalone recommendation. The holder decision was
  reversed on the evidence (chapters [07](#8-07-alternative-architecture-models) and
  [11 §2](#12-11-membership-coverage-and-benefit-inheritance-strategy)).
- The prose chapters were edited for readability by the Humanize pass and checked fact by fact against the evidence
  reports; tables, DDL and API shapes were kept verbatim.

### Standing decisions this proposal treats as constraints

Custom fields opt-in per entity (R2-ARCH-001); milestone standard R2-DEV-001; submission-compatible operations
(R2-ARCH-002); ids standard (configuration strings, records UUIDv7); console one live switch; stabilization first; focus
order (CRM, Accounting, Integrations); portal always last, on Andrew's components; Membership stopped 2026-10-19; CRM
owns the consent ledger (Marketing D-M2); custom-objects D-04 (associations are the model for tenant-defined
relationships including contact-to-contact); payment model "one receipt, many customers"; no Asana; Jira RX for tickets;
Humanize for everything presented.

## 2. 01. Executive Summary

## 01. Executive Summary

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation).

> **Rulings (Andrew, 2026-10-21).** All nine decisions were ruled the day this was delivered. Six follow the
> recommendation. Three do not: relationships live in CRM-owned tables rather than Platform associations (HC-D1); a
> household may also hold a membership (HC-D3); implementation is deferred until a signed customer needs it (HC-D8).
> Chapter [14](#15-14-architecture-decisions-requiring-approval) records each ruling and its consequences; chapters 09, 10 and 11
> carry "as ruled" notes where the design changes.

### The recommendation

Rhythm 2.0 should use three distinct primitives to answer three different questions. Two of those primitives already
exist:

1. **How are two people related?** Restore person-to-person relationships on **Platform associations**, built 2026-10-19
   and approved for contact-to-contact relationships on 2026-10-18. CRM seeds the system types: spouse, partner, parent
   and child, guardian and dependent, grandparent and grandchild, sibling, caregiver, authorised representative. CRM
   also supplies the semantics that associations lack: whether a type is symmetric, whether it may authorise access to a
   minor or dependent, and whether it counts for household naming. Each relationship is dated and uses one row with two
   labels, readable from either side. Backfill the interim `spouse_id` into this structure, then retire it.
2. **What unit does an organization mail, recognise and let a family manage?** Add a narrow **first-class household
   record** in CRM. Compute its name, addressee and salutations from its members, with overrides. Link a shared address
   by reference into members' records. Household membership is dated, with a configurable role, one primary contact per
   household, one primary household per person, and a manager flag. A person may belong to several households. Dissolve,
   reinstate, merge and split are operations with history. The household is **never an organization, never a money party
   and never a membership holder**.
3. **Who is covered by a membership, and why?** Keep each membership held by **one contact or organization**. Represent
   coverage as explicit **beneficiary rows**, each recording a coverage role, dates and the **basis** that justifies it.
   That basis is either a person's designation of the beneficiary within the package's rules, or a rule deriving the row
   from a CRM fact: an affiliation in a role, a relationship of a type, or membership of the holder's household.
   Coverage rules belong to the package and specify the role, predicate, cap, age window, whether to designate or
   derive, what happens when the fact stops holding, and an add-on price for extras. Use the same evaluator for
   corporate flow-down and family memberships. One `/entitlements` answer tells any consumer whether a person is
   entitled under a program and why.

Households and relationships remain independent of each other and of Membership. Membership reads CRM facts only through
its coverage rules and stores no household id. Commerce, Accounting and Billing change nothing.

### Why this and not the alternatives

The brief named five approaches. Once separated by the three questions they answer, those approaches no longer compete
as alternatives to a single model:

- **Households as organizations** (an organization type carrying a household attribute, never a name or a protected row)
  offers the most reuse on paper and was this work's own earlier direction. The code rules it out. A contact's one
  primary affiliation is their employer, so it cannot also express a primary household. An organization's name is typed;
  a household's name is computed. Every service treats an organization as a bill-to, payer and donor. Preventing a
  family from being billed as a company would therefore require about fourteen attribute branches across five services
  ([07 §3.3](#8-07-alternative-architecture-models)). That is precisely the special-casing and coupling the brief asked
  to avoid.
- **Relationships only, no household record** is right for coverage but wrong for the segments. Churches, museums,
  fundraising shops and family clubs need a stable addressee, salutation, shared address, giving unit and history that
  survive any member's change. Raiser's Edge is the twenty-year experiment in this model. Its head-of-household flag,
  and the mailing drop-outs that flag causes, are the result.
- **Memberships with beneficiaries and no household in the engine** is adopted as the coverage design. It is made
  general by supporting both designated and derived beneficiaries, each with a basis, so employees and families use one
  mechanism.
- **The household as a membership holder** was the first draft's choice. It adds a party kind to Membership and Commerce
  for a label. Its operational cases, including death, separation and portal visibility, are already covered by owner
  transfer, beneficiary rows and a manager grant. The grandparent and corporate scenarios are impossible under this
  model.
- **A generic constituent group** is the household table under a more abstract name, alongside two grouping mechanisms
  that already exist.

Forty-one products confirm this shape ([05](#6-05-competitive-research)): where a household exists, it is a record that
people link to; kinship lives in a second typed structure; money stays on a person; family membership has one holder
with named others under caps; and authority over a child is an explicit grant. Nobody returns "why is this person
entitled"; Rhythm can.

### What it costs, and what it does not

The CRM work is a module built on the organization template: tables, aggregate, routes, dataset, merge planner and
console entity. It also includes a seed set and a facade over Platform associations. Platform needs three small
additions: a symmetric flag and system code on association types, and a `crm` derivation source for manager and guardian
roles.

When Membership resumes, its work is a beneficiary table, coverage rules and an evaluator generalised from the
organization flow-down it already runs. There is no change to any party vocabulary, invoice, payment, order or stored
method.

### What to do now

Rule on the nine decisions in chapter [14](#15-14-architecture-decisions-requiring-approval): relationships on Platform associations
(HC-D1); households as a first-class record (HC-D2); the holder stays a contact or organization (HC-D3); explicit
beneficiaries with designated and derived bases (HC-D4); custom fields on households (HC-D5); several households per
person with one primary (HC-D6); manager and guardian authority as derived grants (HC-D7); sequencing (HC-D8); naming
formats as a closed list (HC-D9).

Then build nothing until the retroactive CRM milestones are accepted. Split CRM-M21 into relationships and households.
Schedule coverage as Membership's first household milestone when it resumes. Fundraising, Marketing and Events take
their pieces in their own plans; the portal comes last.

## 3. 02. The Architectural Problem and Why It Matters

## 02. The Architectural Problem and Why It Matters

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Terms used in every household
chapter are defined in §4.

### 1. The problem

Rhythm 2.0 CRM has no household concept. It knows two kinds of party: a **contact** (a person) and an **organization**
(a company, school, chapter or any other non-person body), joined by dated **affiliations**. This model serves
professional and trade associations well: a person works for an employer and holds roles there, while the employer may
be the member and the payer.

The gap affects the second large population of Rhythm's market: organizations whose members and donors come as
**families**. These include museums, zoos, aquariums, science centres, YMCAs and community centres, parent and alumni
associations, faith communities, youth sports leagues, swim and tennis clubs, homeowner associations, and the family and
partner programs of professional societies. They sell memberships that cover several people in one home, mail one letter
to one address with two names on it, let a parent register a child for a camp, and want to know what "the Garcias" have
given over the years.

Without a household concept, each of these organizations must work around the gap. The same workarounds recur in every
generation of nonprofit software ([05](#6-05-competitive-research) §8, `05-competitive-research.md`):

- **Duplicate records of convenience.** "Mr. and Mrs. John Smith" is created as one contact, or a child is created as an
  organization so that a membership can "cover" people.
- **Two letters to one mailbox.** Direct mail and appeals cannot suppress the second copy without a household.
- **No family membership.** A membership record names one person. The other covered people are either invisible, so the
  front desk cannot check them in, or each receives a membership, so counts, dues and renewals are wrong.
- **Giving that cannot be summed.** A spouse's gift and a partner's gift appear as two unrelated donors in every report,
  recognition list and major-gift pipeline.
- **No safe way to let a parent act for a child.** Either the parent cannot register the child, or the child is given a
  login they should not have.
- **A spouse field that is not a relationship.** 2.0 today carries a single reciprocal `spouse_id` (05 §2.1
  (RHYTHM-CRM-ARCH-001 `05-functional-architecture.md`)), ruled an interim measure until relationships exist (`D-19`).

Three distinct questions have been argued as one: how two people are related, what unit an organization mails and
recognises, and who a membership covers and why. This proposal separates those questions. It evaluates the five
approaches the debate produced against the competitive evidence ([05](#6-05-competitive-research)), the segments Rhythm
intends to serve ([06](#7-06-cross-industry-requirements-and-use-cases),
`06-cross-industry-requirements-and-use-cases.md`) and the architecture as it stands
([04](#5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed), `04-rhythm-2-design-constraints.md`). It then recommends one model
([09](#10-09-recommended-domain-model), `09-recommended-domain-model.md`), with its schema
([10](#11-10-proposed-data-model-and-service-boundaries), `10-data-model-and-service-boundaries.md`), coverage strategy
([11](#12-11-membership-coverage-and-benefit-inheritance-strategy), `11-membership-coverage-and-benefit-inheritance.md`),
implementation path ([12](#13-12-migration-and-implementation-implications), `12-migration-and-implementation.md`) and decisions that
need a ruling ([14](#15-14-architecture-decisions-requiring-approval), `14-decisions-requiring-approval.md`).

### 2. What a household is for, and what it is not

A household in Rhythm is **a group of related people that an organization treats as one unit for some purposes**:
mailing, recognition, family membership, family registration, and self-service. It is a CRM record with its own
identity, name, status, contact information and history, and a dated list of members with roles.

A household is **not**:

- a statement that its members live at one address, share a surname, or have any legal relationship with each other;
- a grant of authority: household membership gives nobody the right to see, change or act for another member. Authority
  is a separate, explicit grant ([A4](#19-a4-security-privacy-and-authorization), `A4-security-privacy-authorization.md`);
- a legal person: a household never pays, never gives, and never signs. The people in it do, and financial records
  always name the person or organization that acted ([A3 §2, §5](#18-a3-cross-application-integration), `A3-cross-application.md`);
- a replacement for person-to-person relationships: "parent of", "spouse of" and "guardian of" are facts between two
  people. Those facts are kept whether or not the two share a household
  ([09 §4](#10-09-recommended-domain-model));
- an organization: it has no employees, contact roles, compliance rules, hierarchy, website or tax status, and it must
  never appear in organization lists, pickers, duplicate rules or reports
  ([09 §3](#10-09-recommended-domain-model)).

The model must represent the plain nuclear family, but it cannot require that shape. The households that matter also
include domestic partners; multigenerational homes; a divorced couple whose children belong to both of their households;
blended families; a grandparent with custody; an adult child caring for a parent; two unrelated adults who share a home
and a membership; and a "household" that exists only because a donor wants recognition as "The Patel Family" while
living in three cities. Each must fit the model without being forced into a nuclear-family structure.

### 3. Customer use cases

Each use case identifies who acts, what they do and the observable outcome. The milestones in
[12](#13-12-migration-and-implementation-implications) cite these ids; the acceptance journeys in [A5](#20-a5-testing-requirements)
(`A5-testing-requirements.md`) prove them.

#### 3.1 CRM foundation (Phase 1)

| Id   | Who                   | Use case                                                                                                                                            | Outcome                                                                                                                                                                 |
| ---- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| U-01 | Membership staff      | Create a household from an existing contact, add a spouse (existing contact) and two children (new contacts), and mark the first contact as primary | One household, four dated memberships, one primary; the children's addresses are linked to the household's address; the spouses are related by a symmetric relationship |
| U-02 | Staff                 | Open a contact and see the household(s) they belong to, with role and dates, and jump to Household 360                                              | A Household card on Contact 360; Household 360 shows the members, address, memberships, giving and timeline                                                             |
| U-03 | Staff                 | Record that two contacts are parent and child, with a start date, without putting them in a household                                               | The relationship shows on both records in the right direction; no household is created                                                                                  |
| U-04 | Staff                 | Move the household (change the household's Home address); every member whose address is linked follows                                              | One edit; linked members resolve to the new address; copied or owned addresses do not change                                                                            |
| U-05 | Staff                 | A couple separates: end one adult's household membership on a date, create a new household for them, move one child to both households              | Both households show correct current and past members; the child is current in two households; history is kept; nothing is deleted                                      |
| U-06 | Staff                 | Find probable duplicate households ("The Smith Family" twice) and merge them                                                                        | One survivor with the union of members (duplicates collapsed), addresses chosen per type, aliases for the merged id, every reference in every application re-pointed    |
| U-07 | Staff                 | Merge two contacts who belong to households                                                                                                         | The survivor holds each household membership once; relationships are re-pointed; a self-relationship that would result is removed, not kept                             |
| U-08 | Development staff     | Set the household's mailing name to "The Rivera-Chen Family" and the formal salutation to "Dr. Rivera and Mr. Chen", overriding the computed values | Overrides stick through member changes; the computed values are still visible and can be restored                                                                       |
| U-09 | Any staff             | Query: households with children under 12; contacts in more than one household; households whose primary contact has no e-mail                       | One row per household, counts correct, no duplicate rows from the member join                                                                                           |
| U-10 | Administrator         | Configure relationship types ("Mentor of / Mentee of"), household roles ("Au pair"), and the household naming format                                | Configuration with string ids; system rows cannot be deleted; renaming changes labels only                                                                              |
| U-11 | Data operations staff | Import households, members and relationships from a spreadsheet exported by another system, validate, apply, and roll back                          | Dependency-ordered groups (contacts, households, members, relationships); per-row outcomes; a rollback leaves no household rows                                         |

#### 3.2 Membership coverage (Phase 2)

| Id   | Who                     | Use case                                                                                                                                                      | Outcome                                                                                                                                                                               |
| ---- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| U-20 | Front desk              | Sell a Family membership: the mother holds it, her spouse and three children are named as beneficiaries, proposed from the household; the father pays by card | One membership held by the mother; four beneficiary rows with their basis; one invoice to the payer; each covered person "is a member" with an explainable reason                     |
| U-21 | Front desk              | Scan a child's card or look the child up at the door                                                                                                          | The child shows as a beneficiary of the mother's active membership, with the term, the coverage role and the basis                                                                    |
| U-22 | Portal member           | Renew the family membership online, drop a child who turned 18, add a newborn                                                                                 | New term; coverage recorded as of the renewal; an age-out shown with its reason; proration rules of the program applied                                                               |
| U-23 | Donor                   | Buy a Family membership as a gift for a daughter's family                                                                                                     | The daughter holds it; the donor is the buyer and bill-to and appears on the payment and the acknowledgement; the donor is not covered                                                |
| U-24 | Staff                   | Upgrade a Family membership to Family Plus mid-term                                                                                                           | Same membership, new tier from a date, prorated charge per program rules, coverage capacity recalculated                                                                              |
| U-25 | Staff                   | A covered adult leaves the household                                                                                                                          | An attention item is raised (or coverage ends at once if the package says so); the membership and the other beneficiaries continue; the leaver can spin off into their own membership |
| U-26 | Staff                   | A person holds an individual Professional membership and is covered by their parents' Family membership                                                       | Both show on the person; "member status" answers the right program for each question; no double counting in membership counts by program                                              |
| U-27 | Corporate administrator | An employer's organizational membership covers qualifying employees                                                                                           | Each employee holds a derived beneficiary row whose basis is their affiliation; it ends when the affiliation ends; the same mechanism as the family package                           |

#### 3.3 Broader engagement (Phase 3)

| Id   | Who                  | Use case                                                                                                               | Outcome                                                                                                                                                                                       |
| ---- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| U-30 | Development staff    | See household giving: every gift by any member, with the legal donor on each, and the household total                  | A household total that never double counts; each gift keeps its donor of record; recognition credit at household level                                                                        |
| U-31 | Development staff    | Send one year-end appeal per household, addressed with the household's mailing name, suppressing members who opted out | One piece per household; an opted-out person is not named; a household whose members all opted out receives nothing                                                                           |
| U-32 | Parent on the portal | Register two children for a camp, sign the waiver as guardian, pay                                                     | Registrations name the children as attendees and the parent as registrant and payer; the waiver records the guardian relationship it relied on                                                |
| U-33 | Parent on the portal | Update a child's allergy information and the household's address; try to edit the other adult's profile                | The child's edit succeeds through an explicit guardian authorization; the household address edit succeeds for a household manager; the adult's profile is refused                             |
| U-34 | Staff                | A household dissolves                                                                                                  | All member rows ended with a reason; beneficiary rows justified by them listed for decision; memberships themselves untouched (they belong to people); the household archived, history intact |

### 4. Terms

| Term                         | Meaning in this proposal                                                                                                                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Household                    | A CRM record: a group of related people treated as one unit for mailing, recognition, membership, registration and self-service. Has identity, name, status, type, contact information, members, history.                                                                |
| Household member             | A dated row linking one contact to one household with a role (**household membership** in this document always means this, never a Membership-application membership).                                                                                                   |
| Household role               | Configurable label on a household member row (Adult, Child, Dependent, Guardian, Other). It describes the person's place in the group; it grants nothing.                                                                                                                |
| Primary contact              | The one current member a household names as its default correspondent and the subject of its computed name. At most one at a time. Not "head of household": the term is avoided because it implies authority the record does not confer.                                 |
| Billing contact              | The contact who receives a holder's invoices and renewal notices when the holder is an organization or asks for them to go elsewhere. A Membership concern ([11 §5](#12-11-membership-coverage-and-benefit-inheritance-strategy)).                                                 |
| Relationship                 | A dated, typed fact between two contacts (spouse of, parent of, guardian of), with a label for each direction. Independent of households.                                                                                                                                |
| Relationship type            | Configuration: the two direction labels, whether symmetric, whether it may authorize access (guardian-type).                                                                                                                                                             |
| Membership holder            | The one contact or organization that owns a Membership-application membership. Never a household.                                                                                                                                                                        |
| Payer                        | The party that paid: the contact or organization on the payment. Never a household.                                                                                                                                                                                      |
| Beneficiary (covered member) | A person entitled to some or all benefits of a membership they do not hold, recorded explicitly with dates, a coverage role and the basis that justifies the row ([11](#12-11-membership-coverage-and-benefit-inheritance-strategy)).                                              |
| Authorized manager           | A person permitted to administer a household or a membership on the portal, through a Platform Security grant, never through household membership alone.                                                                                                                 |
| Guardian authorization       | An explicit, dated authorization for one person to act for another (a minor or a dependent adult), derived from a relationship whose type allows it and confirmed by staff or by the authorized person's own consent rules ([A4](#19-a4-security-privacy-and-authorization)). |
| Minor                        | A contact whose age, from their recorded birth date, is below the tenant's age of majority (default 18). A contact with no birth date is treated as an adult for access and as "age unknown" for coverage rules.                                                         |
| Donor of record              | The legal giver named on a gift and its tax acknowledgement. Always a contact or an organization. Household giving is an aggregate over members' gifts, plus recognition credit, never a change of donor ([A3 §5](#18-a3-cross-application-integration)).                             |

## 4. 03. Rhythm 1.0 Functionality and Lessons Learned

## 03. Rhythm 1.0 Functionality and Lessons Learned

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence:
[r6](#26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links) (the inventory) and [r7](#27-r7-rhythm-1-0-relationships-membership-flowdown-and-eligibility-precisely) (the exact shapes,
algorithms, triggers and defect trail, read from `~/rhythm` and the private `rolodex-relationship-types` repository
through the GitHub CLI). Rhythm 1.0 is the production product. Its code is this proposal's most reliable evidence of
what Rhythm's own customers did and where the product broke.

### 1. The one-paragraph answer

Rhythm 1.0 never had a household, family, dependent, guardian or minor concept, and no customer ticket in six years
asked for one by name. It had a one-way `spouse_id` pointer that no form exposed; a generic typed relationship link
(2022) with no direction semantics, displayed only in the 360 tab and consumed only by Fundraising's automatic soft
credits; and a portal delegation design (2023) whose backend was never wired. Its membership flow-down and seat
mechanism operated entirely through the **organization**: an inherited benefit overwrote the person's one membership
record. Event guests were where customers actually encountered spouses and children, and the e-mail match-or-create
mechanism there produced duplicates for years.

### 2. Contact relationships (2022)

**What existed.** `rolodex-relationships` stored one row per link, with `type_id`, `left_side_type/id/name`,
`right_side_type/id/name`, `start_date` and `end_date`. Both endpoints were indexed. A type in use could not be deleted,
and a link was deleted with either endpoint. `rolodex-relationship-types` had exactly five fields (`id`, `name`,
`left_side_type`, `right_side_type`, `is_active`); validation and cleaning remained template stubs. The console offered
a name and two Contact/Organization selects. The Relationships tab showed the type name, the other party and
Current/Past/Future.

**How it behaved.** Direction depended on which record's page the user started from. The editor put the current record
on the left when its type matched, so Larry's page wrote Larry→Jesse and Jesse's page wrote Jesse→Larry. There was no
uniqueness constraint and nothing marked one link as the reciprocal of the other. An asymmetric type ("Parent of") read
identically on both records. Names were copied at save and never refreshed.

No query consumed the relationships: there were zero hits in `system-queries` and `rolodex-query-definitions`. They had
no use in the portal, Membership or Events. Support told customers in 2023 not to use them ("there is no way to set or
view the relationships in the portal … they cannot be pulled into a query", ZD 9942, ZD 10599). The 2.0 migration maps
contact→organization relationships to affiliations and excludes contact→contact ones as having "no 2.0 home".

**The one consumer.** Fundraising settings mapped relationship types to a direction
(`left to right | right to left | both`) and a percentage for automatic soft credits on every gift ("if either spouse
makes a gift, the other spouse gets credit"). The credits were shredded to a reporting table, forward-only. Because
stored direction was arbitrary, this one direction-aware consumer behaved inconsistently for symmetric types.

**Lessons.**

1. A relationship type must carry direction and reciprocity: one row with two labels, or a declared symmetric type. The
   store must refuse duplicates in either order.
2. A relationship primitive without consumers rots. This proposal gives relationships four readers on day one (naming,
   guardian authorization, recognition, coverage rules), plus generated query families and a 360 card.
3. Copied names and cascade-delete on merge lose history. The 2.0 associations re-point on merge and keep history.
4. The soft-credit use proves the demand for typed, directional person-to-person facts. This proposal makes the
   household, not the spouse pair, the recognition basis and keeps the relationship as its justification.

### 3. Membership flow-down and assigned seats

**Structural fact.** A 1.0 membership record's id was `contact:<id>` or `organization:<id>`: **each CRM record had at
most one membership record**. Flow-down and assignment did not add a second membership. They _overwrote the person's
only one_, subject to `conflicting_membership_preference`. They copied status, type, package, dates, chapters, sections
and add-ons from the parent, and recorded `origin`, `parent_membership_id`, `master_membership_id`, `flowdown_depth` and
`overridden_fields`.

**Who received.** Flow-down went to contacts of the organization who held a configured contact role; empty roles meant
everyone. It cascaded to child organizations by organization type, to depth 10. Flow-down **never** read
contact-to-contact relationships (confirmed: no relationship SDK is imported anywhere under `membership-*`). Nothing let
an individual membership cover a spouse, child or guest. "spouse" appears only in events demo data.

**Triggers.** Four SQS queues drove processing: membership saves, contact and organization changes, assigned-roster
diffs, and data-import reprocess. There was no scheduled sweep. Revocation depended only on CRM change events, with
three branches whose outcomes differed according to history. Two of the three were wrong for years: a flow-down member
whose organization was removed lingered lapsed until RB-5496, 2026-07; a person whose own earlier membership had been
overwritten had the _oldest_ snapshot restored, sometimes as active with no expiration, until RB-7487, 2026-09. A
principal member kept pointing at the old organization until RB-6105.

Switching flow-down off on a type did not stop processing. It forced the roles list empty, which the "no roles" test
read as "everyone", and ran a revoke pass over every contact. The console never exposed `flowdown_membership_type_id`,
so a flowed-down contact carried the organization's own membership type.

**Seats.** The organizational package set `maximum_number_of_contacts`, an add-on product for extras and, since RB-5875,
`total_seats_purchased`. The organization record held `assigned_memberships[]`, while the children held a back-pointer:
two sources of truth. The portal "Manage member contacts" roster let users pick existing contacts or type new ones.
Rosters, billing notices and seat credits all derived from the back-pointer, that is, from the copy rather than the
affiliation.

**Defect trail.** Merges and deletions left dangling children and FK-violation loops because the copied contact and
organization ids were not registered references (RB-22, RB-7337, RB-7641; e06 pattern P1). Queue fan-out reached
500,000-message backlogs (RB-2259), and every configuration change needed a manual "reprocess". Role cardinality was
never enforced, so "flow to Owners" and "bill the Owner" picked arbitrary people (RB-7662: 828 organizations with
several Owners). Rows multiplied in queries (P7). The 2026 fix PR counted about 650 silent no-op revocations a month and
eleven known bad live records.

**Lessons.**

1. A derived benefit must be a **second, derived record with lineage**, never an overwrite of the person's own
   membership. services-membership already does this (child rows with `origin` and `granted_by_membership_id`). This
   proposal keeps that model and adds the basis.
2. Policy expressed as declarative options on the type or package (roles, cascade, depth, caps) was right. Keep it as
   coverage rules.
3. Derivation must be event-driven **and** swept, idempotent and diff-gated. A disabled rule must mean "stop", never
   "revoke everyone".
4. Every copied id must be a registered reference with a merge and deletion policy (the 2.0 coverage guard).
5. Roles that drive benefits or billing need enforced cardinality (CRM-Q7) or an explicit designation. This proposal
   uses explicit beneficiary rows and a named billing contact.

### 4. Eligibility and benefits

There were seventeen criterion kinds; none referenced relationships or households. The only "through another record"
evaluation was through the employer (`use_organization_for_eligibility`, organization restrictions,
`organizationMembership` and `parentOrganization` facts).

Two notions of "member" disagreed. The `members` criterion was satisfied by the **existence** of a membership record,
with status checked only when `member_statuses` was configured. Member pricing, by contrast, required
`status === 'active'` or a package in the same order.

Benefits consisted of `member_price` and `special_prices[criteria]` on products, member-only restrictions through the
criteria object, and per-owner commerce entitlements that were never inherited or shared. Age existed as a
`minimum_age`/`maximum_age` criterion on `date_of_birth`.

**Lessons.** Pricing, registration and the gate must consume one entitlement answer, with status and basis (chapter
[11 §6](#12-11-membership-coverage-and-benefit-inheritance-strategy)). Age belongs as a rule on the coverage row, not as a
property of a person's membership.

### 5. Guests, delegation and portal reach

Event guests were per-registration sub-records. They were matched by e-mail or created as contacts, or linked to the
registrant's own id when no contact existed. This produced duplicates for years (RB-2568, RB-2697, RB-2838, RB-3480,
RB-4251; 86 Zendesk guest tickets). Registration groups belonged to organizations.

Portal delegation (RB-2274, "register someone else who isn't in your organization", with consent and a time limit) was
designed in 2023 but never ran. It left a `portal_delegates[]` field, an index and a manageability rule. Portal users
linked to exactly one contact. Their reach was their own contact, the same organization, ancestors and descendants, and
unexpired delegates.

**Lessons.** Every covered person is a real, de-duplicable contact, never a sub-record. "act for someone outside my
organization" is delivered with its flow (guardian and representative grants, chapter
[A4](#19-a4-security-privacy-and-authorization)). Authority is one server-side rule, never per channel.

### 6. What customers asked for, and did not

Customers asked for queryable, portal-visible relationships (ZD 9942, ZD 10599); a person in more than one organization
with one primary (ZD 11124; answered by 2.0 affiliations); acting on behalf of someone outside the employer with consent
(RB-2274, RB-2623); spouse and family guests priced and limited as such (ZD 367, ZD 13811, ZD 18889); and removal of the
dead spouse field from member-facing screens (ZD 10243, ZD 11199).

**Not asked, by name, in six years:** a family or household membership, dependents on a membership, parent–child member
records, minors. The demand for households comes from the market Rhythm intends to serve (chapters
[05](#6-05-competitive-research) and [06](#7-06-cross-industry-requirements-and-use-cases)), not from the 1.0 support
record. That distinction is a reason to build the foundation narrowly and the coverage mechanism generally.

### 7. Concept by concept: 1.0, defect, 2.0 design

| Concept                   | 1.0 representation                                                    | Defect pattern                                                                                       | This proposal                                                                                                  |
| ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Holder                    | The one `contact:<id>` / `organization:<id>` record with `origin`     | Inherited grant overwrote the personal membership; restore-from-snapshot resurrected lapsed ones     | One holder (contact or organization); derived grants are separate child rows with lineage                      |
| Payer                     | `bill_organization` boolean; invoice recipient by contact role        | Unenforced role cardinality → arbitrary invoice recipient                                            | Commerce buyer and bill-to; a named billing contact on the membership                                          |
| Beneficiaries (flow-down) | Child records by role and organization type; fields copied            | Copy drift, queue fan-out, wrong revoke branches, merge orphans, "off" means revoke all              | Beneficiary rows with a basis, derived by rules, event-driven and swept, registered references                 |
| Beneficiaries (seats)     | `assigned_memberships[]` plus back-pointers; `total_seats_purchased`  | Two sources of truth; over-charging; roster failures                                                 | Designated beneficiary rows with caps and add-on products; one source of truth                                 |
| Eligibility               | Criteria object on products and processes; employer-only indirection  | No relationship or household criterion; "member" = record exists                                     | Coverage rules on the package with closed predicates over CRM facts; age as a rule                             |
| Entitlement               | `member_price`, member-only criteria, per-owner entitlements          | Pricing and eligibility disagreed; benefits never inherited                                          | One `/entitlements` answer with status and basis for every consumer                                            |
| Relationships             | Typed, dated two-sided link; type = name and side types; no direction | Direction by page of origin; duplicates; no reciprocal label; no consumers; cascade-deleted on merge | Platform associations with direction and symmetry semantics, seeded types, four consumers, merge participation |
| Households                | None                                                                  | Families modelled as organizations or not at all                                                     | A narrow first-class record, optional per tenant                                                               |

## 5. 04. Rhythm 2.0 Design Constraints: The Existing Architecture, Assessed

## 04. Rhythm 2.0 Design Constraints: The Existing Architecture, Assessed

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence reports, each with
`path:line` citations: [r1 CRM](#21-r1-rhythm-2-0-crm-macroservice-what-a-households-relationships-family-membership-proposal-can-build-on), [r2 Membership](#22-rhythm-2-0-membership-what-exists-today-read-for-the-household-membership-proposal),
[r3 Finance, Commerce, Events, Marketing, Fundraising, Billing](#23-r3-how-the-money-handling-and-engagement-applications-identify-the-person-or-organization),
[r4 Platform](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships), [r5 Console and portal](#25-r5-console-and-portal-architecture-for-households-research-report),
[r6 Rhythm 1.0](#26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links).

**Baselines read.** services-crm `origin/master` `eee6a5f4` (2026-10-21 00:39 ET; the primary checkout `eac3df30` is 255
commits behind and several Part I statements are superseded, flagged below as _newer than Part I_); services-platform
`0614c54cf`; services-membership `fcbddf2`; services-accounting `17cca211` (services-finance is a symlink to it);
services-commerce `bd62910a`; react-console `13e4bcee9`; rhythm-web `5ecc208`; the Membership, Fundraising, Marketing,
Billing, Events, custom-objects, record-lifecycle and mobile proposals at the commits named in the reports.

**Labels.** **Implemented** (read in code, DDL or a named test), **Decided** (Andrew, dated, where recorded),
**Planned** (a proposal or milestone record, not built), **Absent** (nothing found), **Needed** (this document
introduces it).

### 1. The ten findings that shape the design

1. **Nothing household-shaped exists anywhere in Rhythm 2.0** (Implemented: nothing). A grep for household, family,
   spouse, dependent, guardian, minor, joint and beneficiary across every service, proposal and the console finds only
   `crm.contacts.spouse_id`, one paragraph in Part I 05 §5 (RHYTHM-CRM-ARCH-001 `05-functional-architecture.md`), the
   Fundraising proposal's spouse-pair soft-credit rule, and Marketing's "one STOP stops every contact on a shared
   phone". Rhythm 1.0 never had it either (r6 §0).
2. **Platform already has the relationship primitive.** `platform.association_types` / `platform.associations`
   (Implemented, services-platform master, last commit 2026-10-19 22:52 ET) relate any two registered records including
   contact to contact, with labels both ways, cardinality, dates, a primary flag, per-side delete rules, a merge
   participant, record history, a data-job import operation and automatic query exposure on every dataset. The
   custom-objects architecture (Decided, Andrew 2026-10-18, D-04) names associations "the model for every relationship a
   tenant defines, including contact-to-contact". Part I's CRM-table sketch for relationships predates this and does not
   mention it (r1 §4, r4 §1).
3. **Affiliations are the template for a member row, and are organization-specific in every reading path.** The dated,
   one-primary, overlap-excluded, role-carrying contact-to-organization row with linked contact information is exactly
   the shape a household member needs; but its organization id is an immutable composite FK to `crm.organizations`, its
   linked-information FKs name organization addresses, automatic roles key on "joins an organization", and the
   one-primary index spans all affiliations of a contact (r1 §2). Reuse the pattern, not the table.
4. **Every money party in every service is a closed two-value kind** `contact | organization`, enforced by about twenty
   CHECK constraints, two contracts packages, name views, Stripe mapping and aging snapshots (r3 §1, (a)). A household
   must not be a bill-to, payer, remitter, donor of record, credit owner, refund payee, method owner or buyer.
5. **Membership's owner is a two-column exclusive pair** (`owner_kind` `contact | organization`), and organizational
   memberships already cover people through **real child membership rows** (`origin` `flowdown | assigned`,
   `granted_by_membership_id`, mirrored terms, never billed, cascading suspension). Payer and authorized manager are
   absent for every kind today (r2 §2, §3, (a)).
6. **Polymorphic references are the ruled way to point at one of several record kinds** (`{ typeField, targets }`,
   Implemented 2026-10-18). Adding a target value is allowed; a household can become a membership holder target once
   `crm.households` is a registered object (r4 §12).
7. **The console's 360 is a JSON record view over a card registry, and the affiliation editor is a complete four-layer
   pattern** (card, section with counts and status filter, row, editor sheet with injected picker, explicit
   add/edit/end/remove directives) that a household members section can reuse. Organization 360 is bespoke and must move
   onto the record-view system anyway (r5 §2, §3).
8. **No delegated access exists.** Portal identity is one contact per login (`cid`); impersonation is deferred;
   management companies are deferred; the one reusable primitive is contact-keyed **derived assignments**
   (`platform.sync_derived_assignments`, sources `committees` and `eligibility` today) (r4 §4.4, r5 §7).
9. **Merge targets are contacts and organizations only**, discovered participants are two SQL functions per schema, and
   a coverage guard fails CI on any undeclared contact or organization id column. Membership and Accounting have no
   participant yet; the merge block (CRM-ADR-12) protects them (r1 §7, r2 §11, r3 §5).
10. **The query layer never joins to-many in Explorer**: a junction is reached by `collections` + `exists`, by a
    configured-relationship lateral with `LIMIT 1`, or by Platform's generated association families; counts belong on a
    `source` view as computed columns; symmetric aggregation is unbuilt (r4 §2).

### 2. CRM

| Area                                  | State                                                                                                                                                                                                                                                                                                                                                                                          | Household implication                                                                                                                                                                                 |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contacts and organizations            | **Implemented** per Part I 04 (RHYTHM-CRM-ARCH-001 `04-domain-and-data-model.md`). _Newer than Part I:_ archive columns and routes are built; released statuses (`inactive`, `deceased`, M1-D1) leave pickers and release the primary e-mail; organization history exists; the UUIDv7 check is still absent at the database on every record table (row X-03)                                   | `households` follows the organization table pattern; it is the first record table to carry the UUIDv7 check                                                                                           |
| Names                                 | **Implemented**: name parts; `name`, `full_name`, `name_last_first` computed in the database and server-owned; a stored `salutation` text; no addressee, informal name or household name anywhere (r1 §1.1)                                                                                                                                                                                    | Household name, mailing name, formal and informal salutations are **Needed**, computed in the service from members (multi-row), stored with overrides                                                 |
| Affiliations and affiliation types    | **Implemented** and accepted (2026-10-14): one primary per contact, open-ended primary, overlap exclusion (GiST over an expression key), immutable organization, roles with their own dates, 100 per contact, D-09 explicit add/edit/end/remove (_newer than Part I_), D-46 future-dated roles reconciled every five minutes (r1 §2)                                                           | Template for `household_members`: same constraint set, own table, two primaries (per household, per contact), no job title, roles as a single configuration reference rather than a nested collection |
| Contact to contact                    | **Implemented**: `spouse_id` only, reciprocal in code since D-19 (_newer than Part I_), interim by the debt note `docs/debt/spouse-relationship-model.md`; **Absent**: relationship tables, routes, query fields                                                                                                                                                                               | Spouse becomes a seeded association type; `spouse_id` is backfilled and retired ([A1 §2](#16-a1-household-lifecycle-rules))                                                                                      |
| Contact information                   | **Implemented**: typed addresses and phones, one preferred, seasonal month/day (E-06, _newer_), three e-mail slots with tenant-unique primary (now a constraint trigger ignoring released statuses), source modes `owned                                                                                                                                                                       | copied                                                                                                                                                                                                | linked                                                                                                                                                                                                                                                                                                                           | disconnected`(D-25, *newer*), effective views, type`applies_to` contacts/organizations/both refused on mismatch (M1-D8) | Household addresses and phones **Needed** (same shape as organization ones); `applies_to` gains `households`; a fifth link source on contact rows |
| Communication preferences and consent | **Implemented** per contact, communication type is free text; **Decided** (Andrew 2026-10-20, Marketing D-M2): CRM owns the consent ledger; design **Absent**                                                                                                                                                                                                                                  | Consent stays per contact; a household is an audience and a de-duplication rule, never a consenting party ([A3 §4](#18-a3-cross-application-integration))                                                          |
| Types and system rows                 | **Implemented**: contact, organization and affiliation types with configuration ids and layout slots; the `system_code` immutable-column pattern on activity types (delete refused, deactivate instead); baseline seed sets (D-48); `crm.protected_records` exists with no writer yet                                                                                                          | `household_types` and `household_roles` with `system_code`, seeded as seed sets; the first writer of `protected_records` if Andrew wants the system rows protected beyond "cannot delete"             |
| Custom fields                         | **Implemented** for contacts and organizations only (activity and contact-role value tables dropped 2026-10-19); Platform refuses any object not in `APPROVED_CUSTOM_FIELD_OBJECTS` (R2-ARCH-001, Decided)                                                                                                                                                                                     | Custom fields on households need Andrew's list change (HC-D5); without it, purpose-built screens                                                                                                      |
| Duplicates and merge                  | **Implemented**: duplicate rules and scoring (a shared address and a spouse pair are _negative_ evidence); merge engine with planner, plan hash, participants discovered from `pg_proc`, aliases and 410, coverage guard; object types closed to `contacts                                                                                                                                     | organizations`; CRM-ADR-12 merge block (Decided 2026-10-19)                                                                                                                                           | **Needed**: `households` as a third merge object type with a planner modelled on organizations, alias kind, reuse guard, registry entries for every `household_id` and member `contact_id`, guard pattern extension; household-aware warnings in contact merges; duplicate rules for households ([A1 §7](#16-a1-household-lifecycle-rules)) |
| 360 data                              | **Implemented**: the full record read backs the 360; affiliations list route; history routes; activity timeline; no summary route; cards from other applications through layout metadata (only Accounting contributes today)                                                                                                                                                                   | `GET /households/{t}/{id}` with resolved members and contact information; a members list route; history routes; other applications contribute cards later                                             |
| Queries and Explorer                  | **Implemented**: datasets for contacts, addresses, phones, affiliations, organizations, types, tasks, activities; `primary_organization` pick; per-affiliation-type configured relationships; `collections` for `exists`; `discovery` for defaults; `quickSearch` declaration; computed view columns (`contact_count`); grants generated from the manifests; Quick Search fed by change events | A `crm.households` dataset, `households_for_queries` view, `primary_household` pick and `households` collection on contacts, Quick Search type, recipes ([A3 §7](#18-a3-cross-application-integration))            |
| Audit and history                     | **Implemented**: `record_revisions` per record type, change-event streams, business events (only `crm.contact.merged` published), record locks, no-op saves write nothing                                                                                                                                                                                                                      | Household stream with history; member rows registered on both parents' streams; events `crm.household.*`                                                                                              |
| Lifecycle                             | **Implemented**: archive (_newer than Part I_); **Decided**: safe delete deferred to CRM-M20; today's hard delete cascades owned tables and clears `spouse_id`; Platform `apply_endpoint_deletion` called behind a switch                                                                                                                                                                      | Archive and dissolve on households; deletion-impact answers for member rows when CRM-M20 lands                                                                                                        |
| Module layout and API                 | **Implemented**: hand-wired modules, `/<resource>/{tenantId}` routes, PUT with `sys_version`, PATCH as JSON Patch, POST with client UUIDv7 and replay digest; **rule (2026-10-17)**: nothing new lands in `crm-entities-logic`                                                                                                                                                                 | A `households` module with its own stacks and `/households/{tenantId}` routes                                                                                                                         |

### 3. Membership

| Area                                 | State                                                                                                                                                                                                                                                                                                                                                    | Household implication                                                                                                                                                                            |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Programs, types, packages, terms     | **Implemented**: program → type → package (D-04); `member_kind` `contact                                                                                                                                                                                                                                                                                 | organization                                                                                                                                                                                     | either`; the package is the Accounting product; term engine with 74 spec cases; status derived, never stored | `member_kind` unchanged; a package gains coverage rules ([11 §4](#12-11-membership-coverage-and-benefit-inheritance-strategy)) |
| The membership record                | **Implemented**: `owner_kind` with exclusive `contact_id` / `organization_id` and a CHECK; identity `(program, owner[, unit])` (D-02); many memberships per contact across programs; exclusivity groups                                                                                                                                                  | Owner unchanged; `origin` gains `covered`; `billing_contact_id` for organization holders ([11](#12-11-membership-coverage-and-benefit-inheritance-strategy))                                               |
| Organizational memberships           | **Implemented**: flow-down by CRM roles (cascading to depth 10) and assigned seats from the decided application's `assigned_contacts`, each covered person a real child row with mirrored terms, never billed; no roster edit route; no billing contact, key contact or administrator on any membership; notices to organizations skipped `no_recipient` | The same machinery becomes the rule evaluator for every basis; a Membership-owned beneficiary table replaces "seats only through an application"; billing contact added for organization holders |
| Benefits and entitlements            | **Implemented**: `confers_benefits` on the type, grace policy; other apps read memberships directly, through the dataset, or (Decided, Absent) through Eligibility condition types Membership is to provide; no benefits catalogue; no age logic anywhere                                                                                                | Beneficiaries are real child membership rows, so every reader sees them; age rules evaluated by Membership against `crm.contacts.date_of_birth` at term start or continuously                    |
| Upgrade, downgrade, transfer, cancel | **Decided** (D-23, D-24, D-43), **Absent** in code (P4, blocked on Commerce amendments and Accounting M3); cancellation implemented to the contract                                                                                                                                                                                                      | Tier change mid-term and owner transfer are prerequisites for scenarios 6, 7 and the spin-off operation                                                                                          |
| Billing and renewals                 | **Implemented** to the ports (Commerce order placement is a refusing stub); `bill_to` passed through unread; payer ≠ member is Accounting's open D-10; auto-renew and billing runs built; renewal notices to the member's first e-mail                                                                                                                   | The holder (or the contact it names) is the invoice party; notices go to the billing contact when one is named                                                                                   |
| Portal                               | **Implemented**: `GET /memberships/{t}/me` read-only; officer "renew for others" via derived authority; CRM portal delegation dropped (Andrew 2026-10-08); **Decided**: portal last, Andrew builds components                                                                                                                                            | An adult beneficiary or a household manager renews through a package rule or a derived assignment; nothing before the portal milestone                                                           |
| Merge and delete participation       | **Absent**: no `merge_plan_*`/`merge_apply_*`, no delete-impact provider                                                                                                                                                                                                                                                                                 | Prerequisite for beneficiaries citing CRM facts: contact merges must re-point `contact_id` on beneficiary rows                                                                                   |
| State                                | Accepted 2026-10-18, acceptance cycle 1 "not ready", **stopped 2026-10-19 15:40 ET**; no R2-DEV-001 milestone record                                                                                                                                                                                                                                     | Phase 2 cannot start until Membership resumes under a milestone record                                                                                                                           |

### 4. Finance, Commerce, Billing

| Area                       | State                                                                                                                                                                                                                                                                                            | Household implication                                                                                                                   |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| Accounting customer        | **Implemented**: no customer table; every document carries `bill_to_kind/id/name`; ship-to is an address; no billing contact column on an organization bill-to (invoice e-mail to an organization is refused unless `to` is given); `customer_terms` per party; Stripe customer per contact only | Household never a bill-to; the buyer (holder or giver) is the bill-to; the household id travels in `custom_metadata` for reporting only |
| Payments, credits, refunds | **Implemented**: payer optional and offline-only; a payment allocates only to its own bill-to's invoices today; **Decided** (2026-10-19): one receipt, many customers (milestone #11, "decided, not planned"); credits never cross customers; refund payee to default to the payer               | A spouse paying another member's invoice is the one-receipt milestone's case, not a household feature                                   |
| Reporting                  | **Implemented**: aging and summary per `(kind, id)`; no hierarchy rollup; multi-currency per document and business unit                                                                                                                                                                          | Household 360 finance card = read-side sum of member summaries, grouped at query time; never a ledger dimension                         |
| Merge participant          | **Absent**; CRM's engine neither blocks on nor touches Accounting rows (policy O-10 is plan text)                                                                                                                                                                                                | Unchanged by households; noted as a risk                                                                                                |
| Commerce parties           | **Implemented**, **Decided** 2026-10-17: `buyer` (purchasing subject), `acting_contact_id`, `bill_to`, optional `ship_to`, per-line `recipient` ("purchased for", the entitlement holder); `PARTY_KINDS = contact                                                                                | organization`; promotions' `eligibility_subject`; checkout requires a buyer; bill-to options `self                                      | organization | parent_organization` with a remitter rule | Nothing changes: a gift membership is buyer ≠ recipient, already expressible; `PARTY_KINDS` stays closed |
| Billing (proposal)         | **Decided** 2026-10-20: own app and service; consolidation per payer with lines per beneficiary; statements to the bill-to or the organization's billing contact; stored methods with one owner; no billing account or responsible party                                                         | Family dues consolidate onto the payer's invoice with a line per beneficiary if the package prices per person                           |

### 5. Platform

| Area                             | State                                                                                                                                                                                                                                                                                                                 | Household implication                                                                                                                                       |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Entity registration              | **Implemented**: four owner manifests (metadata, queries, recipes, extensions and eligibility) plus Platform-side edits (`modules/queries/datasets.ts`, Quick Search record types, `LAYOUT_PURPOSES`); "Platform first" when a manifest key is newer than the deployed library                                        | The 20-step checklist in r4 (a) is the implementation checklist for [12](#13-12-migration-and-implementation-implications)                                                |
| Associations                     | **Implemented** (see §1 item 2); directional labels only (no symmetric flag), no attributes beyond dates, primary, position and notes (D-12), no `system_code`                                                                                                                                                        | Used for relationships; three small Platform additions requested ([14](#15-14-architecture-decisions-requiring-approval) HC-D1)                                           |
| Query layer                      | **Implemented**: datasets, picks proven single-row, configured relationships, `collections` + `exists`, generated association families, `count_distinct`; **Planned, unbuilt**: `rollup`, `measure`, aggregate request shape, relationship-aware criteria in the visual builder (E-17)                                | Household queries designed within what is built ([A3 §7](#18-a3-cross-application-integration))                                                                          |
| Forms and layouts                | **Implemented**: Platform stores opaque layout and form JSON; assignment on the type record; purposes gated by registry capabilities; 360 cards live in the console, not Platform                                                                                                                                     | Household layouts only with R2-ARCH-001 capabilities; otherwise purpose-built screens on the record-view engine                                             |
| Security                         | **Implemented**: three-segment keys declared in the owner, tiers, built-in policies, `resourceType` keys and `requireAccess` (used by Platform, not CRM; CRM-Q8 open); portal identity `cid`, one contact per portal user; derived assignments from `committees` and `eligibility`                                    | `crm.households.*` keys; a `crm` derivation source for Household Manager and Guardian roles at resource scope ([A4](#19-a4-security-privacy-and-authorization))  |
| Tenant isolation and lifecycle   | **Implemented**: RLS conventions; every tenant table declared under a participant; sandbox copy from a parent **Absent**; tenant data export foundation built, generation Planned, many-to-many exported as a link dataset                                                                                            | Household tables under CRM's `crm.entities` participant; export as link datasets                                                                            |
| Communications and notifications | **Implemented**: e-mail only; recipient is an address with optional contact; purposes are code in the shared package; consent is CRM's; Slack and Teams Planned; responsibilities grant no permission; attention never sends                                                                                          | Household notices resolve to member recipients ([A3 §4](#18-a3-cross-application-integration))                                                                           |
| Data Job Engine                  | **Implemented**: `defineDataOperation`, crosswalk, groups, validate-then-apply, narrow rollback; dependency waves from the Data Run manifest only; E-11 atomic cross-type groups **Planned, unbuilt**; bulk merge **Absent**; `platform.associations/upsert` exists (rejects, rather than blocks, a missing endpoint) | Operations for households and members; relationships through the associations operation after it learns to block ([12](#13-12-migration-and-implementation-implications)) |
| Eligibility                      | **Implemented**: grammar with `related` quantifiers and `crm.contact.age`; relationship paths know contact, organization and affiliation only; **PARKED** (Andrew 2026-10-19)                                                                                                                                         | No household path until resumed; Membership evaluates age rules itself in Phase 2                                                                           |
| Polymorphic references           | **Implemented** 2026-10-18 (see §1 item 6)                                                                                                                                                                                                                                                                            | Unchanged: the holder stays contact or organization                                                                                                         |

### 6. Digital experience

| Area                            | State                                                                                                                                                                                                                                                                                                                                                                                                    | Household implication                                                                                                                                                                       |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Console structure               | **Implemented**: domain registry navigation (CRM item "Relationships" marked planned), noun registry, one live switch, generated snake_case client converted at the transport                                                                                                                                                                                                                            | A Households domain item and noun; a Relationships card instead of a page                                                                                                                   |
| Contact 360                     | **Implemented**: JSON record view, `defineCard` registry, shared cards (`record.field-group`, `record.table`, `record.kpi-strip`, `record.audit`), closed panel registry, visibility rules; Platform layouts do not drive it (the designer saves to the browser); 14 tabs "not available yet" in live mode                                                                                               | Household and Relationships cards on Contact 360; `household-360.json` on the same engine ([A2](#17-a2-household-360-and-the-console-experience))                                                                  |
| Organization 360                | **Implemented, bespoke**, no edit form; **Decided**: must move onto the record-view system (Part I 09 §5)                                                                                                                                                                                                                                                                                                | Household 360 is built on the record-view engine from the start and becomes the template for the Organization 360 move                                                                      |
| Affiliation editing             | **Implemented**: card → section → row → editor sheet; organization-side Add Person dialog; explicit directives with version                                                                                                                                                                                                                                                                              | Reused as the household members section and the "Add household member" dialog                                                                                                               |
| Pickers, Quick Search, Explorer | **Implemented**: `RecordPicker` over `createPickerSource` (contacts dataset-backed; organizations still a 2,000-record browser list); Quick Search union `contacts                                                                                                                                                                                                                                       | organizations`; `defineRecipeExplorer`; one-row configuration toolbars                                                                                                                      | A dataset-backed household picker; a `households` Quick Search type; a Households explorer definition and recipes; `household_roles` and `household_types` configuration lists |
| Portal                          | **Implemented, held**: rhythm-web Web Experience Platform, account package, "Is this you?" linking, 20 written template screens including "organization roster, if I may manage them"; **Planned**: CRM-M22 last, PO-01..05 all Missing; organization-administrator rules already assumed by `web-contacts` (detach never deletes; cannot remove self; no route writes another member's personal record) | The household roster copies the organization roster rules; guardian edits of a minor are the one deliberate exception, under an explicit grant ([A4](#19-a4-security-privacy-and-authorization)) |
| Identity and acting-for         | **Implemented**: one Cognito portal pool per customer; one login ↔ one contact; "acting for an organization" is a session selection; staff impersonation deferred; no multi-contact switching                                                                                                                                                                                                            | A household manager acts under their own login with a derived role; a minor has no login; account switching stays deferred                                                                  |
| Mobile (proposal)               | **Planned**: reuses `cid`; one account per installation; membership card and wallet pass missing; no household mention                                                                                                                                                                                                                                                                                   | Beneficiary cards are a Membership and mobile concern after Phase 2                                                                                                                         |

### 7. Rhythm 1.0 precedent (what to keep and what to avoid)

Rhythm 1.0 had no household, family, dependent, guardian or minor concept, and no customer ticket in six years asked for
one by name (r6 §1.4, §7). What it had that bears on the design:

- **A one-way `spouse_id`** no form exposed, which leaked into query fields and portal opt-out screens (ZD 10243, ZD
  10599, ZD 11199). Avoid: a pointer nothing reads. The 2.0 migration carries it; this proposal retires it.
- **Generic relationships (2022)** built for Avectra imports, read only by the 360 tab, unqueryable, invisible in the
  portal; support told customers not to use them (ZD 9942). Only Fundraising consumed them, for **automatic soft credits
  by relationship type and direction** ("if either spouse makes a gift, the other spouse gets credit"). Keep: that
  consumer is the one proof of demand for typed, directional, reciprocal links; this proposal gives Fundraising the
  household as the recognition basis.
- **Membership covering several people was organization-only**: flow-down by role, assigned seats with a purchasable
  add-on, a portal "Manage member contacts" roster, and the same-organization manageability rule. The merge losses on
  those copied ids are e06 pattern P1. Keep the seat and roster vocabulary; never require the family to be an
  organization.
- **Events guests** were the place customers met spouses and children: a per-registration sub-record matched by e-mail
  or created, a years-long duplicate factory (RB-2568, RB-2697, RB-2838, RB-3480, RB-4251; 86 Zendesk guest tickets).
  Avoid: match-or-create by e-mail inside a sub-flow; every covered person is a real, de-duplicable contact.
- **Portal delegation (RB-2274)** for acting on behalf of someone outside your organization, with consent and a time
  limit, was designed in 2023 and never released. Keep the idea as the guardian authorization; deliver the flow with the
  schema, not two years apart.
- **Age** existed only as an eligibility rule on `date_of_birth`. Keep as the age source; add nothing else to the
  contact.

### 8. What this proposal must introduce

Summarised from §2–§6 (the per-milestone list is [12](#13-12-migration-and-implementation-implications)):

1. The `households` module in CRM: tables, aggregate, routes, metadata, queries, recipes, events, history, seeds, data
   operations, merge planner and registry entries, permission keys.
2. Relationship semantics over Platform associations: seeded system types, `relationship_type_settings`, the spouse
   backfill and retirement, duplicate-scoring and merge-warning rules, a Contact 360 card.
3. Household contact information and the fifth link source on contact addresses and phones.
4. Computed household naming with overrides and tenant formats.
5. Platform: datasets and Quick Search entries, a `crm` derivation source, association `symmetric` and `system_code`,
   audit entity types, a merge policy target.
6. Console: Households list, Household 360, cards, editors, picker, navigation, merge object type (needs Andrew's
   direction).
7. Membership: the beneficiary roster, coverage rules with designated and derived bases, the rule evaluator, the
   entitlement answer, billing contact for organization holders, age rules, spin-off; owner transfer (D-43)
   ([11](#12-11-membership-coverage-and-benefit-inheritance-strategy)).
8. Commerce and Accounting: nothing.
9. Fundraising, Marketing, Events: the household as recognition basis, audience and de-duplication rule, and attendee
   source, in their own plans ([A3](#18-a3-cross-application-integration)).
10. Portal: household roster and guardian flows, last, on Andrew's components
    ([A4](#19-a4-security-privacy-and-authorization)).

## 6. 05. Competitive Research

## 05. Competitive Research

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Six research reports hold every
source URL with its evidence label and the pages that could not be fetched: [w1 Blackbaud](#28-w1-how-blackbaud-products-model-households-family-relationships-and-family-memberships),
[w2 NetForum, iMIS and the first membership products](#29-w2-how-netforum-and-imis-model-households-family-relationships-and-family-memberships),
[w3 Salesforce NPSP and Nonprofit Cloud, Tessitura, the donor CRMs](#30-households-in-nonprofit-and-association-software-salesforce-npsp-salesforce-nonprofit-cloud-tessitura-and-eight-smaller-crms),
[w4 association management platforms](#31-w4-association-management-platforms-households-constituent-relationships-and-membership-coverage),
[w5 museum, zoo and admissions platforms](#32-w5-museum-zoo-aquarium-garden-and-cultural-institution-membership-platforms),
[w6 church, community, alumni and youth platforms](#33-w6-church-management-and-community-alumni-platforms-households-relationships-giving-units-guardian-authorization). Research dates
2026-10-21, public sources only; forty-one products.

**Labels.** _Verified_ = vendor documentation, a published API or database reference, or open-source code. _Reported_ =
community forums, idea portals, implementer material, customer manuals, review sites. _Inferred_ = our reasoning from
the above. _Unavailable_ = searched, not found. No internal database structure of a closed product is described unless
the vendor publishes it (Blackbaud CRM's database reference, Fonteva's and Wild Apricot's object references, Rock RMS's
source, NPSP's source, Tessitura's REST model help); otherwise findings describe functional behaviour only.

### 1. Why these products

The brief named the Blackbaud ecosystem, the association leaders and the museum and donor platforms, and asked for the
actual domain architecture where it can be established. The set was widened in three directions that matter for a
unified nonprofit platform: the association systems built on Salesforce and QuickBooks (Fonteva, Nimble AMS, Novi,
Impexium, Protech, GrowthZone, YourMembership, MemberSuite, Naylor, Glue Up, Wild Apricot); the admissions systems where
family memberships are the business (Altru's published database, Raiser's Edge NXT's membership API, Tessitura's REST
model, Doubleknot, Veevart, Versai, ACME, Gateway Galaxy, Siriusware, Spektrix, PatronManager, Centaman); and the
church, community and youth systems where families and guardianship are modelled most deliberately (Rock RMS from
source, Planning Center, Breeze, Realm, Church Community Builder, MinistryPlatform, FellowshipOne, Shelby, Elvanto,
Hivebrite, Graduway, Encompass, TeamSnap, SportsEngine, CampMinder, Jumbula, Sawyer). "Vectra" could not be found as a
membership product; the nearest names are Avectra (NetForum's former vendor) and Veevart, both covered.

### 2. Blackbaud: Raiser's Edge NXT and RE7, eTapestry, Convio and Luminate, Altru and Blackbaud CRM

**Raiser's Edge NXT and RE7 (Verified; Reported for complaints).** No household record. A household is two constituents
joined by a _spouse_ relationship with a _Head of Household_ flag on the relationship; children and others are typed
relationships, optionally non-constituents. Addresses can be live-linked; spousal soft credit is an opt-in per
relationship, not retroactive; addressee formulas have "Smart" joiners; mailings offer Head of Household processing that
works only when both spouses are constituents and both are selected. Memberships belong to one primary member ("gifts
cannot be linked to joint memberships"), with joint members and card recipients as relationships of the primary,
per-category caps, and a _gift of membership_ separating payer from recipient. The SKY Membership API (w5) shows the
shape today: `total_members` and `total_children` caps, card rows with `joint_member_id`, `relationship_id`, `card_type`
(Child), `address_to_print`, and `send_benefits_to` and `send_notice_to` ∈ {PrimaryMember, Donor}. Users report head of
household silently dropping people from mailings (idea RENXT-I-375, 338 votes, four years to the web view), couples
merged into one record to save licences, and shadow joint members that cannot be queried.

**eTapestry (Verified).** Accounts linked by a household relationship with one Primary; "accounts cannot be members of
multiple households" yet an account may be Primary of several; a Joint persona holds the couple's salutation; household
giving is a two-step query; a merge is refused across households; no membership module.

**Convio Common Ground, Luminate CRM, Luminate Online (Verified).** Common Ground (2008–2012) had a true household
account: every person a household contact under one household account, a required Head of Household, a Secondary
Contact, formal and casual salutation formulas with a manual-override mode, a Parent Account for multigenerational
rollups, a Household Soft Credit switch, and hard, soft and combined giving summaries at both levels; deleting a head
errored. Luminate CRM carried householding and household soft credits forward and was discontinued in 2022. Luminate
Online's Constituent360 never had households; TeamRaiser offers secondary registrants only.

**Altru and Blackbaud CRM (Verified, including the published database reference).** A household is a constituent with
`ISGROUP = 1` and `GROUPDATA.GROUPTYPECODE 0 = Household`; members are `GROUPMEMBER` rows (a bare pair with
`ISPRIMARY`), end-dated through `GROUPMEMBERDATERANGE`, with `GROUPMEMBERROLE`; spouses live in a separate
`RELATIONSHIP` table (`ISSPOUSE`) with no bridge to `GROUPMEMBER`; "an individual can belong to only one household";
constituent groups sit above households. Memberships: `MEMBERSHIP` has **no holder column**; the holder is
`MEMBER.ISPRIMARY`; the payer is `GIVENBYID` with `SENDRENEWALCODE`; children and unnamed guests are integers
(`NUMBEROFCHILDREN`, `NUMBEROFADDONGUESTS`); `MEMBER` rows carry `ISCAREGIVER` and `ISDROPPED`; `MEMBERSHIPCARD` hangs
only off `MEMBER`; `MEMBERSHIPTRANSACTION.UPGRADEMETHODCODE` distinguishes mid-term from renewal upgrades;
`HOUSEHOLDINFO` is a one-row table of six recognition-credit switches. Revenue never posts to a household ("households
cannot have the Donor constituency"); recognition credit flows by type and percentage. Dissolve with date and reason,
reinstate, previous members and a Deceasing Options page are first-class. Users report keeping children in notes "to
prevent online sales from adding an endless amount of people onto a membership".

### 3. Association management: NetForum, iMIS, Fonteva, Nimble AMS, MemberSuite, GrowthZone, YourMembership, Novi, Impexium, Protech, Naylor, Wild Apricot, Glue Up

**NetForum Enterprise (Verified).** Since 2015.1 a Household is a full transacting customer beside Individuals and
Organizations, created for "one piece of mail to the entire household", with its own accounting, membership and gift
tabs; exactly one active head of household is mandatory; the household's address "displays the address of the individual
who is designated as the head of household" (a 2017 SP1 defect when the head changes); family memberships work by
flow-down when both the member type and the relationship type are flagged; soft credits are automatic. No reciprocal
mechanism for individual relationships, no household salutation, no ageing-out, no documented household merge, no eWeb
household self-service. NetForum Pro has no household; benefits flow down over links to ten degrees.

**iMIS (Verified).** No household object in public documentation. Typed contact relationships (code, optional reciprocal
code producing paired records, optional dates, staff-only edits) and the organisation apparatus (one Primary
organization per person, list billing, Company Administrators who "cannot add existing contacts"). A family is modelled
as an organisation or not at all; soft credits are manual; merge discards the losing contact's communication
preferences; list billing "is not supported with AutoPay".

**Fonteva (Verified from the `OrderApi` object reference).** Subscriptions have `Entity` = Account or Contact,
`Assignments_Allowed` (quantity × maximum), Assignment and Assignment Role objects (`Is_Primary`), badge workflows for
"Assigned Contacts", badge-type flow-down "limited to only one level of child accounts", and a cancel that "inactivates
all related Subscribers". Connections and Connection Types are typed, dated, reciprocal relationship records that do
**not** drive benefits. Every person account is named "<Name> Household" by default, a label on a one-person account.

**Nimble AMS (Verified).** `NU__Affiliation__c` with one primary affiliation per person (the address flow-down rule
confirms individual-to-individual affiliations exist); membership flow-down copies Member Type and Member Thru onto the
child account on listed triggers only (no re-flow when the parent's own information changes; changing the flow-down
class does not revert prior writes); "one membership per account by default".

**MemberSuite (Verified, thin).** `ReceivesMemberBenefits` tri-state and a documented query through
`Individual.PrimaryOrganization.Membership`, the only product whose documented pattern evaluates coverage by query
rather than by copy; relationship semantics not public.

**GrowthZone (Verified).** Memberships "Owned By" an organization or individual, Full Inheritance versus Owner Only,
Related Contacts as hand-picked overrides, Location-only organization inheritance; person-to-person relationship types
are explicitly inert; billing contact receives an invoice that "belongs to the child".

**YourMembership (Verified metadata, Reported setup).** `MasterProfileID`, `SubAccounts`, `CanDetachSubAccounts`,
`MembershipEffectiveExpiresDate`; seats, detach and suspend from association guides; customers "can't pull sub-accounts
of ONLY current members with their master".

**Novi AMS (Verified).** A single-parent family tree of member records, three member-type categories, seat types with
per-seat dues and min/max, "Inheriting" and "membership via" (the one product beside Fonteva that can say why a person
is entitled), dependency between member types, parent-expiry demotion, and an anti-abuse rule that forbids freeing a
seat without removing the person.

**Impexium (Verified API).** Relationship operations and types with `reciprocalRelationshipName`, `isPrimary`, dates,
`canPurchaseForOrganization`, `canManageOrganization`; `MembershipData.inheritedMembershipBenefits` stored on the
individual's membership; the only product whose relationship _type_ carries behaviour.

**Protech UX 365 (Verified).** Contact-to-contact parents, "Use Parent Price Level" (copies the Member flag; stale when
toggled), Bill To Parent, and benefit recipients that "do not confer membership status or member pricing": two
entitlement systems that disagree.

**Naylor AMS, Glue Up (Reported, thin).** Firm-to-individual memberships without cascading updates; spaces per category.

**Wild Apricot (Verified OpenAPI and help).** Bundle membership levels (`MembershipLevel.Type`, `BundleMembersLimit`,
"Bundle ID", "Member role"): one price invoiced to the coordinator, one renewal date, status and level for everyone; the
coordinator cannot add an existing member and cannot remove one; members cannot change level while in a bundle.

**MemberClicks (Reported).** Organization member types with linked profiles and key contacts; attributes do not cascade.

### 4. Salesforce: NPSP and Nonprofit Cloud

**NPSP (Verified from open-source code).** Household = Account record type; `Contact.AccountId` means one household per
person (shared custody needs custom junctions). Naming is a format grammar with three per-contact exclusions, a
connector, an overrun word, an anonymous fallback and a pluggable class; "Refresh All Household Names" overwrites custom
names irreversibly; a 2025 known issue reverts them. Deceased sets Do Not Contact, excludes the person from name and
greetings, and re-picks the primary contact by highest giving. Addresses: one default per household, per-contact
override, seasonal month and day windows swapped by a scheduled job. Relationships are reciprocal pairs with
gender-resolved names and Current or Former status. Soft credit: saving a gift creates Household Member contact roles
for every other member; nightly rollups. Moving a contact re-points only the gifts where they hold the primary role and
regenerates soft credits; the standard Salesforce merge strands contacts in anonymous households.

**Nonprofit Cloud (Verified from Trailhead and the object reference).** Person Accounts; a household is a business
account classified by a Party Relationship Group (Category, Subtype, GroupSize, Status); members join through Account
Contact Relationship rows (`IsPrimaryMember`, `IsPrimaryGroup`, `IsIncludedInGroup`, family `Roles` multipicklist);
person-to-person links are two Contact Contact Relationship rows with inverse Party Role Relation types; guided merge
and split; "person accounts can belong to several party relationship groups at once"; soft credit is explicit data
(`GiftSoftCredit` rows, `GiftDefaultSoftCredit` rules). No naming generator and no address inheritance found. The
community recipe warns that fundraising rollups still assume one household per person and that a member link "must be
deleted, not just deactivated, because the Active checkbox on the ACR record does not impact the rollup calculation".
Salesforce's stated reason for the change (Reported): build on common Industries components and treat "every human as an
individual first".

### 5. Museums, zoos and admissions: Tessitura, Doubleknot, Veevart, Versai, ACME, Gateway Galaxy, Siriusware, Spektrix, PatronManager, Centaman

**Tessitura (Verified from the v15 REST model help; Reported for practice).** A household is a converted constituent
with A1 and A2 name slots; members attach through Affiliation records (primary flag, dates, own address, e-mail and
salutation per affiliation); primary affiliates may inherit the household address; salutations are stored records with
envelope and letter variants and an address-level override; seasonal availability is a twelve-character month string.
Coverage is **relationship-driven at read time** (`Memberships/Details?includeAffiliations`,
`BenefitProvidingConstituentId`, constituencies cascading through affiliations); the payer is `CrediteeId`. Memberships
live on the household or the individual, a permanent source of friction: "Move Transactions to Household" and "Convert
to Household" tooling, stray orders after merges, a third adult that does not fit, TNEW logins on the individual that
cannot see household-owned renewals; v12 added Owner, Initiator and Recipient on transactions, at the cost of
double-counting pitfalls; v16 "will change significantly".

**Doubleknot (Verified and Reported).** Memberships with member relationships and cardholders; merges delete Member
Relationships and Contact Notes.

**Veevart (Verified).** Salesforce-based; family caps (`Number of Members Allowed`, children count,
`Maximum Age for Children` default 17, birth date versus checkbox validation) enforced online only; beneficiaries
pre-filled from the Salesforce household and editable up to the cap.

**ACME (Reported; portal login-walled).** `cardholders[]` with primary and secondary, contact-less "Guest of X" cards,
one primary must stay active, per-level channel policies.

**Gateway Galaxy, Centaman, Versai (Reported).** Joint memberships, photo-validated passes, deferred entitlements;
"household memberships with individual identity"; giver and recipient card choice.

**Siriusware (Verified manuals).** No household; a family is guest records linked to a primary guest, one pass per
person created by typed modifiers (ADULT, CHILD, GUEST caps via macros), shared counters via parent-pass validation, and
gate resolution by cascading validation over the guest's passes.

**Spektrix (Verified).** Households for address and analysis only; memberships strictly one per customer; joint
membership is a documented gap handled with soft credit and tags.

**PatronManager (Verified).** A Household-type Benefit covers every Contact in the Account, `Member Role` Main or
Associated, nightly recompute, and explicitly "cannot limit the number of Contacts, or specify adults versus children,
or track names".

### 6. Churches, communities, alumni and youth: Rock RMS, Planning Center, Breeze, Realm, CCB, MinistryPlatform, FellowshipOne, Shelby, Elvanto, Hivebrite, Graduway, Encompass, TeamSnap, SportsEngine, CampMinder, Jumbula, Sawyer

**Rock RMS (Verified from source).** A family is a Group of GroupType Family with GroupTypeRoles Adult and Child; the
group owns addresses, campus, record status cascades and two computed salutations; giving is **not** owned by the family
but by `Person.GivingGroupId` → `GivingId` ("G{groupId}" or "P{personId}", set for adults only at family creation,
re-pointed when a person is removed from other families); a person may be in several families with `PrimaryFamilyId`
computed from `GroupOrder`; spouse is **derived** (two Adults, both Married, optionally opposite gender); **Known
Relationships** are a second GroupType (a hidden owner group per person; roles with an `InverseRelationship` attribute;
automatic inverse create and delete; Previous Spouse, adult Child, Sibling, Business); check-in authority is the
explicit directional pair Allow Check In By ↔ Can Check In; `AgeClassification` at 18 drives family role by job; the old
Implied Relationships group type was retired in v17 for a dedicated Peer Network feature. Documented compromises:
dual-family children double family-based report rows and mailings; the giving group does not follow a person added to a
second family.

**Planning Center (Verified API).** Households with `HouseholdMembership.household_role` (adult, child_or_dependent,
other_adult, parent_guardian) and a primary contact; a person in several households; check-in authority = household
adults plus Trusted People minus Not Authorized; joint donors are pairwise and household-independent.

**MinistryPlatform (Verified help).** Household with six fixed Household Positions, dated "Other Households", Contact
Relationships (trigger-maintained reciprocal, Start and End Date, admin-defined types), Donor Statement Type Individual
or Family, Other Households counting for check-in only when the Household Type matches.

**Realm (Verified and Reported).** Family positions classed Primary, Child, Other with custom labels; children may be in
two families and are pruned to the primary on adulthood; Relationships added in November 2022; shared giving is a
pairwise check box.

**Church Community Builder (Verified).** Scalar `familyId` (one family per person), exactly one `PRIMARY_CONTACT`,
`authorized_individuals` plus an Age Threshold restricting child-profile visibility.

**FellowshipOne (Verified).** Scalar household, Head/Spouse/Child/Other/Visitor; a gift "Attributed to Household" is
shared 50/50 between Head and Spouse and then "cannot be split between households": the one true household-owned gift in
the set, and its own documentation records the cost.

**Breeze, Elvanto, Shelby (Verified and Reported).** One family per person ("a person can only be part of one family at
a time"); relationships overloaded onto family roles ("Grandfather of the primary contact"); Shelby Arena's
`giving_unit_id` with "Contribute Individually" that must be set on both spouses.

**Alumni (Hivebrite, Graduway, Encompass; Unavailable or thin).** No public household model.

**Youth, sports and camps (Verified).** The adult account is the root; children are login-less sub-profiles; guardians
are invited to a child's profile with a pending state and constrained removal; a waiver is acknowledged per participant
or per registration by the identity-matched account holder (TeamSnap admits its waiver is "more of an acknowledgement
than an electronic signature"); CampMinder keeps a primary household that pays and an optional second household for mail
and invoices.

### 7. The ten questions, answered across the market

| Question                                                                      | What the market does                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1. Household: entity, constituent type, organization, group or relationships? | **Where it exists it is a party-like record people link to**: a group constituent (Altru), a group of a family type (Rock), a dedicated household (NetForum, Planning Center, MinistryPlatform, FellowshipOne, CCB), an account (NPSP, Nonprofit Cloud, PatronManager, Veevart), a converted constituent (Tessitura), a contact type (CiviCRM). Raiser's Edge, eTapestry, iMIS and **every association platform in w4** have no household entity; families are seats or parent links |
| 2. Can a household own a membership?                                          | NetForum yes (a full customer); Tessitura either (with permanent friction); PatronManager through the Account; Altru's membership has no holder column at all (holder is a flag on a member row); everywhere else **no**, the holder is a person or organization with named others                                                                                                                                                                                                   |
| 3. Can an individual cover several people without a household?                | Yes almost everywhere that sells family memberships: Altru and RE (members and cards), Fonteva and Novi and Wild Apricot and YourMembership (seats), Veevart, ACME, Siriusware; **no** in Spektrix (one per customer) and PatronManager (container only)                                                                                                                                                                                                                             |
| 4. Spouses, children, guardians, other relationships                          | A separate typed, dated, reciprocal structure in the mature products (Altru `RELATIONSHIP`, Rock Known Relationships, MinistryPlatform, Realm, NPSP pairs, Nonprofit Cloud CCRs, Fonteva Connections, Impexium); derived or overloaded onto family roles in the rest (Rock's spouse, Breeze, Elvanto); never derived from household membership automatically                                                                                                                         |
| 5. Adding and removing members                                                | Procedures and wizards (Move to new family, Split Household, Separate Household, Manage Household); dated ends in Altru, MinistryPlatform, NetForum; one-family-per-person systems push custody into check-in lists and "Visitor" roles                                                                                                                                                                                                                                              |
| 6. Beneficiary identification                                                 | Four models: **materialised member or cardholder rows** (Altru, RE, ACME, Siriusware, Veevart, Fonteva, Novi, Nimble, Protech), **whole-container** (PatronManager), **relationship lookup at read time** (Tessitura, MemberSuite by query), **strictly individual** (Spektrix). Children and unnamed guests are integer slots in Altru, RE and Siriusware; only Veevart and Siriusware test age at sale; no public doc shows mid-term ageing out                                    |
| 7. How benefits flow                                                          | Copied fields or flags with weak provenance (Nimble, Protech, GrowthZone, YourMembership, Impexium); explicit assignment chains in Fonteva and Novi ("membership via"); the gate always resolves a person-level token                                                                                                                                                                                                                                                                |
| 8. Dynamic eligibility through relationships                                  | Tessitura (affiliations), NetForum and NetForum Pro (flagged flow-down), CiviCRM (non-transitive inheritance), PatronManager (container): each with documented inheritance holes; everyone else materialises                                                                                                                                                                                                                                                                         |
| 9. Giving, communications, addresses, history                                 | Giving always on a person; the household sees it through soft or recognition credit (Blackbaud, NPSP, Nonprofit Cloud, NetForum) or a giving-unit pointer (Rock, Arena, MinistryPlatform); one addressee and salutation per household, generated with overrides; a shared default address with per-person override and seasonal alternates; FellowshipOne's household-owned gift is the exception that proves the rule                                                               |
| 10. Limitations and compromises                                               | One household per person; holder as a flag; nightly rollups; names regenerated over overrides; household address as a copy of one member's; dual flags for flow-down; integer children; shadow cardholders; merge without undo or that drops relationships; single-parent coverage links that cannot express employer plus household; two entitlement systems that disagree; no "why is this person entitled" output anywhere                                                        |

### 8. Evidence table

Per product, the strongest evidence grade available for the ten questions (V Verified, R Reported, I Inferred, U
Unavailable). The per-question grades are in each report's own table (w2 §6, w4, w5 §17, w6 §17).

| Product                                             | Household shape | Household owns membership | Individual covers others | Relationships  | Add and remove | Beneficiaries | Benefit flow | Dynamic eligibility | Giving, comms, address | Limitations |
| --------------------------------------------------- | --------------- | ------------------------- | ------------------------ | -------------- | -------------- | ------------- | ------------ | ------------------- | ---------------------- | ----------- |
| Raiser's Edge NXT and RE7                           | V               | V (no)                    | V                        | V              | V              | V             | V            | I                   | V                      | R           |
| eTapestry                                           | V               | U (no module)             | U                        | V              | V              | U             | U            | U                   | V                      | R           |
| Convio Common Ground                                | V               | U                         | U                        | V              | V              | U             | U            | U                   | V                      | V           |
| Luminate CRM and Online                             | V               | U                         | U                        | V              | U              | U             | U            | U                   | V                      | R           |
| Altru and Blackbaud CRM                             | V (DB)          | I (no)                    | V                        | V (DB)         | V              | V (DB)        | V            | V (no)              | V                      | V           |
| NetForum Enterprise                                 | V               | V (yes)                   | V                        | V              | V              | V             | V            | V (flagged)         | V                      | V           |
| NetForum Pro                                        | V               | V (no)                    | V                        | U              | U              | V             | V            | V                   | U                      | V           |
| iMIS                                                | V (none)        | V (no)                    | I                        | V              | V              | I             | V            | V (no)              | V                      | V           |
| Fonteva                                             | V (none)        | V (no)                    | V                        | V              | V              | V             | V            | V (no)              | V / U                  | V           |
| Nimble AMS                                          | U / I           | I                         | V                        | V / I          | V              | V             | V            | V (no)              | V / U                  | V           |
| MemberSuite                                         | U               | U                         | U                        | R              | U              | V / I         | V            | V                   | U                      | V / I       |
| GrowthZone                                          | V (none)        | V                         | U                        | V              | V              | V             | V            | V                   | V / U                  | V           |
| YourMembership                                      | V / R           | R                         | R / I                    | U              | R / V          | R             | V / I        | I                   | U                      | R           |
| Novi AMS                                            | V (tree)        | I                         | V                        | V              | V              | V             | V            | V (partial)         | V / U                  | V           |
| Impexium                                            | V (none)        | U                         | U                        | V              | V / R          | V / R         | V / I        | I                   | V / U                  | V / I       |
| Protech UX 365                                      | V (none)        | I                         | V                        | V              | V              | V             | V            | V (no)              | V                      | V           |
| Naylor AMS                                          | U               | U                         | U                        | U              | U              | I             | I            | U                   | U                      | R           |
| Wild Apricot                                        | V               | V                         | V                        | V (none)       | V              | V             | V            | V (no)              | V                      | V / R       |
| Glue Up                                             | R               | R                         | R                        | U              | R              | R             | R            | U                   | U                      | R           |
| MemberClicks                                        | R               | R                         | R                        | U              | R              | R             | R            | U                   | U                      | R           |
| Salesforce NPSP                                     | V (code)        | V                         | V                        | V (code)       | V              | V             | V            | V                   | V                      | V / R       |
| Salesforce Nonprofit Cloud                          | V               | U                         | U                        | V              | V              | V             | V            | V                   | V / U                  | R           |
| Tessitura                                           | V               | I                         | I (no)                   | V              | V              | V             | V            | V (yes)             | V                      | V / R       |
| Doubleknot                                          | V / R           | U                         | I                        | V              | U              | I             | V            | U                   | V                      | V           |
| Versai                                              | U               | U                         | U                        | U              | U              | I             | V            | U                   | V                      | I           |
| Veevart                                             | R               | U                         | V                        | V              | V              | V             | V / R        | I                   | R                      | V           |
| ACME                                                | R               | U                         | R                        | R              | R              | R             | R            | R (no)              | U                      | R           |
| Gateway Galaxy                                      | U               | U                         | V                        | U              | V              | V / I         | V            | U                   | V                      | I           |
| Siriusware                                          | V (none)        | U                         | V                        | V (none typed) | V              | V             | V            | V (partial)         | R                      | V           |
| Spektrix                                            | V               | V (no)                    | V (no)                   | V              | V              | V (none)      | V            | V (no)              | V                      | V           |
| PatronManager                                       | V               | V (yes)                   | V (no)                   | V              | V              | V             | V            | V (yes)             | V                      | V           |
| Centaman                                            | V (headline)    | U                         | U                        | U              | U              | I             | V            | U                   | V                      | I           |
| Neon CRM                                            | V               | V (no)                    | R                        | R              | R              | R             | R            | U                   | V                      | R           |
| Bloomerang                                          | V               | V (no)                    | V (no)                   | V              | V              | V             | V            | V (no)              | V                      | V           |
| Little Green Light                                  | V               | n/a                       | V                        | V              | V              | U             | U            | U                   | V                      | V           |
| Virtuous                                            | V               | V (yes, as the contact)   | n/a                      | V              | V              | U             | U            | U                   | V                      | R           |
| DonorPerfect                                        | V               | n/a                       | U                        | V              | V              | U             | U            | U                   | V                      | U           |
| CiviCRM                                             | V               | V (yes)                   | V                        | V              | V              | V             | V            | V (yes)             | V                      | V           |
| Kindful, Keela                                      | R / V           | U                         | U                        | U              | U              | U             | U            | U                   | R                      | R           |
| Rock RMS                                            | V (code)        | V                         | n/a                      | V (code)       | V              | n/a           | n/a          | V                   | V                      | V / R       |
| Planning Center                                     | V (API)         | V                         | n/a                      | V              | V              | n/a           | n/a          | V (absent)          | V                      | V / I       |
| MinistryPlatform                                    | V               | V                         | n/a                      | V              | V              | n/a           | n/a          | V                   | V                      | V / I       |
| Realm, CCB, FellowshipOne                           | V               | V                         | n/a                      | V / R          | V / R          | n/a           | n/a          | V / U               | V / R                  | V / R       |
| Breeze, Elvanto, Shelby                             | V / R           | V / U                     | n/a                      | V / R          | R              | n/a           | n/a          | U                   | R                      | R           |
| Hivebrite, Graduway, Encompass                      | U / I           | U                         | U                        | U              | U              | U             | U            | U                   | U                      | R / U       |
| TeamSnap, SportsEngine, CampMinder, Jumbula, Sawyer | V               | V                         | V                        | V              | V              | n/a           | n/a          | V                   | V (payments)           | V / U       |

### 9. Patterns that persisted across generations

1. **A household, where it exists, is a party-like record that people link to**, never a flag on a person and never
   derived from relationships: Common Ground 2008, Altru, NetForum 2015, NPSP, Nonprofit Cloud 2023, Rock, Planning
   Center, MinistryPlatform. The products without one (Raiser's Edge, eTapestry, iMIS, every Salesforce and QuickBooks
   association platform) approximate families with seats, parent links or spouse flags, and their complaint lists are
   the longest.
2. **Kinship lives in a second, typed, dated, reciprocal structure** wherever it is modelled at all (Altru
   `RELATIONSHIP`, Rock Known Relationships with inverse roles, MinistryPlatform Contact Relationships, Realm, NPSP
   pairs, Nonprofit Cloud CCRs, Fonteva Connections, Impexium). Household roles are _position in the household_ (adult,
   child, dependent, other adult, guardian), never kinship. Rock retired its implied-relationship group type because
   inferred connections do not belong beside declared ones.
3. **Exactly one primary per household**, chosen or computed, whose only job is to answer who gets the one piece of
   mail, the one receipt, the one renewal; and, in the systems that allow several households per person, **one primary
   household per person** (Rock's `GroupOrder`, Nonprofit Cloud's `IsPrimaryGroup`, Realm's forced choice,
   MinistryPlatform's dated Other Households).
4. **Generated addressee and salutation with manual override and per-person exclusion**, never regenerated over an
   override without an explicit choice (NPSP's Refresh All and 2025 revert bug are the cautionary cases).
5. **Money stays on a person; the household sees it through a derived credit or a giving-unit pointer** (soft credit,
   recognition credit, `GivingGroupId`). FellowshipOne's household-owned gift and NetForum's household customer are the
   exceptions, and both document the cost.
6. **Family membership = one holder plus explicitly named covered people, with caps per level**, cards per named person,
   a shared guest pool, add-ons for extras, and spin-off and transfer operations (Altru, Raiser's Edge, Veevart, ACME,
   Siriusware, Fonteva, Novi). Named coverage is universal; only seat counts represent unnamed capacity, and only as
   slots.
7. **Payer separated from holder and primary** (`GIVENBYID`, `given_by_id`, `CrediteeId`, Bill To Parent, default
   billable party, gift of membership).
8. **A shared household address with per-person override and seasonal alternates**, by link (RE7, NPSP, Tessitura
   primary affiliates) or by copy (Altru); the copy goes stale, the unowned link propagates a divorce.
9. **Guardian and check-in authority is an explicit, directional grant list on top of household membership**, with
   negative entries (Rock Allow Check In By ↔ Can Check In; Planning Center Trusted People and Not Authorized; CCB
   `authorized_individuals` and Age Threshold; the youth systems' invited guardians with pending state).
10. **Life events are procedures** (Move to new family, Split Household, Dissolve, Deceasing Options), and every forum
    thread about divorce, adult children and death is about the giving unit, the primary or the address not following
    the person.
11. **Coverage materialised as rows beats evaluation at read time**: the three read-time systems (Tessitura, CiviCRM,
    NetForum flow-down flags) carry the documented inheritance holes; the materialising systems' defect is missing
    provenance, which only Novi and Fonteva's assignment chain supply.
12. **No product returns "why is this person entitled"**; the closest are Siriusware's ordered pass list, Altru's card
    print view, Novi's "membership via" and Fonteva's badge chain.

### 10. Weaknesses worth avoiding

- One household or one family per person hard-wired into the person record (NPSP, Bloomerang, Altru, eTapestry, Breeze,
  CCB, FellowshipOne, Elvanto; Nonprofit Cloud's and Rock's rollups).
- A head-of-household flag instead of a record (Raiser's Edge); a holder identified only by a flag on a member row
  (Altru `MEMBER.ISPRIMARY`, ACME's one-primary rule).
- Single-valued coverage links (one primary affiliation, one parent, one master, one bundle) that cannot express
  employer plus household (every association platform in w4).
- Copied member fields with a trigger model that misses changes (Nimble, Protech) and two entitlement systems that
  disagree (Protech member flag versus benefit recipients; Fonteva materialised versus text badges; GrowthZone
  inheritance versus Related Contacts).
- Entitlement without provenance (Nimble, Protech, GrowthZone, YourMembership, Impexium); nightly recomputation
  (PatronManager, NPSP); whole-container coverage without caps or names (PatronManager).
- Children and guests as bare integers (Altru, Raiser's Edge, Siriusware); cardholders without a person record (ACME
  "Guest of X"; Raiser's Edge joint members).
- Deriving spouse from marital status and gender (Rock); relationships overloaded onto family roles relative to a hub
  (Elvanto, Breeze); reciprocity maintained by convention or a second save (iMIS, Rock's two-step role setup,
  MinistryPlatform's "repeat on the other contact").
- Generated names overwriting overrides (NPSP); the household address as a copy of one member's (NetForum) or an unowned
  live link (RE7); family-level cascades without guards (Rock's inactivate-all, campus-at-family).
- Letting the household own gifts (FellowshipOne) or transact in parallel with its members (Tessitura, CiviCRM,
  NetForum): three records per couple, stray transactions, "move transactions" chores.
- A giving unit that does not follow the person through adulthood, divorce or death (Rock, Arena, ShelbyNext).
- Merges refused across households (eTapestry), without undo (NetForum), discarding preferences (iMIS), dropping
  relationships (Doubleknot) or stranding people in anonymous households (standard Salesforce merge).
- Household type doubling as a feature switch (MinistryPlatform's check-in gate); a checkbox treated as consent
  (TeamSnap); portal rights by family position without a child-safety floor.
- Composition rules as scripts (Siriusware macros) and anti-abuse rules as UI prohibitions (Novi's seat rule), because
  the model has no history of the change.

### 11. What Rhythm can do that none of them does

1. Many-to-many household membership with an explicit, dated primary flag on the row (not a sort order), so shared
   custody and carers need no junction, and reports have a "count once by primary household" mode from day one.
2. Relationships and household membership as two first-class structures on existing primitives (Platform associations; a
   CRM member table), each with consumers on day one.
3. Coverage as explicit beneficiary rows carrying their basis (a designation, or the relationship, household membership
   or affiliation that a rule read), produced by one evaluator for families and employers, and an `/entitlements` answer
   that says why: the thing no product in the set returns.
4. Age, caps and composition rules as data on the package, so museums stop keeping children in notes.
5. A household-owned address linked by reference, so a move is one edit and a departure is one explicit disconnection.
6. Authority as dated, derived grants with a child-safety floor, matching the church and youth systems and exceeding the
   association market.
7. Dissolve, reinstate, merge, split, spin-off and transfer as first-class operations with history.
8. One API for staff and self-service, so the portal never lags the console by a product generation.

## 7. 06. Cross-Industry Requirements and Use Cases

## 06. Cross-Industry Requirements and Use Cases

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Across the brief's scenarios,
relationships, households and membership coverage serve distinct purposes: relationships describe connections between
people; households group people for service, giving and communications; explicit beneficiary rows confer membership
coverage.

Each scenario is tested against the candidate models in chapter [07](#8-07-alternative-architecture-models): **R** =
restored person-to-person relationships, **H** = a first-class household record, **O** = household as an organization of
a configurable type, **B** = explicit membership beneficiaries with package rules, **F** = organization flow-down as it
exists today. The segment notes draw on the competitive chapters ([05](#6-05-competitive-research)) and the church and
museum research (evidence [w5](#32-w5-museum-zoo-aquarium-garden-and-cultural-institution-membership-platforms), [w6](#33-w6-church-management-and-community-alumni-platforms-households-relationships-giving-units-guardian-authorization)).

### 0. The requirements the scenarios share

| Id  | Requirement                                                                                                                                                                           | Segments that need it                                                                          |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| R1  | Record typed, dated, both-way facts between two people (spouse, parent, guardian, grandparent, sibling, caregiver, tenant-defined), queryable and visible                             | All                                                                                            |
| R2  | A stable mailing and recognition unit: name, addressee, salutations, shared address, primary member, membership list, history, merge and split                                        | Museums, churches, fundraising, alumni, family clubs; not foundations or trade associations    |
| R3  | One person in more than one unit (shared custody, adult children, carers), with one default                                                                                           | Churches, museums, charities                                                                   |
| R4  | Memberships that cover people other than the holder: named adults, dependents by age, people elsewhere, caregivers, named guests, employees by role, designees; capped, dated, priced | Museums, zoos, clubs, professional societies (spouse and student programs), trade associations |
| R5  | "Is this person entitled under this program, and why" answered the same way for individual, family and organizational memberships                                                     | All with Membership                                                                            |
| R6  | Money stays on a legal party: payer, bill-to, donor of record; household totals and recognition on top                                                                                | All                                                                                            |
| R7  | Authority is explicit: a parent acts for a child, a manager for a family, never by membership alone                                                                                   | All with a portal                                                                              |
| R8  | Life events are operations with history: marriage, separation, death, aging out, moving, merging duplicates                                                                           | All                                                                                            |
| R9  | Everything above is configuration where tenants differ (types, roles, naming, rules), not code                                                                                        | All                                                                                            |

### 1. Museum family membership

_Jane purchases a family membership covering herself, her spouse and three children._

| Question                               | Answer under the recommended hybrid (R + H + B)                                                                                                                                                                                                                                                   | Under O + F                                                                                                                                                 | Under R + B, no household                                |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Does this require a household record?  | **No.** Jane holds the membership; her spouse is a beneficiary under the `adult` rule (justified by `spouse_of` or by household membership), the children under the `dependent` rule. A household makes the proposal one click and gives the family a mailing name; it is not a precondition      | Yes: the family must be an organization for flow-down to cover anyone                                                                                       | No; Jane names them                                      |
| A child turns 18                       | `term_start` basis: covered to the term's end, dropped at renewal with the reason; `continuous`: the row ends on the birthday with notice; the child is offered the program's young-adult package                                                                                                 | Flow-down has no age rule; staff end the affiliation or the child stays covered                                                                             | Same as recommended                                      |
| Jane and her spouse separate           | The spouse is removed (or the `follows_predicate` rule ends the row); the spouse may spin off into their own membership; the children stay covered because the rows name them; the household ends the spouse's member row and relationship; two households, children in both if custody is shared | The spouse's affiliation ends; children's coverage depends on their affiliation to the "household organization"; a second household organization is created | Same as recommended, without a mailing unit to split     |
| A child lives in a different household | Irrelevant to coverage (the row names the child); relevant to mail (the child's primary household is the other one)                                                                                                                                                                               | The child needs an affiliation to both household organizations; neither can be primary if a parent's employer is                                            | Not representable except through the child's own address |

Museum practice (Altru, Tessitura, Doubleknot, ACME) confirms this pattern: a person holds the membership, with named
others covered under per-level caps. Each named person receives a card; the membership has a shared guest pool and
add-ons for extras ([05 §5, §8](#6-05-competitive-research)).

### 2. Zoo grandparent membership

_Two grandparents purchase a membership covering themselves and several grandchildren who do not live with them._

A household-based model does **not** support this naturally. The grandchildren live in their parents' households. Moving
them into the grandparents' household solely to provide membership coverage corrupts the mailing unit: two addresses and
two families become one record.

Direct relationships are better suited to this case. One grandparent is the holder, the other is an `adult` beneficiary,
and each grandchild is a `dependent` beneficiary justified by `grandchild_of`, or simply designated when the
relationship is not recorded. The children's own households remain untouched.

Under O + F, the zoo would have to create a "Grandparents" organization and affiliate children who live elsewhere. This
is exactly the iMIS workaround whose constraints the market complains about.

### 3. Corporate association membership

_Acme Corporation holds an organizational membership that provides benefits to qualifying employees._

- **How are employees identified?** The derivation rule `affiliated_to_holder` identifies them using the configured
  contact roles and organization types, cascading to subsidiaries to the configured depth. This is the mechanism
  services-membership runs today for flow-down, restated as a coverage rule. Designated seats, such as a package that
  sells ten named employees, use the `designee` rule with `max_count` and an add-on seat product. Today's application
  `assigned_contacts` seeds those seats.
- **How do benefits change when they leave Acme?** The affiliation ends in CRM, and the change event re-evaluates the
  rule, as flow-down does today. The beneficiary row ends with `predicate_ended`; the child membership ends the same
  day. The entitlement answer for that person changes from "beneficiary of Acme's membership, basis affiliation X" to
  "none".
- **Can the same general-purpose mechanism support family beneficiaries?** Yes. A family package uses the same row with
  a different predicate, `household_member_of_holder` or `related_to_holder`, and defaults to designation rather than
  derivation. There is one evaluator, one row shape and one entitlement answer. Only the predicate and the method of
  selecting people differ: a person chooses the set, or a rule does.

### 4. Church household management

_A church maintains families and households for giving, communications, pastoral care and participation, and needs
family relationships including people who no longer live together._

Modelling households as organizations does not work naturally here. A congregation has thousands of families and a
handful of real organizations, such as schools and partner charities. Families would dominate every organization list,
picker and report. Pastoral "known relationships" also extend beyond shared residence: a divorced father may still bring
the children, and an adult child who moved to another city may still be "family". These are relationships, not
organization affiliations. Churches do not sell memberships, so flow-down, the one reuse O offers, is irrelevant.

Relationships between contacts provide the foundation for care and history. Households provide the foundation for giving
units, statements, directories and mailings. Church systems built deliberately for this distinguish the two: Rock RMS
models Family as a group with Adult and Child roles **plus** Known Relationships as a separate structure; Planning
Center and Ministry Platform use households with positions, plus relationships. These systems confirm that both
structures are needed and that they serve different purposes (evidence w6).

The recommended hybrid gives the church households with roles and a primary, alongside relationships of any type with
dates. "Formerly married" and "lives elsewhere" remain facts rather than becoming deletions. The household supplies a
giving unit for statements and one mailing per household with the family salutation. Guardian authorization supports
children's check-in and records.

### 5. Fundraising and donor management

_Recognise individual giving, calculate household giving totals, send one communication per household without losing
individual records._

- **Individual giving** stays on the donor of record. Fundraising's gift model is unchanged.
- **Household totals** sum gifts by people who were members of the household **as of each gift date**, de-duplicated by
  gift id and served on a household-grain view. Recognition credits are explicit rows written at recording time under
  the tenant's rule, never regenerated. This requires the household record as a stable key. The spouse-pair key planned
  in the Fundraising proposal works for exactly one pair and breaks on the first blended family.
- **One communication per household** uses the audience rule "once per household, to the primary contact, else the next
  mailable adult", addressed with the household's mailing name and salutation. Each member's consent and suppression
  still apply individually. A household whose members are all suppressed receives nothing.
- **Without losing individual records** means nothing is merged. The household groups intact contacts.

R + B without a household provides no stable unit for either the total or the single mailing. Under O, the household is
also an organization donor-of-record candidate, which no fundraising shop wants.

### 6. Foundation and NGO relationships

_Beneficiaries, dependents, sponsors, guardians and other relationships without membership programs._

Relationships carry this segment entirely: `sponsor_of`, `beneficiary_of`, `guardian_of` and `caseworker_for` are dated
and queryable, with guardian types able to authorise access. Households are used only where the organization mails or
serves families as units, such as a food bank or a refugee resettlement program.

The household architecture is therefore "generally useful outside Membership" in a narrow sense. The relationship half
is useful to every segment. The household half is useful wherever a family is a unit of service, mail or recognition,
and unnecessary otherwise. For that reason, it is optional per tenant and absent from Membership's core.

### 7. Complex membership coverage

_A membership may cover two named adults; children under a configurable age; grandchildren living elsewhere; a
caregiver; an unnamed guest; employees of a company; individuals explicitly designated by the purchaser._

| Covered party                           | Model                                                                                                                                           | Why not the alternatives                                                                           |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Two named adults                        | Beneficiary rows, role `adult`, `max_count` 2, designated (predicate: spouse, partner or household adult, or any contact if the package allows) | A relationship alone does not say _which_ two adults when there are three                          |
| Children under a configurable age       | Rows, role `dependent`, age window, designated from household children or `child_of` relationships; `term_start` or `continuous`                | Age is a rule on the row, not a property of a relationship                                         |
| Grandchildren living elsewhere          | Rows justified by `grandchild_of`, or designated                                                                                                | A household cannot hold them without corrupting the mailing unit                                   |
| A caregiver                             | Row, role `designee` ("Caregiver"), `benefit_scope` limited (admitted with a dependent)                                                         | Not a family member; not a relationship the family wants recorded as such; an explicit designation |
| An unnamed guest                        | **Not a person**: a guest-pass allowance per term on the package, consumed at the gate                                                          | A row needs a contact; the Raiser's Edge shadow-member failure                                     |
| Employees of a company                  | Rows derived by `affiliated_to_holder` (roles, cascade); end when the affiliation ends                                                          | Designation would make HR a membership-desk task                                                   |
| Individuals designated by the purchaser | Rows, role `designee`, `any_contact`, capped by purchased seats                                                                                 | The purest designation case                                                                        |

**No one receives every benefit because they are related to the holder.** A relationship may _justify_ a row and a rule
may _bound_ it. The row, with its role and scope, is what confers coverage.

### 8. Alumni organizations and family clubs

An alumni couple, two alumni in one household with two individual memberships and one mailing, needs households without
coverage. A fraternity's "family of the chapter" needs relationships. A swim club's family membership is the museum case
with a `continuous` age-out and a caregiver designee. None requires a new model.

### 9. What the scenarios decide

1. Relationships are needed by every segment and are not derivable from households (R1, §2, §4, §6).
2. A household record is needed by museums, churches, fundraising and family clubs, and must be optional for foundations
   and trade associations (R2, R3, §1, §4, §5, §6).
3. Coverage consists of explicit rows with a basis: relationships justify, rules bound, rows confer. The same mechanism
   serves employees and families (R4, R5, §3, §7).
4. The household is never the holder and never a money party in any scenario. The grandparent and corporate cases are
   impossible with a household holder, and the museum case does not need one (R6, §1, §2, §3).
5. Age, custody and separation are operations on rows with dates and reasons, never silent recomputation (R8, §1).

## 8. 07. Alternative Architecture Models

## 07. Alternative Architecture Models

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). This chapter evaluates the five
approaches named in the brief, along with the variants surfaced by the research, against the requirements in chapter
[06](#7-06-cross-industry-requirements-and-use-cases) and the current code described in chapter
[04](#5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed). The scoring is in chapter [08](#9-08-tradeoff-and-decision-matrix); the
recommendation is in [09](#10-09-recommended-domain-model). No earlier conversation is treated as settled.

### 1. Three questions, not one

The architecture requires three separate choices: how to represent relationships between people, how to represent the
mailing and recognition unit, and how to represent membership coverage. The debate has gone back and forth because these
questions have been argued as one. Separating them resolves much of the disagreement:

| Question                                                                                        | What it is really about                                                                     | Candidates                                                                                                       |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Q1. How are two people related?**                                                             | Facts between persons: spouse, parent, guardian, grandparent, sibling, caregiver, custom    | Restore relationships (CRM table, or Platform associations); or derive from household rows; or nothing           |
| **Q2. What is the unit an organization mails, recognises, addresses and lets a person manage?** | A named group with an addressee, salutations, a shared address, a primary member, a history | A first-class household record; an organization of a configurable type; a derivation from relationships; nothing |
| **Q3. Who is covered by a membership, and why?**                                                | Ownership, beneficiaries, eligibility, entitlement, payment                                 | Explicit beneficiaries; dynamic inheritance through relationships; organization flow-down; a household holder    |

The five approaches in the brief take positions on these three questions, sometimes on only one:

| Brief's approach                                 | Q1                    | Q2                                  | Q3                                       |
| ------------------------------------------------ | --------------------- | ----------------------------------- | ---------------------------------------- |
| 1. First-class Household entity                  | (silent)              | New record                          | (silent; often assumed household holder) |
| 2. Households as Organizations                   | (silent)              | Organization of a configurable type | Organization flow-down covers the family |
| 3. Households through Contact Relationships      | Restore relationships | None, or derived                    | Benefits flow through relationships      |
| 4. No household concept in the membership engine | (silent)              | (an independent CRM concern)        | Explicit beneficiaries                   |
| 5. Hybrid                                        | any                   | any                                 | any                                      |

“1 versus 3” is therefore a false choice. A household record and person-to-person relationships answer different
questions, and the systems that model households best (Altru, Nonprofit Cloud, Rock RMS) have both. The real decisions
are Q1 (restore relationships, and on which primitive), Q2 (record, organization, or nothing), and Q3 (explicit
beneficiaries, dynamic inheritance, or both; household as holder or not).

### 2. Q1: person-to-person relationships

#### 2.1 Options

| Option                                    | Description                                                                                                                                                                                                                                          |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Q1-a Nothing (status quo)                 | `spouse_id` reciprocal pointer only                                                                                                                                                                                                                  |
| Q1-b CRM-owned relationship tables        | `crm.relationship_types` + `crm.contact_relationships` (the 1.0 shape done properly; the spouse debt note's target)                                                                                                                                  |
| Q1-c Platform associations                | `platform.association_types` / `platform.associations`, built 2026-10-19 and approved for "every relationship a tenant defines, including contact-to-contact" (custom-objects D-04); CRM seeds system types and adds the semantics associations lack |
| Q1-d Derive relationships from households | "spouse" = the two adults of a household; "parent" = adult to child in the same household                                                                                                                                                            |

#### 2.2 Assessment

Q1-a fails every segment. Fundraising recognition, guardian authorization, coverage justification, church pastoral care,
foundation beneficiaries and alumni couples all require typed facts between people. It also regresses 1.0's only
consumer of relationships: automatic soft credits by type and direction.

Q1-d fails on the first blended family: a parent and child who do not live together, grandparents elsewhere, a guardian
who is not a household member, or an ex-spouse who should stay related ("formerly married") but not be mailed together.
Relationships are facts about people; households are facts about mail. Rock RMS, Nonprofit Cloud, Altru, NPSP and
CiviCRM keep them in separate structures for this reason.

The real choice is Q1-b versus Q1-c. Both provide typed, dated rows that can be read from either side. Q1-b provides
composite foreign keys, a CRM-owned history and merge path, and freedom to add attributes. Q1-c already provides a merge
participant, a delete contract that CRM calls, record history, a data-job import operation, automatic query families on
every dataset, and a configuration UI pattern. It is also the primitive Andrew approved for exactly this on 2026-10-18.

Building Q1-b would create two relationship primitives in the product: one for tenants' custom objects and one for CRM's
own people. It would also require two configuration screens, two merge paths, and a rule for which primitive to use. The
custom-objects architecture rejected that duplication for affiliations only because affiliations carry job titles, roles
and an overlap exclusion that a boat ownership does not want. Person-to-person relationships need none of those. They
need a type, two labels, dates, and an optional primary. Q1-c's three gaps (no symmetric flag, no system code, no
attributes beyond the fixed set) are small Platform additions. CRM can work around two of them in its own layer until
they land.

**1.0's lesson** (r6, r7) concerns consumers, not the primitive: relationships rotted because nothing read them. Q1-c
has four readers on day one: household naming, guardian authorization, Fundraising recognition and Membership coverage
rules. It also has the generated query families and a Contact 360 card.

**Directional or reciprocal.** Use one row with two labels: "Parent of" on one side and "Child of" on the other,
readable from either contact. Symmetric types (spouse, partner, sibling) use one row stored in canonical order, with the
same label both ways. NPSP's two-record mirror and iMIS's "paired records" exist because their platforms could not read
a row from both ends. Rhythm's query layer can: the association families generate both sides. A pair-of-rows design is
therefore unnecessary.

### 3. Q2: the mailing and recognition unit

#### 3.1 Options

| Option                                                 | Description                                                                                                                                                                                                                 |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Q2-a First-class household record                      | `crm.households` with members, roles, primary flags, addresses, computed names with overrides, lifecycle operations; `household_types` configurable                                                                         |
| Q2-b Generic constituent group                         | `crm.groups` with configurable group types carrying behaviour flags (mailing unit, recognition unit, shared address); a household is the default group type; giving circles, family foundations and cohorts are other types |
| Q2-c Organization of a configurable type, by name      | A tenant creates an organization type "Household"; behaviour keyed on its name or id; members as affiliations (CRM-ARCH-001 05 §5's earlier sketch)                                                                         |
| Q2-d Organization of a configurable type, by attribute | Organization types gain a configurable attribute (`party_nature = legal_entity                                                                                                                                              | household`, or `is_household`); every behaviour that must differ keys on the attribute, never on a name; members as affiliations with an exclusive "Primary contact" role (CRM-Q7) |
| Q2-e Derived unit (no record)                          | The unit is computed from relationships or a shared address when needed (mailings group by spouse pair and address; giving totals by spouse pair, the Fundraising proposal's ADR-16)                                        |
| Q2-f Custom object                                     | `objects.households` defined by the tenant through Platform custom objects and associations                                                                                                                                 |

#### 3.2 Q2-c is ruled out by the constraint

Coupling to a configurable record's name or identifier is exactly what the brief forbids. The Platform standard for "the
tenant's Call activity type whatever it was renamed to" (`system_code`) exists because names are not stable. Q2-c is not
analysed further; Q2-d is its legitimate form.

#### 3.3 Q2-d: households as organizations, keyed on an attribute

This is the strongest alternative to a new record and deserves the fullest treatment because it reuses the most existing
functionality. Organizations already have numbers, status, archive, custom fields (approved), typed addresses with a
preferred flag, phones, layouts and forms per type, history, Quick Search, a dataset with `contact_count`, merge with
aliases and 410, deletion refusal while affiliations exist, tenant lifecycle, and exports.

Through affiliations, organizations also have dated membership with roles, one primary, overlap exclusion, linked
contact information with defaults and the `disconnected` state, D-09 explicit mutations, and Membership's organization
flow-down by role. The console has an Organizations explorer, a picker and a bespoke Organization 360. Nothing in the
tables forbids an organization whose type is marked household (r1 §2.5).

The question is what must change for an organization to _behave_ as a household, and what every other service must know
about that distinction. Reading the code (r1, r2, r3, r5) produces the following list. Each item requires a branch on
`party_nature` somewhere:

| #   | Where                                   | Why a branch is needed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Severity          |
| --- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| 1   | Affiliations: one primary per contact   | `is_primary` is unique across all of a contact's affiliations and _is_ the contact's Organization field (affiliations README §0). An employed adult's household affiliation can never be primary, so "primary household" has no representation; the "Organization" column, the primary-organization pick and automatic roles ignore the household. Fix: a second primary flag scoped by the organization's nature, which a partial unique index cannot read across tables, so the nature must be denormalised onto every affiliation row by trigger | Structural        |
| 2   | Household's primary member              | Needs an exclusive role (CRM-Q7, not yet decided) on the household's affiliations and a rule that every household has one; the first-contact automatic role rule runs only on primary affiliations (item 1)                                                                                                                                                                                                                                                                                                                                         | High              |
| 3   | Names and salutations                   | `organizations.name` is user-entered text; a household's name, mailing name and two salutations are computed from members with overrides; the aggregate needs a nature branch on save, and the four new columns are meaningless for companies                                                                                                                                                                                                                                                                                                       | High              |
| 4   | Organization-only fields and operations | Website and domain (with domain normalisation and alternate domains), acronym, tax-exempt, logo, parent hierarchy with the no-cycle trigger, "Add Subsidiary", "Set Parent", compliance rules and notifications per type, `restrict_contact_roles`: each must be hidden, refused or ignored for the household nature (layouts hide fields; the service must refuse a parent or a compliance rule)                                                                                                                                                   | Medium            |
| 5   | Duplicate detection                     | Organization rules match on name and domain; households need member-set and address rules; the scorer must branch                                                                                                                                                                                                                                                                                                                                                                                                                                   | Medium            |
| 6   | Lists, reports, pickers, Quick Search   | Every organization list, count, directory, picker and "member organizations" report must filter by nature or households leak into them; the Organizations explorer, the organization picker (today a 2,000-row browser list) and the Quick Search type all need nature awareness                                                                                                                                                                                                                                                                    | Medium            |
| 7   | Address and phone types                 | `applies_to` is contacts, organizations or both; a household wants Home, not Billing; a per-nature filter is new                                                                                                                                                                                                                                                                                                                                                                                                                                    | Low               |
| 8   | **Accounting**                          | `bill_to_kind = organization` is allowed for any organization; `tax_exempt` is read from the organization at issue; a household organization becomes a bill-to and an aging party overnight unless Accounting learns the nature from CRM (a cross-service read of a CRM configuration attribute at document issue)                                                                                                                                                                                                                                  | **Cross-service** |
| 9   | **Commerce**                            | "bill my organization" resolves to the buyer's primary affiliation (safe only because of item 1), organization purchasing roles, organization-member pricing, `purchasing_organization` promotion subjects, organization bill-to options: each must exclude household-natured organizations or a family buys at corporate rates and bills the family                                                                                                                                                                                                | **Cross-service** |
| 10  | **Membership**                          | `member_kind = organization` lets a household hold an organizational membership: flow-down by role covers the family (the attraction), but the organization path also means organization-member pricing, the descendant cascade to depth 10, chapter and unit rules, and no payer (the organization is the buyer); and every Membership report counts the family as an organization member                                                                                                                                                          | **Cross-service** |
| 11  | **Billing**                             | "the organization pays" defaults and organization billing contacts apply to households                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Cross-service     |
| 12  | Portal                                  | Organization administrator (CRM-ADR-11, derived from affiliation roles) becomes the household manager for free, but the organization roster rule "no route writes another member's personal record" must gain the guardian exception by nature                                                                                                                                                                                                                                                                                                      | Medium            |
| 13  | Merge                                   | Merging a household into a company is allowed unless the planner checks nature                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | Low               |
| 14  | Published reads and events              | `crm.organization.*` events and views carry households; every subscriber (Integration Platform connectors, Quick Search, Membership's change handler) sees them as organizations                                                                                                                                                                                                                                                                                                                                                                    | Medium            |

Items 8 to 11 are decisive. They make four other services depend on a CRM configuration attribute to avoid treating a
family as a company. They also make the household a money party by default, which chapter
[06](#7-06-cross-industry-requirements-and-use-cases) §7 shows no segment wants. The competitive record shows that this
produces the worst defects: NetForum's household address, Tessitura's stray transactions, and CiviCRM's three records
per couple. Item 1 is structural inside CRM.

Q2-d's reuse is real, but it reuses _organization_ behaviour, most of which must then be switched off by a flag in a
dozen places across five services. Those are precisely the special cases and coupling the brief asks to minimise.

#### 3.4 Q2-e: no record

Chapter [06](#7-06-cross-industry-requirements-and-use-cases) §4 (church), §5 (fundraising) and §1 (museum) each require
a stable unit with a name, an addressee, salutations, a shared address, a primary member, overrides and a history that
survives any member's change.

A derived unit has no stable identity. Editing a relationship changes the unit, so last year's giving statement is
addressed differently this year. There is nowhere to keep an override and no support for life-event operations. Raiser's
Edge is the twenty-year experiment in Q2-e; its head-of-household flag and the resulting mailing drop-outs are the
outcome ([05 §2](#6-05-competitive-research)). The Fundraising proposal's spouse-pair key is Q2-e, and its own author
lists it as working "only for the spouse pair".

#### 3.5 Q2-f: custom object

The custom-objects architecture excludes anything with money, terms or platform-guaranteed rules. Custom records cannot
be merged, are not eligibility subjects, and their portal is paused (r4 §11). A household must merge, must be a
Fundraising recognition basis and must feed coverage rules. The platform's own boundary rules out Q2-f.

#### 3.6 Q2-a versus Q2-b: a household record, or a generic group

Q2-b is attractive on paper: one `groups` table with typed roles serves households, giving circles, family foundations
and alumni classes, and "household" is configuration rather than code. Rock RMS does exactly this: Family is a Group of
type Family; Known Relationships are another group type. Three points weigh against it:

- The behaviours that make a household a household are specific: computed mailing name and salutations from members, a
  primary member, address linking into members' records, dissolution and reinstatement, coverage proposals, and guardian
  derivation. A generic group with behaviour flags per type is a household table with a more abstract name and a flag
  per behaviour.
- Rhythm already has two grouping mechanisms for everything else: Committees (membership with roles and terms) and
  custom objects with associations ("Contact member of Giving Circle", approved 2026-10-18). A third generic grouping
  would require a rule for when to use which.
- `household_types` (configurable, with layout slots) already gives tenants the configurability Q2-b promises for
  household-like units. A "Giving household" type and a "Family" type are two household types with the same semantics.

Q2-a is therefore the simplest model that correctly represents the requirement: a narrow household record with
configurable types, no generic abstraction, and other groupings left to the mechanisms that already exist.

#### 3.7 Why a new record and not the existing model: the demonstration the brief asked for

The existing CRM model is insufficient for Q2 on four counts that configuration cannot remove: (1) a contact's single
primary affiliation cannot also be a primary household; (2) an organization's name is input, while a household's name is
computed from its members; (3) a household must never be a bill-to, payer or donor, but every service treats an
organization as all three; (4) relationships alone provide no stable identity for mailing, recognition and history.

A new record costs a CRM module built on the organization template and two Platform list entries. It requires no branch
in any other service because households are neither money parties nor membership holders.

### 4. Q3: who is covered, and why

#### 4.1 Options

| Option                                         | Description                                                                                                                                                                                                                        |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Q3-a Explicit beneficiaries                    | A membership has one holder (contact or organization) and dated beneficiary rows with a coverage role; who may be a beneficiary is bounded by package rules; rows are the only source of entitlement                               |
| Q3-b Dynamic inheritance through relationships | A person is entitled if related to a holder by a type the program flags (1.0 fundraising's soft-credit shape applied to benefits; NetForum's flow-down flags; CiviCRM's "Household Member of" inheritance); evaluated at read time |
| Q3-c Organization flow-down generalised        | Today's materialised child memberships derived from affiliations and roles, extended to derive from relationships and household membership by configurable rules                                                                   |
| Q3-d Household holder                          | The household owns the membership; its members are covered by membership in the household                                                                                                                                          |
| Q3-e Both a and c                              | Explicit beneficiary rows as the one representation, produced either by designation or by a derivation rule                                                                                                                        |

#### 4.2 Assessment

Q3-b fails the brief's own requirement: "entitled under this program, and why" must be consistent and explainable. A
read-time walk over relationships changes its answer whenever a relationship is edited, cannot be capped or priced, and
cannot say which of two spouses' memberships covers a child. Every product that did it (CiviCRM, NetForum Pro) reports
the inheritance holes. It is also slow at the gate.

Q3-d (household as holder) was the first draft's choice. Chapter
[11 §2](#12-11-membership-coverage-and-benefit-inheritance-strategy) explains why it was dropped: it adds a party kind to
Membership and Commerce for a label, still needs a billing contact and beneficiary rows, and does not help the
grandparent, caregiver, corporate or no-household tenants. Owner transfer plus beneficiary rows cover its operational
cases.

Q3-a alone cannot serve organizational memberships, whose roster is HR's, not a membership desk's. Q3-c alone cannot
serve the museum family where the buyer names who is in. Q3-e uses one representation with two sources, and Membership
already half-has it: flow-down and assigned seats are two sources of the same child-membership row.

**Recommendation for Q3: Q3-e**, with the holder a contact or organization. Detailed in chapter
[11](#12-11-membership-coverage-and-benefit-inheritance-strategy).

### 5. The hybrid that results, and the hybrids rejected

| Hybrid                                              | Q1                                       | Q2                           | Q3                                                                            | Verdict                                                                                                |
| --------------------------------------------------- | ---------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Recommended**                                     | Platform associations with CRM semantics | First-class household record | Explicit beneficiaries, designated or derived; contact or organization holder | Fewest special cases; no new party kind anywhere; each question answered by the primitive built for it |
| Organizations-by-attribute + flow-down (approach 2) | (none, or associations)                  | Organization                 | Flow-down by role                                                             | Most reuse on paper; a dozen nature branches across five services; household becomes a money party     |
| Relationships + beneficiaries, no household (3 + 4) | Associations                             | None                         | Explicit beneficiaries                                                        | Right for coverage; insufficient for churches, fundraising householding and family UX                  |
| Household record + household holder (first draft)   | Associations                             | Record                       | Household holder + roster                                                     | Correct but heavier: third party kind in Membership and Commerce for little gain                       |
| Generic group + associations                        | Associations                             | Generic group                | Beneficiaries                                                                 | A household table under another name plus a rule about three grouping mechanisms                       |

The recommended hybrid is spelled out in chapter [09](#10-09-recommended-domain-model) and scored in chapter
[08](#9-08-tradeoff-and-decision-matrix).

## 9. 08. Tradeoff and Decision Matrix

## 08. Tradeoff and Decision Matrix

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). **A. Recommended hybrid** is the
recommendation. The candidates are the hybrids from chapter [07 §5](#8-07-alternative-architecture-models), assessed
against the brief's thirteen criteria. Ratings are **Strong**, **Adequate** or **Weak**, each supported by the one fact
that determines it. A numeric score would imply more precision than the assessment supports. The last row counts special
cases and cross-service couplings, which the brief asked to weigh most heavily.

Candidates:

- **A. Recommended hybrid**: Platform associations for person-to-person relationships; a first-class household record;
  memberships held by a contact or organization, with explicit beneficiary rows produced by designation or derivation
  rules.
- **B. Organizations by attribute**: organization types gain a household nature; members are affiliations; coverage
  follows organization flow-down; relationships are as in A or absent.
- **C. Relationships and beneficiaries, no household**: associations plus explicit beneficiaries; no mailing unit
  record.
- **D. Household record with household holder**: as A, but the household is a third membership holder kind.
- **E. Generic constituent group**: as A, with a `groups` table and typed group roles instead of `households`.

| Criterion                                    | A. Recommended                                                                                                                                                      | B. Organizations by attribute                                                                                                                      | C. Relationships + beneficiaries only                                                               | D. Household holder                                                                                                  | E. Generic group                                                             |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Domain simplicity                            | **Strong**: three questions, three primitives, no new party kind                                                                                                    | **Weak**: one table, two natures, a dozen nature branches (07 §3.3)                                                                                | **Strong** in objects, **Weak** in meaning: the mailing unit exists only in people's heads          | **Adequate**: one more holder kind and a billing contact beside it                                                   | **Adequate**: more abstract than the requirement; a third grouping mechanism |
| Real-world nonprofit requirements (06)       | **Strong**: every scenario in 06 passes                                                                                                                             | **Adequate**: passes with branches; the family becomes a bill-to and an organization member by default                                             | **Weak**: church, fundraising householding and museum family UX fail (06 §1, §4, §5)                | **Strong**                                                                                                           | **Strong**                                                                   |
| Compatibility with CRM 2.0                   | **Strong**: a module on the organization template; affiliations untouched; associations as approved                                                                 | **Weak**: the one-primary rule must become nature-aware (trigger-denormalised); `name` semantics split by nature                                   | **Strong**: no CRM change beyond associations                                                       | **Strong**                                                                                                           | **Adequate**: a new generic concept beside Committees and custom objects     |
| Compatibility with Membership 2.0            | **Strong**: holder unchanged; flow-down generalised into the rule evaluator it already is                                                                           | **Adequate**: flow-down works, but organization semantics (pricing, cascade, chapters) apply to families                                           | **Strong**                                                                                          | **Weak**: owner CHECK, identity indexes, `prepare`, snapshots, exports, polymorphic holder, Commerce recipient kinds |
| Extensibility across segments                | **Strong**: foundations use relationships only; churches use households without Membership; museums use rules                                                       | **Adequate**: segments without "organizations" in the legal sense still get them                                                                   | **Adequate**: fine until a segment needs a mailing unit (most do)                                   | **Strong**                                                                                                           | **Strong** (by design), at the price above                                   |
| Query and reporting complexity               | **Strong**: household dataset with computed counts; `exists` over members; association families; no to-many join                                                    | **Adequate**: every organization report needs a nature filter; "member organizations" counts need care                                             | **Weak**: household totals need a derived key that changes with edits                               | **Strong**                                                                                                           | **Strong**                                                                   |
| Benefit eligibility and inheritance          | **Strong**: one row shape, two bases, one entitlement answer with a basis                                                                                           | **Adequate**: flow-down by role only; designation needs the application path; no age rules                                                         | **Strong** (same as A)                                                                              | **Strong** (same as A)                                                                                               | **Strong** (same as A)                                                       |
| Membership lifecycle management              | **Strong**: renewal re-proposal, age-out, spin-off, owner transfer, attention on composition change                                                                 | **Adequate**: the organization lifecycle (no spin-off, no age-out)                                                                                 | **Strong**                                                                                          | **Adequate**: dissolution must handle a membership the household owns                                                | **Strong**                                                                   |
| Data integrity                               | **Strong**: composite FKs inside CRM; exclusion constraints; beneficiary rows with a basis                                                                          | **Adequate**: integrity intact, semantics leak                                                                                                     | **Strong** for what exists                                                                          | **Strong**                                                                                                           | **Strong**                                                                   |
| API design                                   | **Strong**: `/households`, `/contacts/{id}/relationships`, `/memberships/{id}/beneficiaries`, `/entitlements`                                                       | **Adequate**: household operations hang off `/organizations` with nature checks                                                                    | **Adequate**: no household resource; clients invent one                                             | **Adequate**: `holder.kind = household` on every membership client                                                   | **Adequate**: `/groups` with type semantics clients must learn               |
| Customer configurability                     | **Strong**: household types, roles, relationship types, naming formats, coverage rules all configuration                                                            | **Strong** in theory (the nature attribute), **Weak** in practice (configuring a household means configuring an organization type's hidden fields) | **Adequate**                                                                                        | **Strong**                                                                                                           | **Strong**                                                                   |
| Migration from Rhythm 1.0                    | **Strong**: 1.0 had no households; spouse backfills to an association; flow-down maps to derived rules; seats to designated rows                                    | **Strong**: same, plus no new table                                                                                                                | **Strong**                                                                                          | **Strong**                                                                                                           | **Strong**                                                                   |
| Implementation and maintenance cost          | **Adequate**: a CRM module (tables, aggregate, routes, dataset, merge planner, console entity), Membership's beneficiary roster and rules, small Platform additions | **Adequate** to build, **Weak** to maintain: every future organization feature must ask "and for households?"                                      | **Strong** to build, **Weak** later: the mailing unit is built anyway when a church or museum signs | **Weak**: A plus a party-kind change in two services                                                                 | **Adequate**: A plus the abstraction tax                                     |
| **Special cases and cross-service coupling** | **0** nature branches; **0** new party kinds; Membership reads CRM facts only inside rules                                                                          | **~14** nature branches, **4** services coupled to a CRM configuration attribute                                                                   | **0**, but the requirement is unmet                                                                 | **~12** branch points in Membership and **3** vocabularies in Commerce                                               | **0**, plus a rule for three grouping mechanisms                             |

### Reading the matrix

- **A and C tie on coverage** because they share the beneficiary design. They differ only in whether the mailing unit
  has its own record. The segment requirements decide that question: every church, most museums and every fundraising
  shop need the record.
- **B is the only candidate whose cost grows with every future feature.** Each new organization capability, whether a
  hierarchy report, a purchasing rule, a compliance notice or a pricing audience, must define what it means for a
  household. A avoids this recurring cost because its household is not an organization.
- **D buys a label.** Its one real advantage over A is ownership continuity through a family's changes. Owner transfer
  and the beneficiary rows already cover that need; D adds a party kind in Membership and Commerce.
- **E is A with a more general name.** Committees, custom objects and configurable household types already provide that
  generality.

### Decision

**A**, with the approvals listed in chapter [14](#15-14-architecture-decisions-requiring-approval). The strongest arguments, in order,
are: (1) no household is ever a money party or a holder, so four services stay untouched; (2) the three questions are
answered by the three primitives built for them, two of which already exist; (3) the coverage design answers "entitled,
and why" with one row shape for individual, family and organizational memberships; (4) every segment in chapter
[06](#7-06-cross-industry-requirements-and-use-cases) passes without a special case.

## 10. 09. Recommended Domain Model

## 09. Recommended Domain Model

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Chapters
[07](#8-07-alternative-architecture-models) and [08](#9-08-tradeoff-and-decision-matrix) explain the choice; this
chapter states the recommended model. Evidence: [r1](#21-r1-rhythm-2-0-crm-macroservice-what-a-households-relationships-family-membership-proposal-can-build-on) §2, §4, §15; [r4](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships) §2,
§4, §11, §12; chapter [05](#6-05-competitive-research) §10.

### 1. In one paragraph

> **As ruled (Andrew 2026-10-21).** Two of the choices in this chapter were overruled: relationships live in CRM-owned
> tables rather than Platform associations (HC-D1; DDL in [10 §2a](#11-10-proposed-data-model-and-service-boundaries)), and a
> household may hold a membership (HC-D3; note in [11 §2](#12-11-membership-coverage-and-benefit-inheritance-strategy)). The rest
> of the model stands as written. The rulings are recorded in [14](#15-14-architecture-decisions-requiring-approval).

The recommended model uses three primitives to answer three distinct questions, each owned by the service built for it.
**Relationships between people** are Platform associations of CRM-seeded and tenant-defined types. A thin CRM semantics
layer defines whether each type is symmetric or directional, may authorise access, or counts for naming. **A household**
is a narrow first-class CRM record: a mailing and recognition unit with its own name, addressee, salutations, shared
address, dated members with roles, and a primary contact. It is never an organization, a money party or a membership
holder. **Membership coverage** is a set of explicit beneficiary rows on a membership held by one contact or
organization. Each row carries its role and the basis that justifies coverage: designation by a person within package
rules, or derivation by a rule from CRM facts such as an affiliation, a relationship or household membership. These rows
are materialised as the child membership rows Membership already uses for organizations. Households and relationships
are independent of each other and of Membership. Membership reads CRM facts only inside its coverage rules.

### 2. Definitions

| Concept           | Definition                                                                                                                                                                                         | Does **not** imply                                                                                                      |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Relationship      | A dated, typed fact between two contacts, labelled in each direction, read from either side                                                                                                        | Household membership; any authority (a guardian type may _produce_ a grant, which is a separate dated row); any benefit |
| Household         | A group of related people treated as one unit for mailing, recognition, family registration and self-service: a record with identity, name, status, type, contact information, members and history | A shared address, surname or legal tie; authority; a customer, donor or payer; an organization; a membership holder     |
| Household member  | A dated row placing one contact in one household with a role, two primary flags (the household's primary contact; the contact's primary household) and a manager flag                              | A relationship to any other member                                                                                      |
| Household role    | Configuration: adult, child, dependent, other, and tenant additions; carries `counts_as_adult` and `implies_dependent` for naming and coverage rules                                               | Any permission                                                                                                          |
| Membership holder | The one contact or organization that owns a membership (unchanged)                                                                                                                                 | That the holder pays, or is covered alone                                                                               |
| Beneficiary       | A dated row: this contact is covered by this membership in this coverage role, on this basis                                                                                                       | That the person is related to the holder (they may be a named guest) or lives with them                                 |
| Coverage rule     | Package configuration: role, predicate over CRM facts, cap, age window, basis mode, add-on product, benefit scope                                                                                  | That it runs at read time                                                                                               |
| Entitlement       | Membership's answer to "entitled under program P on date D, and why": the membership rows (holder or beneficiary) with status, term and basis                                                      | A global "is member" flag                                                                                               |
| Payer and bill-to | Commerce's buyer and bill-to, Accounting's bill-to: a contact or organization                                                                                                                      | The holder                                                                                                              |

### 3. The model at a glance

```mermaid
erDiagram
    CONTACT }o--o{ CONTACT : "related (platform.associations, typed, dated)"
    ASSOCIATION_TYPE ||--o| RELATIONSHIP_TYPE_SETTINGS : "CRM semantics"
    HOUSEHOLD ||--o{ HOUSEHOLD_MEMBER : "has (dated, role)"
    CONTACT ||--o{ HOUSEHOLD_MEMBER : "belongs through"
    HOUSEHOLD_TYPE ||--o{ HOUSEHOLD : types
    HOUSEHOLD_ROLE ||--o{ HOUSEHOLD_MEMBER : "held as"
    HOUSEHOLD ||--o{ HOUSEHOLD_ADDRESS : owns
    HOUSEHOLD ||--o{ HOUSEHOLD_PHONE_NUMBER : owns
    CONTACT_ADDRESS }o--o| HOUSEHOLD_ADDRESS : "may link to (via member row)"
    CONTACT ||--o{ MEMBERSHIP : "holds (owner_kind contact)"
    ORGANIZATION ||--o{ MEMBERSHIP : "holds (owner_kind organization)"
    MEMBERSHIP ||--o{ MEMBERSHIP_BENEFICIARY : "covers (role, basis)"
    CONTACT ||--o{ MEMBERSHIP_BENEFICIARY : "is covered as"
    MEMBERSHIP_PACKAGE ||--o{ PACKAGE_COVERAGE_RULE : "bounds coverage by"
    MEMBERSHIP_BENEFICIARY ||--o| MEMBERSHIP : "materialised as child (origin covered)"
```

No line joins HOUSEHOLD to MEMBERSHIP. Through a read, the Household 360 shows memberships held by or covering its
members. A coverage rule may reference household membership as a predicate, but nothing in Membership stores a household
id.

### 4. Why these three and not fewer

- **Why relationships are not households.** A parent and child who do not live together, grandparents elsewhere, a
  guardian outside the home, an ex-spouse who stays related: these are facts about people, not facts about mail. Every
  system that modelled households deliberately (Altru, Nonprofit Cloud, Rock RMS, CiviCRM, Ministry Platform) keeps both
  relationships and households.
- **Why households are not organizations.** Configuration cannot remove four differences: a contact's single primary
  affiliation cannot also be a primary household; an organization's name is input, while a household's is computed from
  its members; every service treats an organization as a bill-to, payer and donor; and the organization's legal-entity
  behaviour (tax, hierarchy, purchasing, compliance, pricing audiences) would have to be switched off by a nature flag
  in about fourteen places across five services ([07 §3.3](#8-07-alternative-architecture-models)). Not using
  organizations at all therefore meets the brief's constraint against a protected or name-coupled organization type. The
  list in 07 §3.3 provides the demonstration the brief asked for.
- **Why beneficiaries are not relationships.** "Who is covered" must be capped, priced, dated and auditable, and handled
  the same way for employees and families. A row with a basis supports all of those requirements; a read-time walk over
  relationships supports none of them.
- **Why the household is not a holder.** Making a household a holder adds a party kind to Membership and Commerce for a
  label. Owner transfer, beneficiary rows and the household manager grant already cover its operational cases (death,
  separation, portal visibility). The grandparent and corporate cases are impossible with a household as holder, and
  that model would make the household a Commerce recipient on the way to becoming a bill-to
  ([11 §2](#12-11-membership-coverage-and-benefit-inheritance-strategy)).

### 5. Entities

Full DDL is in chapter [10](#11-10-proposed-data-model-and-service-boundaries) (CRM) and
[11 §5](#12-11-membership-coverage-and-benefit-inheritance-strategy) (Membership).

| Entity                                                      | Owner      | Key points                                                                                                                                                                                                                                             |
| ----------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `platform.association_types` / `platform.associations`      | Platform   | Existing. CRM seeds `crm_spouse`, `crm_partner`, `crm_parent_child`, `crm_guardian_dependent`, `crm_sibling`, `crm_grandparent_grandchild`, `crm_caregiver_dependent`, `crm_authorized_representative` per tenant as a seed set; tenants add their own |
| `crm.relationship_type_settings`                            | CRM        | One row per contact↔contact association type: `kind`, `symmetric`, `may_authorize_access`, `counts_for_naming`, `system_code`; what associations cannot say today                                                                                      |
| `crm.households`                                            | CRM        | Number; type; status; four computed names with overrides; `name_format`; region; dissolution; archive; UUIDv7-checked id                                                                                                                               |
| `crm.household_members`                                     | CRM        | Immutable household and contact; role; `is_primary_contact` (one open per household); `is_primary_household` (one open per contact); `is_manager`; dates; `end_reason`; overlap exclusion per contact and household; 50 per household                  |
| `crm.household_types`, `crm.household_roles`                | CRM        | Configuration-id tables with `system_code`, seeded (`household`; `adult`, `child`, `dependent`, `other`), deactivatable, deletable when unreferenced; code never depends on a particular row existing (defaults are the first active)                  |
| `crm.household_addresses`, `crm.household_phone_numbers`    | CRM        | As organization addresses and phones; address and phone types' `applies_to` gains households                                                                                                                                                           |
| Contact address and phone rows                              | CRM        | A fifth link source: `source_household_member_id`, `source_household_id`, `source_household_address_id`; `disconnected` reasons `member_ended`, `household_address_removed`                                                                            |
| `crm.activity_links.household_id`, `crm.tasks.household_id` | CRM        | A household as a subject                                                                                                                                                                                                                               |
| `membership.membership_beneficiaries`                       | Membership | Row per covered person per membership: role, basis, basis record, dates, end reason, the materialised child membership, add-on line                                                                                                                    |
| `membership.package_coverage_rules`                         | Membership | Role, predicate kind and parameters, caps, age window and basis, basis mode, follows-predicate, add-on product, benefit scope                                                                                                                          |
| `membership.memberships`                                    | Membership | Unchanged owner; `origin` gains `covered`; `billing_contact_id` added for organization holders                                                                                                                                                         |

### 6. Invariants that span tables

| #    | Invariant                                                                                                                                                                                                                                 | Enforced by                                                              |
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| I-1  | A household has at most one open primary contact; a household with open members has exactly one at commit                                                                                                                                 | partial unique index + deferred constraint trigger + aggregate           |
| I-2  | A contact has at most one open primary household; the first household joined becomes it unless the save says otherwise                                                                                                                    | partial unique index + aggregate                                         |
| I-3  | A contact is never an open member of one household twice; may be an open member of several                                                                                                                                                | exclusion constraint                                                     |
| I-4  | Ending a member row disconnects the contact information linked through it, clears its flags and recomputes the household's names in the same transaction                                                                                  | household aggregate                                                      |
| I-5  | A contact's name, status or death date change recomputes the names of every household it is an open member of, inside the contact save                                                                                                    | contact aggregate hook                                                   |
| I-6  | A symmetric relationship type is stored once in canonical order; a second row the other way is refused and collapsed on merge                                                                                                             | CRM relationship service over Platform's routes; Platform's participant  |
| I-7  | No permission follows from a household member row or a relationship; a manager or guardian role is a dated derived assignment re-derived on every relevant change                                                                         | `platform.sync_derived_assignments` from a CRM derivation (A4)           |
| I-8  | A household merge leaves zero references to the merged id in every participating schema, one open member row per contact in the survivor, and an alias; a contact merge leaves the survivor an open member of each household at most once | merge engine + coverage guard                                            |
| I-9  | A beneficiary row exists only within its membership's term and its rule's caps and age window at the time it was written; every open row has exactly one open child membership                                                            | Membership persistence under the membership row lock                     |
| I-10 | Entitlement is answered from membership rows only; no service derives entitlement from relationships or household rows                                                                                                                    | the `/entitlements` contract; Events, Commerce and the portal consume it |
| I-11 | No household id appears in any money document or in Membership's tables; the household appears in `custom_metadata` or `selling_context` for reporting only                                                                               | schema review; the closed `BILL_TO_KINDS` and `PARTY_KINDS`              |
| I-12 | Omitting members, addresses, phones, relationships or beneficiaries from a save leaves them unchanged; add, end and remove are explicit operations with their own permission keys                                                         | write contracts W3, E-09, D-09                                           |

### 7. Worked example

The Rivera-Chen family at the City Museum consists of Jane Rivera (adult, primary contact, manager), Wei Chen (adult,
spouse of Jane), Maya Chen (11), Leo Chen (17, shared custody, primary household his father's), and Rosa Rivera (Jane's
mother, lives with them, holds her own Professional membership at a society that is a different tenant). Wei's parents,
Hal and Ann, live across town and bought a Grandparents membership at the zoo.

| Fact                                                                                                                                                                                                                                                         | Where it lives                                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Jane spouse of Wei; Jane and Wei parent of Maya and Leo; Rosa parent of Jane; Hal and Ann grandparent of Maya and Leo                                                                                                                                        | `platform.associations`, types `crm_spouse` (one row), `crm_parent_child` (four rows), `crm_grandparent_grandchild` (four rows)                                           |
| The Rivera-Chen Family: five members, Jane primary contact and manager, Leo's primary household elsewhere, Home address at 14 Elm St linked into four members' Home rows, Leo's Home owned (his father's)                                                    | `crm.households`, `crm.household_members`, `crm.household_addresses`, contact address rows                                                                                |
| Museum Family membership M-1 held by Jane: Wei `adult` (designated, basis `crm_spouse` row), Maya and Leo `dependent` (designated, basis household member rows, rule "under 18 at term start"), Rosa not covered (over the dependent age; not an adult slot) | `membership.memberships` (owner Jane) + three `membership_beneficiaries` rows + three child membership rows (`origin = covered`)                                          |
| Zoo Grandparents membership M-2 held by Hal: Ann `adult`, Maya and Leo `dependent` (basis `crm_grandparent_grandchild` rows)                                                                                                                                 | another tenant's Membership; same shape                                                                                                                                   |
| Who may renew M-1                                                                                                                                                                                                                                            | Jane (holder); Wei (adult beneficiary, package allows); a household manager grant (Jane); staff                                                                           |
| Who may register Maya for camp and sign the waiver                                                                                                                                                                                                           | Jane or Wei: Guardian grants derived from `crm_parent_child` while Maya is a minor                                                                                        |
| Invoice for M-1                                                                                                                                                                                                                                              | Bill-to Jane (buyer), or Hal if he pays as a gift (buyer and bill-to Hal, recipient Jane)                                                                                 |
| Leo turns 18 next March                                                                                                                                                                                                                                      | `term_start` basis: covered until M-1's term ends in May; dropped at renewal with the reason; Guardian grants over Leo end on his birthday                                |
| Jane and Wei separate in June                                                                                                                                                                                                                                | Wei's member row ends (`left`), the spouse association ends, Wei's beneficiary row ends (`removed`) or Wei spins off; a new household for Wei; Leo added to it as primary |
| Household giving total for the museum's gala                                                                                                                                                                                                                 | Σ gifts by Jane, Wei, Rosa while members, de-duplicated by gift; recognition credit rows per gift under the tenant's rule; donor of record per gift unchanged             |

Nothing in the example required a household to hold anything, pay anything or confer anything. Nor did it require a
relationship to carry a benefit.

## 11. 10. Proposed Data Model and Service Boundaries

## 10. Proposed Data Model and Service Boundaries

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). The model is chapter
[09](#10-09-recommended-domain-model); the Membership tables are in chapter
[11 §5](#12-11-membership-coverage-and-benefit-inheritance-strategy). This chapter is the CRM DDL, the APIs and the service
boundaries. Conventions: RHYTHM-CRM-ARCH-001 `04` §1 and services-platform `docs/persistence.md`, `docs/indexing.md`,
`docs/row-level-security.md`, `docs/naming-conventions.md`. Everything is representative SQL for review; migrations are
generated from Drizzle table files in `services-crm/modules/households/db/`, with the exclusion constraints added in the
migration SQL as the affiliation ones were (`0065`, `0067`). `crm.uuid_v7_check(id)` and
`crm.configuration_id_check(id)` stand for the shared library checks; every table carries `tenant_id`, a
`(tenant_id, id)` key, the `sys_*` columns, forced row-level security on `app.tenant_id`, and the text-normalization
trigger. Dates are dates (`docs/time.md`).

### 1. Service boundaries

| Service                            | Owns (writes)                                                                                                                                                                                                                                                                                                                                                                                                  | Publishes (reads with a compatibility promise)                                                                                                                                                                                                                                                                                              | Reads from others                                                                                               |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **CRM**                            | `crm.households`, `household_members`, `household_types`, `household_roles`, `household_addresses`, `household_phone_numbers`, `relationship_type_settings`; the household link columns on contact addresses and phones; household subjects on activity links and tasks; the relationship routes (which write Platform associations inside CRM's transaction); seed sets; the manager and guardian derivations | `crm.households_summary`, `crm.household_members_current`, `crm.household_of_contact_on(contact_id, date)`, `crm.household_primary_contact(household_id)`, `crm.relationships_of(contact_id, type_ids[], as_of)`, `crm.guardians_of(contact_id)`, business events `crm.household.*`, `crm.household_member.*`, `crm.contact_relationship.*` | `platform.associations` and `association_types` (through the Platform service function in the same transaction) |
| **Platform**                       | `platform.association_types`, `platform.associations` (the relationship rows); derived security assignments (source `crm`); the query datasets registry entries; Quick Search type                                                                                                                                                                                                                             | What it publishes today: association routes and service functions, `sync_derived_assignments`, `apply_endpoint_deletion`, merge participant functions                                                                                                                                                                                       | CRM objects for endpoint verification                                                                           |
| **Membership**                     | `membership.membership_beneficiaries`, `membership.package_coverage_rules`, `memberships.origin = covered`, `memberships.billing_contact_id`; the rule evaluator; the entitlement answer                                                                                                                                                                                                                       | `GET /memberships/{t}/entitlements`; beneficiaries dataset                                                                                                                                                                                                                                                                                  | CRM's published reads inside `prepare`, the evaluator and the sweep; CRM and Platform business events           |
| **Commerce, Accounting, Billing**  | Nothing new                                                                                                                                                                                                                                                                                                                                                                                                    | —                                                                                                                                                                                                                                                                                                                                           | Membership's entitlement answer (Commerce pricing); nothing about households                                    |
| **Fundraising, Marketing, Events** | Their own records citing households as a recognition basis, an audience kind, an attendee source ([A3](#18-a3-cross-application-integration))                                                                                                                                                                                                                                                                               | —                                                                                                                                                                                                                                                                                                                                           | CRM's published reads; Membership's entitlement answer                                                          |

Rules: no cross-schema foreign key (platform rule); every reference into another schema is a text id verified at write;
Membership never stores a household id; CRM never stores a membership id; the household appears in a money document only
inside `custom_metadata` or `selling_context`, which no ledger reads.

### 2. CRM configuration tables

```sql
CREATE TABLE crm.household_types (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT household_types_id_configuration_id_check CHECK (crm.configuration_id_check(id)),
    name                   text        NOT NULL CONSTRAINT household_types_name_not_blank_check CHECK (btrim(name) <> ''),
    description            text,
    system_code            text        CONSTRAINT household_types_system_code_check CHECK (system_code IN ('household')),
    is_active              boolean     NOT NULL DEFAULT true,
    display_order          integer,
    console_page_layout_id text, console_form_id text, console_create_form_id text, portal_page_layout_id text, portal_form_id text,
    -- sys_version, sys_created_at, sys_created_by_id, sys_last_modified_at, sys_last_modified_by_id, sys_locked (+ check), sys_external_id
    CONSTRAINT household_types_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT household_types_name_uq UNIQUE (tenant_id, name)
);
CREATE UNIQUE INDEX household_types_system_code_uidx ON crm.household_types (tenant_id, system_code) WHERE system_code IS NOT NULL;

CREATE TABLE crm.household_roles (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT household_roles_id_configuration_id_check CHECK (crm.configuration_id_check(id)),
    name                   text        NOT NULL CONSTRAINT household_roles_name_not_blank_check CHECK (btrim(name) <> ''),
    description            text,
    system_code            text        CONSTRAINT household_roles_system_code_check CHECK (system_code IN ('adult', 'child', 'dependent', 'other')),
    counts_as_adult        boolean     NOT NULL DEFAULT true,   -- naming: named in salutations; coverage rules: counts against adult caps
    implies_dependent      boolean     NOT NULL DEFAULT false,  -- coverage rules: counts against dependent caps
    is_active              boolean     NOT NULL DEFAULT true,
    display_order          integer,
    -- sys_*
    CONSTRAINT household_roles_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT household_roles_name_uq UNIQUE (tenant_id, name)
);
CREATE UNIQUE INDEX household_roles_system_code_uidx ON crm.household_roles (tenant_id, system_code) WHERE system_code IS NOT NULL;

-- CRM's semantics for a contact-to-contact association type (what platform.association_types does not carry).
CREATE TABLE crm.relationship_type_settings (
    tenant_id              text        NOT NULL,
    association_type_id    text        NOT NULL,  -- platform.association_types.id, verified at write
    kind                   text        NOT NULL CONSTRAINT relationship_type_settings_kind_check CHECK (kind IN
                                       ('spouse', 'partner', 'parent_child', 'guardian_dependent', 'grandparent_grandchild', 'sibling', 'caregiver_dependent', 'authorized_representative', 'other')),
    symmetric              boolean     NOT NULL DEFAULT false,
    may_authorize_access   boolean     NOT NULL DEFAULT false,
    requires_staff_confirmation boolean NOT NULL DEFAULT false, -- for authorized_representative between adults
    counts_for_naming      boolean     NOT NULL DEFAULT false,
    system_code            text,
    -- sys_*
    CONSTRAINT relationship_type_settings_pk PRIMARY KEY (tenant_id, association_type_id),
    CONSTRAINT relationship_type_settings_access_check CHECK (NOT may_authorize_access OR kind IN ('parent_child', 'guardian_dependent', 'caregiver_dependent', 'authorized_representative')),
    CONSTRAINT relationship_type_settings_symmetric_check CHECK (NOT symmetric OR kind IN ('spouse', 'partner', 'sibling', 'other'))
);
CREATE UNIQUE INDEX relationship_type_settings_system_code_uidx ON crm.relationship_type_settings (tenant_id, system_code) WHERE system_code IS NOT NULL;

SELECT platform.attach_query_description_stamp('crm.household_types', 'tenant_id');
SELECT platform.attach_query_description_stamp('crm.household_roles', 'tenant_id');
SELECT platform.attach_query_description_stamp('crm.relationship_type_settings', 'tenant_id');
```

Seeded per tenant as a baseline seed set (D-48), never a migration insert, and never protected beyond "cannot delete
while referenced; deactivate instead": household type `household`; roles `adult`, `child` (dependent), `dependent`
(dependent, any age: a dependent parent), `other`; association types `crm_spouse` (symmetric, naming), `crm_partner`
(symmetric, naming), `crm_parent_child` (access), `crm_guardian_dependent` (access), `crm_grandparent_grandchild`,
`crm_sibling` (symmetric), `crm_caregiver_dependent` (access), `crm_authorized_representative` (access, staff
confirmation), each with its settings row. Code finds a seeded row by `system_code` and degrades gracefully when a
tenant deactivated it (no naming order from spouse; no guardian derivation from that type).

### 2a. Relationship tables, as ruled (HC-D1)

Andrew ruled on 2026-10-21 that person-to-person relationships live in CRM-owned tables, not Platform associations. This
section replaces `crm.relationship_type_settings` (§2) with the two tables below; the semantics (symmetric or
directional, access-authorising, naming) move onto the type. Everything else in the proposal that reads relationships
(naming, guardian derivation, coverage predicates, Fundraising recognition, duplicate scoring) reads these tables
instead.

```sql
CREATE TABLE crm.relationship_types (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT relationship_types_id_configuration_id_check CHECK (crm.configuration_id_check(id)),
    name                   text        NOT NULL CONSTRAINT relationship_types_name_not_blank_check CHECK (btrim(name) <> ''),
    forward_label          text        NOT NULL,   -- shown on contact A: "Parent of"
    reverse_label          text        NOT NULL,   -- shown on contact B: "Child of" (equals forward_label when symmetric)
    symmetric              boolean     NOT NULL DEFAULT false,
    kind                   text        NOT NULL CONSTRAINT relationship_types_kind_check CHECK (kind IN
                                       ('spouse', 'partner', 'parent_child', 'guardian_dependent', 'grandparent_grandchild', 'sibling', 'caregiver_dependent', 'authorized_representative', 'other')),
    may_authorize_access   boolean     NOT NULL DEFAULT false,
    requires_staff_confirmation boolean NOT NULL DEFAULT false,
    counts_for_naming      boolean     NOT NULL DEFAULT false,
    one_open_per_contact   boolean     NOT NULL DEFAULT false,   -- spouse: at most one open row per contact
    dated                  boolean     NOT NULL DEFAULT true,
    system_code            text,
    is_active              boolean     NOT NULL DEFAULT true,
    display_order          integer,
    -- sys_*
    CONSTRAINT relationship_types_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT relationship_types_name_uq UNIQUE (tenant_id, name),
    CONSTRAINT relationship_types_access_check CHECK (NOT may_authorize_access OR kind IN ('parent_child', 'guardian_dependent', 'caregiver_dependent', 'authorized_representative')),
    CONSTRAINT relationship_types_symmetric_labels_check CHECK (NOT symmetric OR forward_label = reverse_label)
);
CREATE UNIQUE INDEX relationship_types_system_code_uidx ON crm.relationship_types (tenant_id, system_code) WHERE system_code IS NOT NULL;
SELECT platform.attach_query_description_stamp('crm.relationship_types', 'tenant_id');

CREATE TABLE crm.contact_relationships (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT contact_relationships_id_uuid_v7_check CHECK (crm.uuid_v7_check(id)),
    relationship_type_id   text        NOT NULL,
    contact_a_id           text        NOT NULL,   -- the "forward" side; for a symmetric type the lower contact id (canonical order, enforced by the write path and a CHECK below)
    contact_b_id           text        NOT NULL,
    start_date             date,
    end_date               date,
    end_reason             text        CONSTRAINT contact_relationships_end_reason_check CHECK (end_reason IN ('ended', 'deceased', 'merged', 'correction')),
    notes                  text,
    -- sys_*
    CONSTRAINT contact_relationships_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT contact_relationships_type_fk FOREIGN KEY (tenant_id, relationship_type_id) REFERENCES crm.relationship_types (tenant_id, id),
    CONSTRAINT contact_relationships_a_fk FOREIGN KEY (tenant_id, contact_a_id) REFERENCES crm.contacts (tenant_id, id),   -- no delete action: deletion goes through the impact contract
    CONSTRAINT contact_relationships_b_fk FOREIGN KEY (tenant_id, contact_b_id) REFERENCES crm.contacts (tenant_id, id),
    CONSTRAINT contact_relationships_not_self_check CHECK (contact_a_id <> contact_b_id),
    CONSTRAINT contact_relationships_dates_check CHECK (end_date IS NULL OR start_date IS NULL OR end_date >= start_date),
    CONSTRAINT contact_relationships_ended_check CHECK ((end_date IS NULL) = (end_reason IS NULL))
);
-- No second open row of one type between one pair, in either order (the key sorts the pair).
ALTER TABLE crm.contact_relationships ADD CONSTRAINT contact_relationships_no_overlap_excl EXCLUDE USING gist (
    crm.overlap_exclusion_key(tenant_id, relationship_type_id, least(contact_a_id, contact_b_id), greatest(contact_a_id, contact_b_id)) WITH =,
    daterange(start_date, end_date, '[]') WITH &&
) DEFERRABLE INITIALLY IMMEDIATE;
CREATE INDEX contact_relationships_a_idx ON crm.contact_relationships (tenant_id, contact_a_id, relationship_type_id, end_date) INCLUDE (contact_b_id, start_date);
CREATE INDEX contact_relationships_b_idx ON crm.contact_relationships (tenant_id, contact_b_id, relationship_type_id, end_date) INCLUDE (contact_a_id, start_date);
-- one_open_per_contact (spouse): a deferred constraint trigger counts open rows of the type on either side (a partial unique index cannot read the type)
-- limit: at most 200 relationships per contact, open and ended together
```

Rules the aggregate enforces (replacing A1 §2's "CRM's write path over Platform's"): a symmetric type is stored once
with the lower contact id as `contact_a_id` and the same label both ways; a directional type keeps the direction the
user chose; `one_open_per_contact` refuses a second open spouse naming the existing one; a relationship with a released
or archived contact is kept as history and never offered; ending is preferred to deleting; a delete of an ended row
corrects history. History is captured on both contacts' revision streams (the table registered twice, as the household
member table is). Merge: registry entries `contact_a_id` and `contact_b_id` (`reassign-dedupe` on `(type, pair)`,
collapsing colliders and removing a would-be self-relationship with a warning). Deletion: CRM's own cascade today and
the deletion-impact contract later (`detach` with tombstone). Queries: collections `relationships` and
`relationships_reverse` on the contacts dataset for `exists`, generated per active type as a configured-relationship
family (the affiliation-types precedent), so "contacts who are parent of a contact under 12" works without a join; a
child dataset `crm.contact_relationships` for lists. Import: a `crm.contact_relationships/upsert` operation with
`dependsOn: ['crm.contacts']` and blocking. The spouse backfill writes rows of the seeded `spouse` type; `spouse_id`
becomes a projection, then retires. The seeded system types (`spouse`, `partner`, `parent_child`, `guardian_dependent`,
`grandparent_grandchild`, `sibling`, `caregiver_dependent`, `authorized_representative`) are a baseline seed set with
`system_code`, renamable, deactivatable, never deletable while referenced.

What this ruling gives up and gains, for the record: it gives up Platform's already-built merge participant, delete
contract, history, import operation and automatic query families for these rows, and it introduces a second relationship
primitive beside associations (recorded as an exception to custom-objects D-04); it gains composite foreign keys inside
CRM, type-level semantics without waiting for Platform additions, and a single owner for everything about people in CRM.

### 3. CRM record tables

```sql
CREATE TABLE crm.households (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT households_id_uuid_v7_check CHECK (crm.uuid_v7_check(id)),
    number                 bigint      NOT NULL,                       -- platform.number_seeds scope crm.households
    household_type_id      text        NOT NULL,
    status                 text        NOT NULL DEFAULT 'active' CONSTRAINT households_status_check CHECK (status IN ('active', 'inactive')),
    name                   text        NOT NULL,                       -- computed, server-owned
    name_override          text        CONSTRAINT households_name_override_check CHECK (name_override IS NULL OR btrim(name_override) <> ''),
    mailing_name           text        NOT NULL,
    mailing_name_override  text        CONSTRAINT households_mailing_name_override_check CHECK (mailing_name_override IS NULL OR btrim(mailing_name_override) <> ''),
    formal_salutation      text        NOT NULL,
    formal_salutation_override text    CONSTRAINT households_formal_salutation_override_check CHECK (formal_salutation_override IS NULL OR btrim(formal_salutation_override) <> ''),
    informal_salutation    text        NOT NULL,
    informal_salutation_override text  CONSTRAINT households_informal_salutation_override_check CHECK (informal_salutation_override IS NULL OR btrim(informal_salutation_override) <> ''),
    name_format            text        CONSTRAINT households_name_format_check CHECK (name_format IN ('family', 'names', 'primary_only')),  -- NULL = tenant default
    region_id              text,
    override_automatic_region_assignment boolean NOT NULL DEFAULT false,
    notes                  text,
    dissolved_on           date,
    dissolution_reason     text        CONSTRAINT households_dissolution_reason_check CHECK (dissolution_reason IN ('separation', 'death', 'moved', 'duplicate', 'other')),
    archived_at            timestamptz, archived_by_id text, archive_reason text, unarchive_reason text,
    origin_service         text, origin_service_record_id text,
    sys_region_address_fingerprint text,
    sys_create_digest      text,
    -- sys_*
    CONSTRAINT households_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT households_number_uq UNIQUE (tenant_id, number),
    CONSTRAINT households_type_fk FOREIGN KEY (tenant_id, household_type_id) REFERENCES crm.household_types (tenant_id, id),
    CONSTRAINT households_region_fk FOREIGN KEY (tenant_id, region_id) REFERENCES crm.regions (tenant_id, id),
    CONSTRAINT households_dissolution_check CHECK ((dissolved_on IS NULL) = (dissolution_reason IS NULL)),
    CONSTRAINT households_archive_check CHECK ((archived_at IS NULL) = (archived_by_id IS NULL) AND (archived_at IS NULL) = (archive_reason IS NULL))
);
CREATE INDEX households_type_idx   ON crm.households (tenant_id, household_type_id);
CREATE INDEX households_status_idx ON crm.households (tenant_id, status) WHERE archived_at IS NULL;
CREATE INDEX households_region_idx ON crm.households (tenant_id, region_id);
-- crm.households_not_merged_id_trigger: refuses an id that is a merged-away alias

CREATE TABLE crm.household_members (
    tenant_id              text        NOT NULL,
    id                     text        NOT NULL CONSTRAINT household_members_id_uuid_v7_check CHECK (crm.uuid_v7_check(id)),
    household_id           text        NOT NULL,   -- immutable (trigger)
    contact_id             text        NOT NULL,   -- immutable (trigger)
    household_role_id      text        NOT NULL,
    is_primary_contact     boolean     NOT NULL DEFAULT false,
    is_primary_household   boolean     NOT NULL DEFAULT false,
    is_manager             boolean     NOT NULL DEFAULT false,
    exclude_from_naming    boolean     NOT NULL DEFAULT false,
    start_date             date,
    end_date               date,
    end_reason             text        CONSTRAINT household_members_end_reason_check CHECK (end_reason IN ('left', 'deceased', 'dissolved', 'merged', 'correction')),
    notes                  text,
    -- sys_*
    CONSTRAINT household_members_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT household_members_household_fk FOREIGN KEY (tenant_id, household_id) REFERENCES crm.households (tenant_id, id),
    CONSTRAINT household_members_contact_fk   FOREIGN KEY (tenant_id, contact_id)   REFERENCES crm.contacts   (tenant_id, id),
    CONSTRAINT household_members_role_fk      FOREIGN KEY (tenant_id, household_role_id) REFERENCES crm.household_roles (tenant_id, id),
    CONSTRAINT household_members_contact_household_uq UNIQUE (tenant_id, id, contact_id, household_id),   -- what a linked address names
    CONSTRAINT household_members_dates_check CHECK (end_date IS NULL OR start_date IS NULL OR end_date >= start_date),
    CONSTRAINT household_members_ended_check CHECK ((end_date IS NULL) = (end_reason IS NULL)),
    CONSTRAINT household_members_flags_open_check CHECK ((NOT is_primary_contact AND NOT is_primary_household AND NOT is_manager) OR end_date IS NULL)
);
CREATE UNIQUE INDEX household_members_one_primary_contact_uidx   ON crm.household_members (tenant_id, household_id) WHERE is_primary_contact AND end_date IS NULL;   -- I-1
CREATE UNIQUE INDEX household_members_one_primary_household_uidx ON crm.household_members (tenant_id, contact_id)   WHERE is_primary_household AND end_date IS NULL; -- I-2
ALTER TABLE crm.household_members ADD CONSTRAINT household_members_no_overlap_excl EXCLUDE USING gist (                                              -- I-3
    crm.overlap_exclusion_key(tenant_id, household_id, contact_id) WITH =,
    daterange(start_date, end_date, '[]') WITH &&
) DEFERRABLE INITIALLY IMMEDIATE;
CREATE INDEX household_members_household_idx ON crm.household_members (tenant_id, household_id, end_date) INCLUDE (contact_id, household_role_id, is_primary_contact, start_date);
CREATE INDEX household_members_contact_idx   ON crm.household_members (tenant_id, contact_id, end_date)   INCLUDE (household_id, household_role_id, is_primary_household, start_date);
CREATE INDEX household_members_current_idx   ON crm.household_members (tenant_id, household_id) WHERE end_date IS NULL;
CREATE INDEX household_members_role_idx      ON crm.household_members (tenant_id, household_role_id);
-- MAX_HOUSEHOLD_MEMBERS = 50 (open and ended), checked under the household row lock
```

`household_addresses` and `household_phone_numbers` follow the organization tables column for column (type, preferred,
values, seasonal `MM-DD` window, `seasonal_preferred`; one row per type; one preferred; seasonal never preferred; owned
FK `ON DELETE CASCADE`). The address and phone **type** tables' `applies_to` check gains `households` and the
combinations that include it; the seed adds a `home` address type for contacts and households. No household e-mail.

### 4. Changes to existing CRM tables

```sql
-- Contact addresses and, identically, phones: a fifth link source.
ALTER TABLE crm.contact_addresses
    ADD COLUMN source_household_member_id text, ADD COLUMN source_household_id text, ADD COLUMN source_household_address_id text,
    ADD COLUMN copied_from_household_id text, ADD COLUMN disconnected_from_household_id text,
    ADD CONSTRAINT contact_addresses_source_household_member_fk FOREIGN KEY (tenant_id, source_household_member_id, contact_id, source_household_id)
        REFERENCES crm.household_members (tenant_id, id, contact_id, household_id) DEFERRABLE INITIALLY IMMEDIATE,
    ADD CONSTRAINT contact_addresses_source_household_address_fk FOREIGN KEY (tenant_id, source_household_address_id, source_household_id)
        REFERENCES crm.household_addresses (tenant_id, id, household_id) DEFERRABLE INITIALLY IMMEDIATE;
-- contact_addresses_source_check: owned → no source columns; copied → exactly one provenance (organization or household) + copied_at;
--   linked → exactly one of the organization triple or the household triple; disconnected → exactly one of the two disconnected ids + at + reason.
-- Disconnected reasons gain 'member_ended' and 'household_address_removed'. The linked-values check and the effective views are extended, not changed.

ALTER TABLE crm.activity_links ADD COLUMN household_id text,
    ADD CONSTRAINT activity_links_household_fk FOREIGN KEY (tenant_id, household_id) REFERENCES crm.households (tenant_id, id);
-- activity_links_subject_check: exactly one of contact_id, organization_id, household_id
ALTER TABLE crm.tasks ADD COLUMN household_id text,
    ADD CONSTRAINT tasks_household_fk FOREIGN KEY (tenant_id, household_id) REFERENCES crm.households (tenant_id, id) ON DELETE SET NULL;
-- MERGE_OBJECT_TYPES gains 'households'; merged_record_aliases, merge_plans, merges accept it; number seeds scope crm.households;
-- record_revisions record_type 'crm.households'; the household_members stream registered on both the household and the contact captures.
```

Merge reference registry entries: `household_members.contact_id` (`reassign-dedupe` on `(household_id, contact_id)`,
keep the earliest open row, end the other `merged`), `household_members.household_id` (same key, household merges),
`households.*_override` (scalar conflicts), `activity_links.household_id` and `tasks.household_id` (`reassign`),
`contact_addresses.source_household_*` (second order, follows the member row), `relationship_type_settings` (none:
configuration). Coverage-guard name patterns gain `household_id` and `*household_member_id`.

### 5. Query views

```sql
CREATE VIEW crm.households_for_queries WITH (security_invoker = true) AS
SELECT h.*,
  (SELECT count(*) FROM crm.household_members m WHERE m.tenant_id = h.tenant_id AND m.household_id = h.id AND m.end_date IS NULL) AS current_member_count,
  (SELECT count(*) FROM crm.household_members m WHERE m.tenant_id = h.tenant_id AND m.household_id = h.id)                        AS member_count,
  (SELECT m.contact_id FROM crm.household_members m WHERE m.tenant_id = h.tenant_id AND m.household_id = h.id AND m.is_primary_contact AND m.end_date IS NULL) AS primary_contact_id,
  (SELECT min(c.date_of_birth) FROM crm.household_members m JOIN crm.contacts c ON c.tenant_id = m.tenant_id AND c.id = m.contact_id
     WHERE m.tenant_id = h.tenant_id AND m.household_id = h.id AND m.end_date IS NULL) AS oldest_member_date_of_birth,
  (SELECT max(c.date_of_birth) FROM crm.household_members m JOIN crm.contacts c ON c.tenant_id = m.tenant_id AND c.id = m.contact_id
     WHERE m.tenant_id = h.tenant_id AND m.household_id = h.id AND m.end_date IS NULL) AS youngest_member_date_of_birth
FROM crm.households h;

CREATE VIEW crm.household_members_current WITH (security_invoker = true) AS
SELECT m.* FROM crm.household_members m
WHERE (m.start_date IS NULL OR m.start_date <= crm.business_date(m.tenant_id)) AND (m.end_date IS NULL OR m.end_date >= crm.business_date(m.tenant_id));

-- contacts_for_queries gains: household_count (open member rows), primary_household_id (through the one-primary index)
```

One row per household by construction (correlated subqueries, never a join). `rhythm_query_reader` grants are generated
from the registered columns.

### 6. Index decisions (for `docs/migrations/postgres-indexing/README.md`)

| Index                                          | Serves                                                                     |
| ---------------------------------------------- | -------------------------------------------------------------------------- |
| `household_members_one_primary_contact_uidx`   | I-1; the `primary_contact_id` pick                                         |
| `household_members_one_primary_household_uidx` | I-2; the `primary_household` pick on contacts (proven single-row)          |
| `household_members_no_overlap_excl`            | I-3 only; its expression key means no read ever chooses it                 |
| `household_members_household_idx`              | a household's members with status; Household 360; counts                   |
| `household_members_contact_idx`                | a contact's households; Contact 360 card; `households` collection `exists` |
| `household_members_current_idx`                | current members: naming, coverage proposals, mailings, derivations         |
| `household_members_role_idx`                   | "households with a member in role X"; configuration delete refusal         |
| `households_status_idx` (partial)              | discoverable lists and pickers                                             |
| `households_type_idx`, `households_region_idx` | filters                                                                    |
| contact address `source_household_member_id`   | ending a member row finds the links to disconnect                          |

### 7. APIs

CRM routes (snake_case JSON; cursor paging `limit` 1–100; W1 versions; W3 omitted means unchanged):

| Operation                               | Route                                                                                                                                                                                | Permission                                                                                                                                                                                               |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List, create, read, replace, patch      | `GET                                                                                                                                                                                 | POST /households/{t}`, `GET                                                                                                                                                                              | PUT                                                   | PATCH /households/{t}/{id}`                                                                                 | `crm.households.read/create/update`         |
| Archive, unarchive, dissolve, reinstate | `POST /households/{t}/{id}/archive                                                                                                                                                   | unarchive                                                                                                                                                                                                | dissolve                                              | reinstate`                                                                                                  | `crm.households.archive/dissolve/reinstate` |
| Members                                 | `GET                                                                                                                                                                                 | POST /households/{t}/{id}/members`, `PUT /…/members/{memberId}`, `POST /…/members/{memberId}/end                                                                                                         | make_primary_contact`, `DELETE /…/members/{memberId}` | `crm.household-members.*`                                                                                   |
| Apply household address to members      | `POST /households/{t}/{id}/addresses/{addressId}/apply_to_members`                                                                                                                   | `crm.households.update`                                                                                                                                                                                  |
| A contact's households                  | `GET /contacts/{t}/{id}/households`                                                                                                                                                  | `crm.contacts.read`                                                                                                                                                                                      |
| Relationships                           | `GET                                                                                                                                                                                 | POST /contacts/{t}/{id}/relationships`, `POST /…/relationships/{relId}/end`, `DELETE /…/relationships/{relId}` (CRM validates semantics, calls Platform's association functions in the same transaction) | `crm.relationships.*`                                 |
| Relationship types                      | `GET                                                                                                                                                                                 | POST                                                                                                                                                                                                     | PUT                                                   | DELETE /relationship_types/{t}[/{id}]` (a CRM facade over Platform association types plus the settings row) | `crm.relationship-types.*`                  |
| Configuration                           | `/household_types/{t}`, `/household_roles/{t}`                                                                                                                                       | `crm.household-types.*`, `crm.household-roles.*`                                                                                                                                                         |
| History                                 | `GET /households/{t}/{id}/history[...]`                                                                                                                                              | `crm.households.history`                                                                                                                                                                                 |
| Portal (last)                           | `GET /households/{t}/portal/me/households`, `PUT /…/households/{id}/contact_information`, `POST /…/households/{id}/members`, `POST /…/members/{memberId}/end`, `POST /…/invitations` | portal keys under `requireAccess`                                                                                                                                                                        |

Published service functions (SQL, `SECURITY DEFINER`, executable by the other services' roles, in the caller's
transaction, as the merge participant functions are): `crm.household_of_contact_on`, `crm.household_primary_contact`,
`crm.relationships_of`, `crm.guardians_of`.

Membership routes added by chapter [11](#12-11-membership-coverage-and-benefit-inheritance-strategy):
`GET|POST /memberships/{t}/{id}/beneficiaries`, `POST /…/beneficiaries/{bId}/end | spin_off`,
`GET /memberships/{t}/{id}/beneficiary_proposals`, `GET /memberships/{t}/entitlements`, and the package coverage-rule
configuration under `/membership_packages/{t}/{id}/coverage_rules`.

### 8. Example rows

See chapter [09 §7](#10-09-recommended-domain-model) for the family; the rows:

```sql
INSERT INTO crm.households (tenant_id, id, number, household_type_id, status, name, mailing_name, formal_salutation, informal_salutation)
VALUES ('acme', '0192f1a0-5b2e-7c10-8d4e-aa0000000001', 10021, 'household', 'active',
        'The Rivera-Chen Family', 'Ms. Jane Rivera and Mr. Wei Chen', 'Ms. Rivera and Mr. Chen', 'Jane and Wei');

INSERT INTO crm.household_members (tenant_id, id, household_id, contact_id, household_role_id, is_primary_contact, is_primary_household, is_manager, start_date) VALUES
 ('acme', '…bb01', '…aa01', '<jane>', 'adult',     true,  true,  true,  '2019-06-01'),
 ('acme', '…bb02', '…aa01', '<wei>',  'adult',     false, true,  false, '2019-06-01'),
 ('acme', '…bb03', '…aa01', '<maya>', 'child',     false, true,  false, '2015-03-12'),
 ('acme', '…bb04', '…aa01', '<leo>',  'child',     false, false, false, '2019-06-01'),   -- primary household elsewhere
 ('acme', '…bb05', '…aa01', '<rosa>', 'dependent', false, true,  false, '2024-02-01');

INSERT INTO crm.household_addresses (tenant_id, id, household_id, type_id, is_preferred, line_1, city, state, postal_code, country)
VALUES ('acme', '…cc01', '…aa01', 'home', true, '14 Elm St', 'Decatur', 'GA', '30030', 'US');

-- Maya's Home address is the household's: a linked row with no value of its own.
INSERT INTO crm.contact_addresses (tenant_id, id, contact_id, type_id, source_mode, source_household_member_id, source_household_id, source_household_address_id)
VALUES ('acme', '…dd01', '<maya>', 'home', 'linked', '…bb03', '…aa01', '…cc01');

-- Jane spouse of Wei: one Platform association of the symmetric seeded type, canonical order.
-- POST /contacts/acme/<jane>/relationships { association_type_id: 'crm_spouse', other_contact_id: '<wei>', start_date: '2012-09-15' }

-- Membership (chapter 11): Jane holds M-1; three beneficiary rows.
INSERT INTO membership.membership_beneficiaries (tenant_id, id, membership_id, contact_id, coverage_rule_id, coverage_role, basis, basis_record_type, basis_record_id, designated_by_contact_id, start_date, covered_membership_id) VALUES
 ('acme', '…e1', 'M-1', '<wei>',  'rule-adult',     'adult',     'designated', 'platform.associations',  '<jane-wei-spouse>', '<jane>', '2026-06-01', 'M-1-wei'),
 ('acme', '…e2', 'M-1', '<maya>', 'rule-dependent', 'dependent', 'designated', 'crm.household_members',  '…bb03',             '<jane>', '2026-06-01', 'M-1-maya'),
 ('acme', '…e3', 'M-1', '<leo>',  'rule-dependent', 'dependent', 'designated', 'crm.household_members',  '…bb04',             '<jane>', '2026-06-01', 'M-1-leo');
```

## 12. 11. Membership Coverage and Benefit Inheritance Strategy

## 11. Membership Coverage and Benefit Inheritance Strategy

**Recommendation.** Keep each membership held by one contact or one organization. Record coverage for other people as
explicit beneficiary rows, whether designated by a person or derived from CRM facts. Package rules determine who
qualifies and how coverage changes; consumers read the recorded entitlement rather than deriving it from relationships.

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence:
[r2](#22-rhythm-2-0-membership-what-exists-today-read-for-the-household-membership-proposal) (services-membership `fcbddf2` and the Membership architecture),
[r7](#27-r7-rhythm-1-0-relationships-membership-flowdown-and-eligibility-precisely) (Rhythm 1.0 flowdown and seats),
[r3](#23-r3-how-the-money-handling-and-engagement-applications-identify-the-person-or-organization) §6 and §10, and the competitive chapters
[05](#6-05-competitive-research) §10 and §11. The Membership baseline is preserved: programs (national, chapter,
affiliate, section), types within programs, packages and pricing, multiple concurrent memberships, optional exclusivity,
program-specific standing and no global "is member" flag.

### 1. Six concepts, kept apart

The brief's distinction anchors this design. Each row represents a different fact with a different owner. The model
keeps these facts separate and never lets one stand in for another.

| Concept                  | Question it answers                                       | Owner                                 | Representation                                                                                                                                                                                           |
| ------------------------ | --------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Constituent relationship | How are these two people related?                         | CRM                                   | A Platform association of a CRM-seeded or tenant type (chapter [09](#10-09-recommended-domain-model))                                                                                                     |
| Household membership     | Is this person part of this mailing and recognition unit? | CRM                                   | `crm.household_members`                                                                                                                                                                                  |
| Membership ownership     | Whose membership is this?                                 | Membership                            | `memberships.owner_kind` + id: a **contact or an organization** (unchanged)                                                                                                                              |
| Membership beneficiary   | Who is covered by it, in what role, since when, and why?  | Membership                            | **New** `membership.membership_beneficiaries` row with `coverage_role`, dates and a `basis`; materialised as a child membership row                                                                      |
| Membership eligibility   | Who may hold or be covered, under which rules?            | Membership (rules), reading CRM facts | Package **coverage rules**: caps, age limits and the CRM predicates a beneficiary must satisfy (household member of the holder, related to the holder by type, affiliated to the organization in a role) |
| Membership entitlement   | What does being holder or beneficiary confer?             | Membership                            | The type's `confers_benefits` and program policy today; a benefit catalogue when Membership designs one; every consumer asks "entitled under program P?" and gets a membership row with its basis        |
| Financial responsibility | Who pays?                                                 | Commerce and Accounting               | `buyer` and `bill_to` on the order, `bill_to` on the invoice, the payment's card holder: a contact or an organization, never a household                                                                 |

### 2. The holder: one named party, never a household

> **As ruled (HC-D3, Andrew 2026-10-21).** A household **may** hold a membership. The recommendation below was not
> adopted; it is kept as the record of the argument. What changes: `memberships.owner_kind` gains `household` with a
> `household_id` column (a soft reference to `crm.households`, verified at write), allowed only for programs whose
> `member_kinds` include `household`; the owner CHECK and identity indexes gain the third kind; `prepare` accepts a
> household recipient; Commerce's recipient and entitlement-holder kinds gain `household` (buyer, bill-to, ship-to and
> every Accounting party stay `contact | organization`); a household-held membership requires `billing_contact_id`
> (defaulting to the household's primary contact), which is the invoice party; renewals and notices go to the billing
> contact; auto-renew uses the payer's stored method. Everything in §3 to §9 applies unchanged to a household-held
> membership: coverage is still explicit beneficiary rows with a basis, proposed from the household's members; the
> household is never a bill-to or payer; `/entitlements` answers `role: holder` only for a contact or organization and
> `role: beneficiary` for every covered person of a household-held membership (a household itself is never entitled).
> Household merge re-points `household_id`; dissolution lists memberships the household holds for transfer or ending.

**Recommendation.** A membership is held by exactly one contact or one organization, as today. A household is not a
holder kind. This reverses the direction of this proposal's first draft and departs from the household-customer model of
NetForum Enterprise. The reasons below are weighed against the brief's criteria: simplicity, fewer special cases and
less coupling.

1. **Everything money-related already assumes a contact or organization**: Commerce's buyer, bill-to and recipient,
   Accounting's bill-to, the stored payment method, the renewal notice address and the portal `/me`. A household holder
   would add a third value to a closed vocabulary in Membership's owner CHECK and identity indexes, Commerce's recipient
   and entitlement-holder kinds and contracts packages, snapshots, exports and the polymorphic holder reference. It
   would still need a billing contact because a household cannot be billed. That is a dozen branch points to support a
   holder label.
2. **Two existing or decided operations cover the cases a household holder was meant to solve**: owner transfer
   (Membership D-43, Planned) when the holder dies or leaves, and the beneficiary roster for everyone else. Altru,
   Raiser's Edge, Neon, Bloomerang and Tessitura hold memberships on a person with named others. Tessitura's v12 added
   Owner and Initiator precisely to keep the person attributable. Only NetForum made the household a transacting
   customer, with the resulting household address and merge defects.
3. **"Is this individual entitled under this program, and why?" has one answer shape** when the holder is always a party
   that can transact and the beneficiary is always an explicit row: _principal holder_ or _beneficiary of M held by H,
   basis B_. A household holder would add a third answer ("your household holds it") that would still have to resolve to
   people at the gate.
4. **Membership stays decoupled from CRM households.** Membership never stores a household id. It reads CRM facts
   (household membership, relationships, affiliations, birth dates) only inside coverage rules, through published reads.
   A tenant with no households therefore uses the same packages with relationship or designation rules.

Household 360 instead shows the memberships **held by** its current members and the memberships **covering** them. This
is a read, not a reference. A household manager renews or adjusts a family membership through a grant, not through
ownership ([A4 Security](#19-a4-security-privacy-and-authorization)). Making a household a holder later remains possible (the
polymorphic reference accepts a new target). It is deferred until a customer requirement identifies a case this model
cannot serve ([14](#15-14-architecture-decisions-requiring-approval) HC-D3).

### 3. Beneficiaries: explicit rows, two ways to get one

A beneficiary is a dated row naming the covered person, their coverage role and the **basis** that justifies the row.
There are two bases; the package decides which it uses:

| Basis           | How the row comes to exist                                                                                                                                                                                                                                           | When it ends                                                                                                                                                                                      | Used for                                                                                                                                                  |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Designation** | The buyer, a manager or staff names the person at purchase, renewal or mid-term, within the package's caps and predicates; Membership **proposes** candidates from CRM (household members, related contacts, affiliated contacts) and the user confirms              | Explicit removal; the membership's end; an age-out (per the package's basis); a predicate that stops holding, **if** the package says coverage follows the predicate; otherwise an attention item | Family memberships (museum, zoo, club), named guests, caregivers, designated employees, grandchildren elsewhere, any case where someone chooses who is in |
| **Derivation**  | A package rule says "everyone who satisfies predicate P is covered"; Membership evaluates P against CRM facts at purchase and **re-evaluates on CRM change events and a nightly sweep**, adding and ending rows itself (today's organization flow-down, generalised) | The predicate stops holding (the employee leaves, the role ends, the child turns 26, the member leaves the household), on the event or the sweep                                                  | Organizational memberships ("all employees in role Member"), chapter flow-down, and a tenant that wants "spouse and children automatically covered"       |

Both bases produce the same row and the same child membership. Every reader, including status functions, the dataset,
snapshots, Eligibility, Events pricing and the gate, therefore sees the same representation. The row is also the audit
trail: who designated whom and when, or which rule derived coverage and from which CRM fact. The fact's id and type are
stored on the row as `basis_record_type` and `basis_record_id`, for example `crm.household_members`/`<id>`,
`platform.associations`/`<id>` or `crm.contact_organization_affiliations`/`<id>`.

**Why not dynamic evaluation at read time.** Answering "is Maya entitled right now?" by walking Maya's relationships to
Jane and Jane's memberships at every read is simple to describe but difficult to operate. It cannot be audited because
editing a relationship changes the answer retroactively. It cannot be capped (how many children count?) or priced (which
add-on seat did Maya use?). It cannot survive the holder's divorce without a rule specifying which relationships still
count. This is the shape behind CiviCRM's non-transitive inheritance and NetForum's dual-flag flow-down. Derivation on
events retains the convenience without that ambiguity: the rule produces rows, the rows are the truth, and a change
produces a new row or ends an existing one with a timestamp and a reason.

**Why not designation only.** Organizational memberships need derivation today: every role holder is covered, and the
roster changes with HR rather than with a membership desk. A church or club that wants "your spouse is automatically
covered" is also a legitimate configuration. One mechanism with two bases creates fewer special cases than a flow-down
engine for organizations and a separate roster for families.

### 4. Coverage rules on the package

A package (the Accounting product that a tenant configures and prices) carries zero or more coverage rules. Each rule
has a **coverage role**, a **predicate** over CRM facts, a **cap**, an **age window**, and a **basis mode**:

| Rule field           | Values                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `coverage_role`      | `adult`, `dependent`, `guest`, `employee`, `designee` (closed list; the label a tenant shows is configurable)                                                                                                                                                                                                                                                                                                                                      |
| `predicate`          | `any_contact` (designation only); `household_member_of_holder` (optionally restricted to household roles); `related_to_holder` with relationship type ids and direction (spouse_of, child_of, grandchild_of, dependent_of, cared_for_by); `affiliated_to_holder` with contact role ids and organization types (the organization case, cascading to child organizations as today); `designated_contact` (named by the holder, no CRM fact required) |
| `max_count`          | Cap on open rows under this rule (null = unlimited)                                                                                                                                                                                                                                                                                                                                                                                                |
| `min_age`, `max_age` | Age window from `crm.contacts.date_of_birth` on the evaluation date (null = no bound); a missing birth date fails an age-bounded rule and is reported, never silently passes                                                                                                                                                                                                                                                                       |
| `age_basis`          | `term_start` (the age on the term's first day holds for the term; the market norm) or `continuous` (the row ends the day the bound is crossed)                                                                                                                                                                                                                                                                                                     |
| `basis_mode`         | `designate` (proposed, confirmed), `derive` (automatic), `designate_within_derived` (the derived set is the pool, the buyer picks up to the cap)                                                                                                                                                                                                                                                                                                   |
| `follows_predicate`  | When a designated person stops satisfying the predicate, end the row (`true`) or raise an attention item and keep it to the term's end (`false`, default)                                                                                                                                                                                                                                                                                          |
| `add_on_product_id`  | Rows beyond `max_count` may be bought at this product's price (an extra named adult, an extra caregiver)                                                                                                                                                                                                                                                                                                                                           |
| `benefit_scope`      | `full` or a named subset once a benefit catalogue exists (a caregiver admitted only with a dependent; a guest with no reciprocal privileges) — until then `full`, with the role visible to consumers that want to differ                                                                                                                                                                                                                           |

Examples:

- **Museum Family**: `adult`, `related_to_holder(spouse_of, partner_of) OR household_member_of_holder(role adult)`, max
  1, designate; `dependent`,
  `household_member_of_holder(role child, dependent) OR related_to_holder(child_of, grandchild_of, dependent_of)`, max
  4, max_age 18 at term start, designate; `guest`, `any_contact`, max 1, designate (named), plus an unnamed guest-pass
  allowance of 2 per term (a separate package field, consumed at the gate).
- **Zoo Grandparents**: `adult`, `any_contact`, max 1, designate (the other grandparent); `dependent`,
  `related_to_holder(grandchild_of) OR designated_contact`, max 6, max_age 17, designate. No household is needed: the
  grandchildren live elsewhere, and coverage is justified by the relationship or simply by naming them.
- **Family Plus with caregiver**: as Family plus `designee` ("Caregiver"), `any_contact`, max 1, designate,
  `benefit_scope: with_dependent`.
- **Corporate**: `employee`, `affiliated_to_holder(roles: Member, Staff; cascading organization types: Subsidiary)`,
  unlimited, **derive**, follows_predicate true; `designee`, `designated_contact`, max = purchased seats, designate,
  add-on seat product. An employee who leaves Acme loses the row when the affiliation ends (the CRM change event), as
  flow-down does today.
- **Church family (if the church sells a program at all)**: `adult`, `household_member_of_holder(role adult)`, derive,
  follows_predicate true; `dependent`, `household_member_of_holder(role child)`, derive, max_age 18 continuous.

Rules are configuration: a tenant's family program is data, not a special case in code. The predicates form a closed
list that Membership implements once against CRM's published reads. When the Eligibility engine resumes, it can host
those predicates as condition types without changing the rows.

### 5. The data model

```sql
-- One row per covered person per membership, with its justification.
CREATE TABLE membership.membership_beneficiaries (
    tenant_id              text NOT NULL,
    id                     text NOT NULL,                      -- UUIDv7
    membership_id          text NOT NULL,                      -- the principal membership (contact- or organization-held)
    contact_id             text NOT NULL,                      -- crm.contacts (soft reference, verified at write)
    coverage_rule_id       text,                               -- the package rule that admitted this row (null for a staff override)
    coverage_role          text NOT NULL CHECK (coverage_role IN ('adult', 'dependent', 'guest', 'employee', 'designee')),
    basis                  text NOT NULL CHECK (basis IN ('designated', 'derived', 'staff_override')),
    basis_record_type      text,                               -- 'crm.household_members' | 'platform.associations' | 'crm.contact_organization_affiliations' | null
    basis_record_id        text,
    designated_by_contact_id text,                             -- who named them (designation)
    start_date             date NOT NULL,
    end_date               date,
    end_reason             text CHECK (end_reason IN ('removed', 'aged_out', 'predicate_ended', 'membership_ended', 'transferred', 'spun_off', 'correction')),
    covered_membership_id  text,                               -- the materialised child memberships row (origin = 'covered')
    add_on_order_line_id   text,                               -- when a seat was bought for this person
    -- sys_* columns
    CONSTRAINT membership_beneficiaries_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT membership_beneficiaries_membership_fk FOREIGN KEY (tenant_id, membership_id) REFERENCES membership.memberships (tenant_id, id),
    CONSTRAINT membership_beneficiaries_dates_check CHECK (end_date IS NULL OR end_date >= start_date),
    CONSTRAINT membership_beneficiaries_ended_check CHECK ((end_date IS NULL) = (end_reason IS NULL)),
    CONSTRAINT membership_beneficiaries_basis_check CHECK ((basis = 'derived') = (basis_record_type IS NOT NULL AND basis_record_id IS NOT NULL) OR basis = 'designated')
);
CREATE UNIQUE INDEX membership_beneficiaries_open_uidx    ON membership.membership_beneficiaries (tenant_id, membership_id, contact_id) WHERE end_date IS NULL;
CREATE INDEX        membership_beneficiaries_contact_idx  ON membership.membership_beneficiaries (tenant_id, contact_id, end_date);
CREATE INDEX        membership_beneficiaries_rule_idx     ON membership.membership_beneficiaries (tenant_id, coverage_rule_id) WHERE end_date IS NULL;   -- cap counting

-- Coverage rules on the package.
CREATE TABLE membership.package_coverage_rules (
    tenant_id              text NOT NULL,
    id                     text NOT NULL,                      -- UUIDv7 (a record, not configuration: packages are tenant records keyed by product id)
    package_id             text NOT NULL,
    coverage_role          text NOT NULL,
    label                  text NOT NULL,
    predicate_kind         text NOT NULL CHECK (predicate_kind IN ('any_contact', 'household_member_of_holder', 'related_to_holder', 'affiliated_to_holder', 'designated_contact')),
    predicate              jsonb NOT NULL DEFAULT '{}',        -- role ids, relationship type ids and direction, contact role ids, organization type ids, cascade depth
    max_count              integer,
    min_age                integer, max_age integer,
    age_basis              text NOT NULL DEFAULT 'term_start' CHECK (age_basis IN ('term_start', 'continuous')),
    basis_mode             text NOT NULL CHECK (basis_mode IN ('designate', 'derive', 'designate_within_derived')),
    follows_predicate      boolean NOT NULL DEFAULT false,
    add_on_product_id      text,
    benefit_scope          text NOT NULL DEFAULT 'full',
    position               integer NOT NULL DEFAULT 0,
    -- sys_* columns
    CONSTRAINT package_coverage_rules_pk PRIMARY KEY (tenant_id, id),
    CONSTRAINT package_coverage_rules_package_fk FOREIGN KEY (tenant_id, package_id) REFERENCES membership.membership_packages (tenant_id, id)
);
-- Package fields: unnamed_guest_passes_per_term integer, adult_beneficiaries_may_renew boolean DEFAULT true.
```

Changes to existing Membership structures: `MEMBERSHIP_ORIGINS` gains `covered`. The child-sync service, which today
derives organization children from affiliations, becomes the **rule evaluator**. It derives and reconciles beneficiary
rows for every basis and materialises or ends child rows (`followParent` unchanged). The decided application's
`assigned_contacts` seeds `designated` rows, and `assigned_seat_limit` becomes a `designee` rule's `max_count`. The
`memberships_organization_principal_check` stays; nothing changes in `owner_kind`.

Membership adds `billing_contact_id` for organization holders to address today's `no_recipient` gap. The same field also
serves a holder who wants invoices sent to someone else; it is not a household feature.

### 6. Entitlement: one explainable answer

Every consumer asks Membership the same question and receives the same answer shape:

```
GET /memberships/{tenantId}/entitlements?contact_id=<maya>&program_id=family&as_of=2026-11-02
→ {
    "contact_id": "<maya>", "program_id": "family", "as_of": "2026-11-02",
    "entitled": true,
    "grounds": [
      {
        "membership_id": "M-10021", "role": "beneficiary", "coverage_role": "dependent",
        "status": "active", "term": { "starts_on": "2026-06-01", "ends_on": "2027-05-31" },
        "held_by": { "kind": "contact", "id": "<jane>", "name": "Jane Rivera" },
        "basis": { "kind": "designated", "by": "<jane>", "on": "2026-06-01",
                   "rule": { "id": "...", "label": "Dependents under 18", "predicate": "household_member_of_holder(child)" },
                   "fact": { "type": "crm.household_members", "id": "..." } },
        "benefit_scope": "full"
      }
    ]
  }
```

For Jane, the answer is `role: holder`. For an Acme employee, it is
`role: beneficiary, coverage_role: employee, basis: derived, fact: crm.contact_organization_affiliations/<id>`. A person
covered twice, as principal in Professional and beneficiary in Family, has two grounds under two programs. The gate, the
event registration pricer, the portal and the Household 360 card all render this object. None re-derives entitlement
from relationships on its own.

### 7. Lifecycle, with the beneficiary rows

| Event                          | Behaviour                                                                                                                                                                                                                                                            |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Purchase                       | `prepare` evaluates the package's rules for the holder: derives `derive` rows, proposes candidates for `designate` rows (from household members, related contacts, affiliates, in that order), checks caps and ages; placement writes the rows and child memberships |
| Renewal                        | The new term re-proposes: age-outs under `term_start` drop out with the reason shown; derived rows continue if the predicate holds; designated rows continue unless the buyer removes them; a dropped dependent is offered the program's `renews_into` suggestion    |
| Composition change (CRM event) | Derived rows end or appear per predicate; designated rows with `follows_predicate = false` raise an attention item ("John is still covered until 2027-05-31 but left the household on …") for staff or the holder                                                    |
| Age reached (`continuous`)     | Nightly sweep ends the row with `aged_out`; a notice 60 days before (configurable) to the holder                                                                                                                                                                     |
| Holder dies or leaves          | Owner transfer (D-43) to a named contact (the spouse, an adult beneficiary); rows continue; the portal manager grant follows the household, not the membership                                                                                                       |
| Divorce                        | The holder removes the former partner (or the predicate ends); the former partner may **spin off** into their own principal membership of a package the program names, priced by Commerce with the program's proration policy (D-24)                                 |
| Tier change mid-term           | The in-term amendment (D-23, D-24, P4); rules re-evaluated; rows over the new caps listed for decision before commit                                                                                                                                                 |
| Cancellation                   | Rows end `membership_ended`; refundable fraction per program                                                                                                                                                                                                         |
| Gift purchase                  | Buyer and bill-to = the giver; recipient = the holder (a contact), who may be the giver's daughter; beneficiaries proposed from the holder's facts; the giver is covered by nothing unless designated                                                                |
| Who may renew or pay           | The holder; an adult beneficiary when the package allows (`adult_beneficiaries_may_renew`, default true: either parent renews); a household manager through the CRM grant; staff. Coverage changes: the holder, a manager, staff                                     |

### 8. Scenarios from the brief

| Scenario                                                                                                                                 | Household record needed?                               | Model                                                                                                                                                                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Jane buys a family membership covering herself, spouse, three children                                                                   | **No** (helpful)                                       | Holder Jane; spouse designated under the adult rule (justified by spouse_of or household); children under the dependent rule. With a household, the proposal is one click; without, Jane picks related contacts or names them                                                                  |
| A child turns 18                                                                                                                         |                                                        | `term_start`: covered to term end, dropped at renewal with reason; `continuous`: row ends on the birthday                                                                                                                                                                                      |
| Jane and her spouse separate                                                                                                             |                                                        | Spouse removed (or predicate ends); spin-off offered; the children stay covered under Jane's membership regardless of which household they live in, because the rows name the children, not the house                                                                                          |
| A child lives in a different household                                                                                                   |                                                        | Irrelevant to coverage: the child is a designated dependent justified by `child_of` Jane; the household is a mailing unit                                                                                                                                                                      |
| Grandparents cover grandchildren who live elsewhere                                                                                      | **No**                                                 | Holder one grandparent; other grandparent `adult`; grandchildren `dependent` by `grandchild_of` or designation                                                                                                                                                                                 |
| Acme's organizational membership covers qualifying employees                                                                             | **No**                                                 | Holder Acme; `employee` rows **derived** from affiliations with the configured roles, cascading to subsidiaries; a leaver's row ends on the affiliation end event; the same evaluator, a different predicate                                                                                   |
| Church families                                                                                                                          | **Yes** for mailing, giving and care; not for coverage | Households and relationships in CRM; no membership program                                                                                                                                                                                                                                     |
| Donor recognition of household giving                                                                                                    | **Yes** (as basis)                                     | Fundraising recognition from household membership as of the gift date ([A3 Cross-application](#18-a3-cross-application-integration) §5)                                                                                                                                                                     |
| Foundation beneficiaries, dependents, sponsors, guardians                                                                                | Relationships, not households                          | Association types `beneficiary_of`, `sponsor_of`, `guardian_of`; households only where the foundation mails families                                                                                                                                                                           |
| Complex coverage (two named adults, children under an age, grandchildren elsewhere, a caregiver, an unnamed guest, employees, designees) | Mixed                                                  | Named adults: designate, max 2; children: dependent rule with age; grandchildren: relationship-justified designation; caregiver: `designee` with `benefit_scope`; unnamed guest: pass allowance, not a person; employees: derive by affiliation; designees: designate `any_contact` with seats |

The last row answers the brief's question directly: **relationships justify, rules bound, beneficiaries record.** No one
receives a benefit merely because they are related to the holder. They receive it because a beneficiary row exists, and
that row records which relationship or rule made the coverage legitimate.

### 9. Service boundaries

| Service    | Owns                                                                                                                                                                                          | Reads                                                                                                                                                                 | Never                                                                              |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| CRM        | Contacts, households, member rows, relationship semantics; publishes `household_members_current`, `relationships_of(contact, type ids)`, `affiliations_current`, birth dates, business events | —                                                                                                                                                                     | Stores anything about memberships or coverage                                      |
| Platform   | Associations (the relationship rows), derived assignments, Eligibility (parked)                                                                                                               | CRM objects for endpoint verification                                                                                                                                 | Decides coverage                                                                   |
| Membership | Memberships, beneficiaries, coverage rules, the rule evaluator, entitlement answers                                                                                                           | CRM's published reads inside `prepare`, the evaluator and the sweep; CRM business events (`crm.household_member.*`, `crm.contact_relationship.*`, affiliation events) | Stores a household id; evaluates entitlement by walking relationships at read time |
| Commerce   | Orders with buyer, bill-to, recipient; add-on seat lines; entitlements for Commerce products                                                                                                  | Membership's entitlement answer for member pricing                                                                                                                    | Learns a household kind (no vocabulary change in this design)                      |
| Accounting | Invoices to the bill-to                                                                                                                                                                       | —                                                                                                                                                                     | Learns a household kind                                                            |
| Events     | Registrations, attendees, guest-pass consumption                                                                                                                                              | Membership's entitlement answer per attendee; CRM's guardian grants                                                                                                   | Re-derives coverage                                                                |

### 10. What this design does not do, on purpose

There is no household holder and no household id in Membership. There is no read-time inheritance through relationships,
and no flags on relationship types that make benefits flow: the rule lives on the package. There is no second coverage
engine for organizations and no benefit catalogue (deferred to Membership's own design). Unnamed people are not
beneficiaries: an unnamed guest is an allowance, not a row. A CRM edit does not silently change coverage unless the
package asked for it.

## 13. 12. Migration and Implementation Implications

## 12. Migration and Implementation Implications

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation).

Rhythm 2.0 has no existing customers (Andrew, 2026-10-19), so there is no legacy household data to migrate and nothing
to grandfather. "Migration" here covers three things: the internal change from spouse links to associations, importing
household data from the systems prospects leave, and keeping households interoperable with external CRMs through the
Integration Platform. Evidence: [r4](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships) §7, [r6](#26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links) §6,
[05](#6-05-competitive-research).

Implementation starts with the CRM foundation after stabilization. Membership coverage follows when Membership resumes;
engagement work belongs in each application's plan, and portal work comes last. Nothing in Accounting, Commerce or
Billing changes at any phase.

### 1. The one internal migration: spouse links

`crm.contacts.spouse_id` (reciprocal in code since D-19) becomes a `crm_spouse` Platform association:

1. The CRM-M21a migration seeds the system association types and `relationship_type_settings` rows per tenant (seed set,
   not an insert).
2. A one-off data job runs per tenant. It reads every reciprocal pair once, using the lower id as `from`, and writes one
   open association with no dates because the pair's start is unknown. It lists half pairs (A names B, B names nobody)
   in a remediation report and creates an association for each with a data-quality flag, so staff can confirm or end
   them.
3. `spouse_id` becomes a server-owned projection of the open spouse association. It stays on the wire for the console's
   field and the duplicate scorer. The editor's spouse picker is removed in favour of the Relationships card, and the
   import mapper's `spouse_source_key` writes the association.
4. Once the console and scorer read the association (CRM-M21a's own scope), a later migration drops the column.

The job is idempotent: it skips a pair that already has the association. Like every data job, it runs
validate-then-apply. Its rollback removes the associations it created.

### 2. Importing households from other systems

Prospects arrive from the products in [05](#6-05-competitive-research). Each exports a shape that the Data Job Engine
maps onto four operations in dependency order: contacts → households → members, with relationships last. Imports use the
standard mapper and a preview the user confirms:

| Source                                    | Household source shape                                                                                                                                                                                       | Mapping                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Salesforce NPSP                           | Household Account (Id, Name, Formal and Informal Greeting, custom-name flag, Primary Contact), Contacts with AccountId, Address records (default, seasonal), Relationships (type, status), naming exclusions | One household per Household Account (`sys_external_id` = Account Id; overrides from greetings when the custom flag is set, else computed); member rows from AccountId (role by age), primary contact from `npe01__One2OneContact__c`; default Address → household Home, contacts without Address Override → linked, with Override → owned; seasonal → seasonal; Relationships → associations by a type map; "anonymous household" accounts skipped |
| Salesforce Nonprofit Cloud                | Business Account + Party Relationship Group, Account Contact Relationships (IsPrimaryMember, IsPrimaryGroup, Roles), Contact Contact Relationships                                                           | One household per group; member rows from ACRs with both primary flags; roles from the Roles multipicklist; relationships from CCRs (pairs collapsed to one)                                                                                                                                                                                                                                                                                       |
| Blackbaud Raiser's Edge                   | Constituents with spouse relationship and Head of Household flag; addressee and salutation per constituent; shared addresses; joint members on memberships                                                   | A household per spouse pair with the head as primary contact; children from Parent/Child relationships that are constituents (non-constituent relationships become contacts flagged "created from RE relationship" or are dropped per the mapper's choice); shared addresses → household Home with links; joint members → designated beneficiaries of the head's membership (Phase 2)                                                              |
| Blackbaud Altru / Blackbaud CRM           | Household constituent, members with primary contact, groups above households, recognition settings                                                                                                           | One household per household constituent; groups → a `giving_group` household type if the tenant wants them, else dropped; recognition settings → Fundraising's rule, not per household                                                                                                                                                                                                                                                             |
| NetForum Enterprise                       | Household customer with head of household and member relationship rows; memberships held by the household                                                                                                    | One household per customer; head → primary contact; memberships → memberships held by the head with the other members as designated beneficiaries (Phase 2 import)                                                                                                                                                                                                                                                                                 |
| iMIS, MemberClicks, Wild Apricot, Fonteva | Family modelled as an organisation or a bundle with a coordinator and linked profiles                                                                                                                        | The organisation or bundle becomes a household **when the mapper is told the member type is a family type**; the coordinator becomes primary contact and manager; linked profiles become members; the organisation record is not imported as an organization                                                                                                                                                                                       |
| Spreadsheets                              | One row per person with a household key or shared surname and address                                                                                                                                        | The mapper offers "group by household key" or "propose households from shared address and surname" with a preview; nothing is grouped without the user confirming                                                                                                                                                                                                                                                                                  |

The following rules apply to every source:

- **Names are computed on import**. They are overridden only when the source carries an explicit custom name or
  greeting. A source salutation that equals the computed one is not stored as an override.
- **One household per person by default**, with the source's primary flag honoured. A person appearing in two source
  households (rare in sources that forbid it) becomes a member of both, with the first as primary.
- **Addresses are linked, not copied**, when the source says the person used the household address; otherwise they are
  owned.
- **Duplicates**: contacts pass through the match-or-create function (CRM-ADR-03) before households are built. A
  household therefore never links a duplicate that the import itself created, addressing the 1.0 guest-import lesson.
  Household duplicate rules run after the import and surface in Data Quality.
- **Validate, then apply**, with per-row outcomes. A dependency row whose parent is missing is `blocked`, never
  rejected. A run's rollback removes the households, member rows and address links it created, and restores the contact
  address rows it converted to `linked`.
- **Crosswalk**: `sys_external_id` and `data_id_mappings` are maintained per object type (`crm.households`,
  `crm.household_members`). A second run therefore updates rather than duplicates, and a merge re-points the source keys
  to the survivor.

### 3. Interoperability with external CRMs

Households are ordinary catalogued objects in the Integration Platform (contracts 0.7.x):

- **Events**: `crm.household.created/updated/archived/unarchived/dissolved/reinstated/merged/deleted`,
  `crm.household_member.added/updated/ended/removed`, `crm.contact_relationship.created/ended/removed`. Payloads contain
  ids and changed field names only.
- **Reads**: the published views of [A3 §9](#18-a3-cross-application-integration), exposed through the API Gateway's CRM routes
  (`GET /households/{t}`, `/households/{t}/{id}`, `/households/{t}/{id}/members`, `/contacts/{t}/{id}/households`,
  `/contacts/{t}/{id}/relationships`).
- **Writes**: the same routes under the write contracts (versions, omitted means unchanged), with Data Job operations
  for bulk writes.
- **Outbound mappings**: for a tenant that keeps NPSP or Nonprofit Cloud in parallel, household ↔ Household Account or
  Party Relationship Group, member row ↔ AccountId or ACR, and association ↔ Relationship or CCR. The connector carries
  external ids in `sys_external_id` and the Integration Platform's id mappings. Where the external system enforces one
  household per person, the connector sends the contact's **primary household** and records the others as unmapped in
  its reconciliation report.
- **Identity on merge**: the alias (410 with survivor) and the `merged` event let a connector re-point its side. If a
  connector cannot do so because an external record has been deleted, it reports the problem; it never resurrects the
  merged household.

### 4. Sandbox and tenant lifecycle

Sandbox copy from a parent tenant does not exist yet (Platform tenant-lifecycle plan: "Defer"). When it does, household
tables copy with CRM's other tables under the `crm.entities` participant. Keys are `(tenant_id, id)`, so cross-table
references survive a tenant rewrite. Platform-owned associations copy with Platform's participant in the same run.

Tenant reset empties household tables with the rest of CRM. Tenant purge deletes them with history capture suppressed.
Tenant data export emits households and link datasets for member rows ([A3 §8](#18-a3-cross-application-integration)).

### 5. Integrity, audit and rollback expectations

Household data meets the same bar as contacts and organizations. UUIDv7 ids are assigned by the client where it can,
supporting replay-safe creates. CRM uses composite foreign keys and forced RLS. Every write is versioned (W1), no-op
writes are silent (W2), and relationship collections remain unchanged when omitted (W3).

Every committed change is recorded in `record_revisions` with actor and source. Business events go through the outbox.
The same requirements apply for merge aliases, archive before delete, and data jobs with validate-then-apply and narrow
rollback. Nothing household-specific weakens these requirements.

### 6. Implementation: what to build now, what to defer

Standing orders bound the sequence: stabilization first, with nothing new until the retroactive CRM milestones are
accepted; focus on CRM, Accounting and Integrations; Membership stopped on 2026-10-19; portal always last; console
changes only on explicit direction. Every milestone needs an approved plan (R2-DEV-001).

#### 6.1 Build now (once stabilization lifts): the CRM foundation, in two milestones

CRM-M21 in the CRM roadmap ("Relationships and households, after CRM-Q2") becomes two milestones. Each is a vertical
slice with its own walkthrough. Both are scheduled after CRM-M15, whose platform-facing contracts are needed by the
household merge participant and published reads, and before CRM-M22 (portal, last):

| Milestone                  | Purpose and scope                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Prerequisites                                                                                             | Explicit exclusions                                                    |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| **CRM-M21a Relationships** | Seed the system association types and `relationship_type_settings`; CRM relationship routes with symmetry and exclusivity rules; the spouse backfill, projection and picker retirement; the Contact 360 Relationships card; the configuration screen; duplicate-scoring and merge-warning rules; business events; the guardian derivation (Platform `crm` source) **without** portal routes                                                                             | CRM-M1..M8 accepted; Platform associations on master (done); Platform `crm` derivation source migration   | Portal; attributes on associations; Fundraising recognition (its plan) |
| **CRM-M21b Households**    | The `households` module (tables, aggregate, routes, metadata, dataset, recipes, Quick Search, history, events, seeds, data operations, merge planner and registry entries, permission keys); household addresses and the fifth link source; `applies_to` for households; activity and task subjects; computed naming with overrides and settings; Households list, Household 360, Contact 360 Household card, member editors, picker (console scope named for approval) | CRM-M21a; CRM-M15 published reads and participant conventions; Platform datasets and Quick Search entries | Custom fields unless HC-D5 approves; portal; anything in Membership    |

Each milestone gets a Milestone Implementation Plan (template sections 1–10, including the entity-capabilities table per
R2-ARCH-001), acceptance scenarios from [A5](#20-a5-testing-requirements), and the guided walkthrough of
[A5 §10](#20-a5-testing-requirements). Indicative size on the fleet: M21a about 6 agent-days, M21b about 10.

#### 6.2 Build when Membership resumes: coverage, one milestone

The **MEMB coverage milestone** gets its number when Membership gets its R2-DEV-001 record. Its scope covers
`membership_beneficiaries`, `package_coverage_rules`, `origin = covered`, and the rule evaluator generalised from
organization flow-down, including designated and derived bases, caps, age windows, sweep and attention items. It also
includes `billing_contact_id`, `/entitlements`, `/beneficiary_proposals`, spin-off, the application's
`assigned_contacts` seeding rows, and snapshot and dataset changes.

The Membership merge participant and delete-impact provider are prerequisites. The milestone also includes console
configuration for coverage rules and the beneficiaries editor, and the Household 360 Memberships tab.

Coverage depends on CRM-M21a (relationships) for relationship predicates and on CRM-M21b for household predicates. It
works without households for tenants that have none. Owner transfer (D-43) remains its own Membership item and gates the
death and separation scenarios. Indicative size is about 12 agent-days, plus P4 amendments (tier change) already
planned.

#### 6.3 Build in their own plans: engagement

Fundraising uses the household as the recognition basis, replacing the spouse-pair key. Its scope includes explicit
credit rows, household totals on a household-grain view, recognition name and opt-out, and anonymity on credits.

Marketing adds `household_primary_contact` and `household_members` audience kinds, the "once per household" rule,
`household.*` merge fields, and minors' consent.

Events adds `registered_by_contact_id`, consent signer, family registration from a household, guardian-verified minors,
and guest-pass consumption. Each application's plan cites this proposal's chapter [A3](#18-a3-cross-application-integration).

#### 6.4 Last: portal

Portal work covers household manager pages, guardian access to a minor's profile, invitations and representative access,
and family renewal and registration. It falls under CRM-M22 and the Membership and Events portal milestones, on Andrew's
components ([A4 §4](#19-a4-security-privacy-and-authorization)).

#### 6.5 Dependency diagram

```
Platform: associations (done) ─┬─▶ CRM-M21a Relationships ─▶ CRM-M21b Households ─▶ (CRM-M22 portal, last)
Platform: `crm` derivation source ┘           │                        │
                                              │                        ├─▶ Fundraising recognition basis (its plan)
CRM-M15 contracts ────────────────────────────┤                        ├─▶ Marketing householding (its plan)
                                              │                        └─▶ Events family registration (its plan)
Membership resumes + merge participant ───────┴─▶ MEMB coverage milestone ─▶ Membership portal (last)
Membership D-43 owner transfer, P4 amendments ─────────────────┘
```

#### 6.6 While CRM and Membership 2.0 are still under active development

Record the decisions in chapter [14](#15-14-architecture-decisions-requiring-approval) now; build nothing until the retroactive CRM
milestones are accepted. Three things can ride along with work already in flight at no scheduling cost, because each is
a small change inside a milestone that is already approved rather than a new household build: the Platform `symmetric`
flag and `system_code` on association types (the custom-objects milestone owns that module), the `crm` derivation source
migration (a one-file Platform change), and the spouse projection (CRM already keeps the pair reciprocal; the backfill
waits for M21a). Nothing in Accounting, Commerce or Billing changes at any phase, which is the point of the design.

## 14. 13. Risks and Unresolved Questions

## 13. Risks and Unresolved Questions

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation).

### 1. Risks (ranked)

| #    | Risk                                                                                                                                                      | Likelihood                                | Impact                                                     | Mitigation                                                                                                                                                                                          | Owner                 |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| R-1  | The relationship primitive rots again: associations get seeded types and a card but no consumer ships with them, and 1.0's history repeats                | Medium if milestones are split too finely | A second unused relationship feature; spouse field lingers | The first milestone that creates relationships also ships two readers (household naming, guardian derivation); Fundraising recognition and coverage rules follow in theirs                          | CRM plan              |
| R-2  | Membership and Accounting still have no merge participant; beneficiary rows and member rows add contact references that a merge today would strand        | High until participants land              | Dangling beneficiaries and member rows after merges        | CRM-ADR-12's merge block protects until participants exist; Membership's participant is a prerequisite of its coverage milestone; household merge ships with the household module                   | CRM, Membership       |
| R-3  | Derived coverage repeats 1.0's flow-down failures (queue fan-out, wrong revoke branches, "off" meaning revoke-all)                                        | Medium                                    | Silent wrong entitlement at the gate                       | Event-driven **and** swept, idempotent, diff-gated evaluator; a disabled rule stops; beneficiary rows carry their basis; the 1.0 defect list becomes named tests ([A5](#20-a5-testing-requirements)) | Membership            |
| R-4  | Composition change surprises families: a departure ends coverage nobody expected, or an attention item nobody reads                                       | Medium                                    | Front-desk disputes                                        | `follows_predicate` defaults to false (attention, not ending); attention items route to the Membership renewals responsibility; the portal shows the pending question to the holder                 | Membership, Attention |
| R-5  | Age rules evaluated by Membership diverge from the Eligibility engine's later `crm.contact.age`                                                           | Low                                       | Two answers to "is Maya a dependent"                       | One shared helper for age-at-date in `macroservice-records`; the engine's condition type calls it when the engine resumes                                                                           | Membership, Platform  |
| R-6  | Household names computed in the service (not the database) go stale when a contact's name changes through a path that skips the hook (an import, a merge) | Medium                                    | Wrong addressee on a letter                                | The recompute runs in the contact aggregate used by every path; a nightly consistency job recomputes computed values (never overrides) and reports differences                                      | CRM                   |
| R-7  | Platform additions (symmetric flag, `system_code` on association types, the `crm` derivation source) are not scheduled                                    | Medium                                    | CRM carries workarounds longer                             | Each is a small change requested in the household milestone plan; CRM's canonical ordering and settings row work without them                                                                       | Platform              |
| R-8  | The console household work is not authorised when the CRM milestone is approved (console changes need explicit direction)                                 | Medium                                    | A backend with no screens                                  | The milestone plan lists the console scope explicitly for approval                                                                                                                                  | Andrew                |
| R-9  | Double counting in household reports (a person in two households; a gift by a member of two households)                                                   | Medium                                    | Overstated totals                                          | Recipes default to "count by primary household"; Fundraising's household total uses the primary household as of the gift date unless the report asks for all; `count_distinct` where a report joins | CRM, Fundraising      |
| R-10 | Shared e-mail in a family defeats portal identity matching and Events member verification ("exactly one unclaimed contact")                               | High in family segments                   | Members cannot sign up or verify                           | Primary e-mail uniqueness stands; the portal's likely-match flow offers the household's other adult; Events' plan accepts a guardian-verified child without an e-mail                               | Platform, Events      |
| R-11 | Minor privacy: a tenant with no birth dates derives no guardian grants and treats children as adults                                                      | Medium                                    | A parent cannot act for a child; or a child is reachable   | The console flags "age unknown" on households with children; imports map birth dates; youth programs require them at registration                                                                   | CRM, Events           |
| R-12 | The first draft's household-holder design is cited somewhere as decided                                                                                   | Low                                       | A later milestone builds a party kind nobody approved      | This document supersedes the draft; HC-D3 records the decision; the CRM and Membership documents are not modified until Andrew rules                                                                | this proposal         |

### 2. Unresolved questions (owned elsewhere)

| Question                                                                                                                                                 | Owner                   | Why it matters here                                                                                       |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
| Membership's benefit catalogue (what a benefit is beyond `confers_benefits`)                                                                             | Membership architecture | `benefit_scope` on coverage rules is a string until a catalogue exists                                    |
| Owner transfer (Membership D-43) design and timing                                                                                                       | Membership              | Death and separation of a holder depend on it                                                             |
| Eligibility engine resumption and the `household`, `householdMembers` relationship paths                                                                 | Andrew; Platform        | Coverage predicates move into the engine when it resumes                                                  |
| Exclusive contact roles (CRM-Q7) and e-mail types (CRM-Q6)                                                                                               | CRM architecture        | Billing contact for organizations could derive from an exclusive role; shared mailboxes need typed e-mail |
| Record-level staff scoping (CRM-Q8)                                                                                                                      | CRM architecture        | Households scope like contacts when it lands                                                              |
| Events: `registered_by_contact_id`, consent signer, guardian-verified minors without e-mail, guest-pass consumption                                      | Events plan             | Family registration and waivers                                                                           |
| Marketing: householding rule, `household.*` merge fields, minors' consent                                                                                | Marketing plan          | One communication per household                                                                           |
| Fundraising: replace the spouse-pair key with the household as of the gift date; `household_recognition_name`, `household_opt_out`; anonymity on credits | Fundraising plan        | Household giving totals and recognition                                                                   |
| "One receipt, many customers" milestone date (Accounting #11)                                                                                            | Accounting              | A spouse paying another member's invoice                                                                  |
| Platform: `symmetric` flag and `system_code` on association types; attributes on associations; the `crm` derivation source                               | Platform                | Relationship semantics and portal authority                                                               |
| Mobile: membership card and wallet pass for holders and beneficiaries                                                                                    | Mobile architecture     | The gate's person-level token                                                                             |

### 3. What could not be verified, and the consequence

| Item                                                                                                              | Consequence                                                                                                  |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Community-walled vendor material (Blackbaud Community, Tessitura forums, ACME, Centaman, Versai, Glue Up, Naylor) | Those claims are Reported from indexed excerpts and marked so; no decision rests on one of them alone        |
| Production counts of 1.0 `spouse_id`, relationships and flow-down settings                                        | The 1.0 lessons rest on code, tickets and defects, not on usage volumes                                      |
| Whether any 1.0 tenant modelled a family as an organization type                                                  | If one did, its data imports as households through the "organisation is a family type" mapping               |
| Membership's milestone state (stopped; no R2-DEV-001 record)                                                      | Phase 2 timing is an estimate against an unscheduled resumption                                              |
| The deployed behaviour of services-crm `origin/master` after the acceptance repair waves                          | Statuses in chapter [04](#5-04-rhythm-2-0-design-constraints-the-existing-architecture-assessed) are from code at `eee6a5f4`, not from a live run |

## 15. 14. Architecture Decisions Requiring Approval

## 14. Architecture Decisions Requiring Approval

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). **HC-D1 to HC-D9** require Andrew's
ruling. Nothing is built until the HC-D decisions are ruled (R2-DEV-001 rule 1). **HC-I1 to HC-I9** are internal
choices, cheap to reverse and listed so they can be overturned.

Each decision identifies what it governs, the current behaviour, why it matters now, the realistic options and their
tradeoffs, the recommendation and its reason, and the impact on existing architecture.

### Rulings (Andrew, 2026-10-21, about 10:20 ET)

All nine decisions were ruled in the delivery conversation, after the full text below was presented. Six follow the
recommendation; three do not. Where a ruling differs from the recommendation, the chapters it changes carry an "As
ruled" note, and the consequences are listed here.

| Decision | Ruling                                                                                    | Same as recommended? | Consequence                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------- | ----------------------------------------------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HC-D1    | **CRM-owned relationship tables** (`crm.relationship_types`, `crm.contact_relationships`) | No (option 2)        | CRM people relationships become a recorded exception to custom-objects D-04 (associations stay the model for tenant-defined relationships involving custom objects and organizations). CRM owns the type semantics, the rows, their history, merge registry entries and query collections; the spouse backfill targets the CRM table. Chapter [10 §2a](#11-10-proposed-data-model-and-service-boundaries) has the DDL.                                                                                                |
| HC-D2    | Narrow first-class household record                                                       | Yes                  | As designed in chapters 09 and 10.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| HC-D3    | **A household may also hold a membership**                                                | No (option 2)        | Membership's `owner_kind` gains `household` with `household_id` for programs whose `member_kinds` include it; Commerce's recipient and entitlement-holder kinds gain `household` (buyer, bill-to and Accounting's party vocabulary stay closed); a billing contact (a contact) is required on a household-held membership; the beneficiary design of HC-D4 is unchanged and applies to household-held memberships too. Chapter [11 §2](#12-11-membership-coverage-and-benefit-inheritance-strategy) carries the note. |
| HC-D4    | Explicit beneficiary rows with a basis, designated or derived                             | Yes                  | As designed in chapter 11.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| HC-D5    | Custom fields on households approved                                                      | Yes                  | `crm.households` joins the R2-ARCH-001 list; a value table and layout slots.                                                                                                                                                                                                                                                                                                                                                                                                                                |
| HC-D6    | Several households per person, one explicit primary                                       | Yes                  | As designed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| HC-D7    | Manager and guardian authority as dated derived grants                                    | Yes                  | As designed in A4.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| HC-D8    | **Defer implementation until a signed customer needs it**                                 | No (option 3)        | Nothing is planned or built now; the milestone sequence in chapter [12 §6](#13-12-migration-and-implementation-implications) is the plan to use when a customer requirement arrives. The CRM roadmap's CRM-M21 is not re-planned yet. The three Platform items are not requested.                                                                                                                                                                                                                                         |
| HC-D9    | Closed list of naming formats with overrides                                              | Yes                  | As designed in A1 §4.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

### Decisions that need Andrew

#### HC-D1. Restore person-to-person relationships, on Platform associations

**What it governs.** Whether Rhythm 2.0 records typed facts between two people (spouse of, parent of, guardian of,
grandparent of, sibling of, caregiver of, tenant-defined), and which primitive stores them. Today, the only such fact is
the reciprocal `spouse_id` pointer, ruled interim (D-19). 1.0's generic relationships were unqueryable and unused except
by Fundraising's automatic soft credits.

**Why now.** Every segment needs these relationships (chapter [06](#7-06-cross-industry-requirements-and-use-cases)).
Household naming, guardian authorization, Fundraising recognition and Membership coverage rules all read them. The
spouse field already leaks into member-facing screens.

**Options.**

1. _Platform associations with a thin CRM semantics layer_ (Recommended). Use the primitive Andrew approved on
   2026-10-18 for "every relationship a tenant defines, including contact-to-contact". Merge participant, delete
   contract, history, import and query families already exist. CRM seeds eight system types and a
   `relationship_type_settings` row per type (symmetric, may authorise access, counts for naming). Costs: three small
   Platform additions (a `symmetric` flag, a `system_code`, later attributes), a CRM facade for configuration and a
   Contact 360 card.
2. _CRM-owned tables_ (`crm.relationship_types`, `crm.contact_relationships`, the spouse debt note's sketch). This gives
   CRM composite foreign keys and full control, but introduces a second relationship primitive beside the approved one:
   two configuration screens, two merge paths, two query exposures, and a rule about which to use.
3. _Keep `spouse_id` only._ No cost now, but every consumer above is blocked or builds its own mechanism.

**Recommendation and reason.** Option 1 follows the standing decision, reuses five built mechanisms, and gives
relationships the day-one consumers 1.0 never had.

**Impact.** Supersedes the CRM architecture's 05 §5 sketch of CRM relationship tables. The CRM document is not modified
here; a pointer is added once ruled. Platform needs three additions. CRM needs the seed set, settings table, routes,
card, spouse backfill and retirement.

#### HC-D2. Households as a first-class CRM record, not an organization type

**What it governs.** Whether the mailing and recognition unit is a new record (`crm.households`), an organization whose
configurable type carries a household attribute, or nothing. Today, no such unit exists. The CRM architecture's unruled
CRM-Q2 recommended an organization of a system type.

**Why now.** This is the decision the debate has circled. Museums, churches, fundraising shops and family clubs need the
unit. The brief's constraint rules out the organization route by name or protected type; the attribute route has the
costs below.

**Options.**

1. _First-class household record_ (Recommended): a narrow CRM module on the organization template, with configurable
   household types and roles, computed names with overrides, shared addresses by reference, dated members with two
   primary flags and a manager flag, life-event operations, and merge. Never a money party, never a membership holder.
   Costs: a CRM module, two Platform list entries, a console entity, a third merge object type.
2. _Organization type with a household attribute_ (`party_nature`): maximal reuse of organization mechanics, at the cost
   of about fourteen attribute branches across CRM, Membership, Commerce, Accounting and Billing, and a structural
   change to the one-primary affiliation rule. The household is a bill-to, payer and organization member by default
   unless every service checks the attribute ([07 §3.3](#8-07-alternative-architecture-models)).
3. _Generic constituent group_ with household as a group type: option 1 under a more abstract name, plus a rule for a
   third grouping mechanism beside Committees and custom objects.
4. _No record_: relationships and beneficiaries only. This is right for coverage, but insufficient for the segments that
   need a stable addressee, salutation, shared address, giving unit and history.

**Recommendation and reason.** Option 1 is the simplest model that correctly represents the requirement. It costs no
branch in any other service because households hold nothing and pay nothing.

**Impact.** New CRM module; `applies_to` on address and phone types; a fifth link source on contact addresses and
phones; household subjects on activity links and tasks; Platform datasets and Quick Search entries; console work under a
separate direction.

#### HC-D3. Memberships are held by one contact or organization; the household is never a holder

**What it governs.** Membership's owner model. Today, `owner_kind` is `contact | organization`. The first draft of this
work proposed `household` as a third kind.

**Why now.** This determines whether Membership, Commerce and the polymorphic holder reference change, and what the
family membership "is".

**Options.**

1. _Holder stays a contact or organization; the household is never a holder_ (Recommended). A person holds the family
   membership, with explicit beneficiaries. Owner transfer (D-43) and beneficiary rows handle death and separation.
   Household 360 shows memberships held by or covering members through a read. No change to Commerce, Accounting or the
   party vocabularies.
2. _Household as a third holder kind._ The family's membership literally belongs to the family. This adds a dozen branch
   points in Membership, recipient and entitlement-holder kinds in Commerce, and a billing contact beside the household
   because it cannot be billed. The grandparent and corporate cases still need option 1's mechanisms.

**Recommendation and reason.** Option 1 gives fewer special cases, no coupling and one entitlement answer shape. The
market's person-held models (Altru, Raiser's Edge, Neon, Bloomerang, Tessitura v12 with Initiator) work; the one
household-customer model (NetForum) carries the defects. Reopen only on a concrete customer requirement option 1 cannot
serve.

**Impact.** None on Membership's owner. Membership adds `billing_contact_id` for organization holders and the owner
transfer it already planned.

#### HC-D4. Coverage is explicit beneficiary rows with a basis, produced by designation or by derivation rules on the package

**What it governs.** How "who is covered, and why" is represented. Today, coverage consists of organization flow-down
(derived from affiliations and roles, materialised as child memberships) and assigned seats from an application. There
is nothing for families and no entitlement explanation.

**Why now.** The brief requires entitlement to be consistent and explainable across individual, family and
organizational memberships. That is the acceptance test for the whole design.

**Options.**

1. _Explicit beneficiary rows with a basis, two bases (designated, derived), rules on the package, materialised child
   memberships, one `/entitlements` answer_ (Recommended). One mechanism replaces flow-down and seats and adds families.
2. _Dynamic inheritance through relationships at read time._ No rows; entitlement is computed by walking relationships.
   This is unauditable, uncappable and unpriceable, the shape behind CiviCRM's and NetForum's inheritance defects.
3. _Designation only._ Fails organizational memberships, whose roster is HR's.
4. _Derivation only._ Fails the museum family, where the buyer chooses who is in.

**Recommendation and reason.** Option 1: relationships justify, rules bound, rows record. The same evaluator serves
employees and families, and every consumer gets the same explainable answer.

**Impact.** Membership: `membership_beneficiaries`, `package_coverage_rules`, `origin = covered`, the evaluator,
`/entitlements`. The application's `assigned_contacts` seeds rows. CRM publishes the reads the predicates need.

#### HC-D5. Custom fields on households (R2-ARCH-001 list change)

**What it governs.** Whether `crm.households` joins the approved custom-field entities. Only Andrew decides this, per
the standard.

**Options.** 1. _Approve_ (Recommended): parity with contacts and organizations. Fundraising segmentation ("giving
capacity", "stewardship tier") and church use ("small group", "pastoral notes") want it. The shared infrastructure costs
one value table and the layout slots already on `household_types`. 2. _Decline_: purpose-built screens; the household
editor is a FormEngine definition with no configurable fields.

**Impact.** The list in `approved-custom-field-objects.ts`, a `household_custom_field_values` table, `LAYOUT_PURPOSES`
entries.

#### HC-D6. Contacts may belong to several households, with one primary

**What it governs.** Whether a person can be an open member of more than one household (shared custody, adult children,
carers, roommates). Every one-household-per-person system in the research re-imposes the limit in reporting and forces
duplicates or "Visitor" roles.

**Options.** 1. _Several, with exactly one open primary household per contact, explicit and dated_ (Recommended): the
primary drives default mailing, the portal default and "count once by primary household" reports. The Explorer and
Fundraising use the primary unless asked otherwise. 2. _One household per contact_: simpler counts, but fails the family
scenarios in 06 §1, §2, §4.

**Impact.** The two partial unique indexes; the `is_primary_household` flag; a "count by primary household" mode in
household recipes.

#### HC-D7. Guardian and manager authority as derived Platform assignments from a CRM derivation source

**What it governs.** How a parent acts for a minor and a manager acts for a household on the portal. Today, no
delegation primitive exists; derived assignments exist for committees and eligibility.

**Options.** 1. _Derived, dated assignments_ (Recommended): Household Manager at `household:<id>` from the manager flag;
Guardian at `contact:<minor>` from an access-authorising relationship while the contact is a minor; an
`authorized_representative` relationship for adults, staff-confirmed or self-consented. Household membership alone never
grants authority. Another adult can never edit an adult's records without their opt-in. This needs a Platform migration
adding the `crm` derivation source. 2. _Household membership grants access_ (the organization-roster analogy): simpler,
but violates the brief's privacy requirement and the church and youth systems' child-safety floor. 3. _Defer all portal
authority to CRM-M22 without designing it_: the schema would ship without the flow, repeating the 1.0 `portal_delegates`
failure.

**Impact.** Platform migration; CRM derivation job; portal routes under `requireAccess`, built last.

#### HC-D8. Sequencing: what is built now, what is deferred

**What it governs.** When the work starts relative to the standing orders: stabilization first; focus on CRM, Accounting
and Integrations; Membership stopped; portal last.

**Options.** 1. _Record the decisions now; build the CRM foundation (relationships, households, Household 360, queries)
as CRM-M21 split in two after the retroactive milestones are accepted and CRM-M15's contracts exist; build coverage as
Membership's first household milestone when Membership resumes; Fundraising, Marketing and Events pieces in their own
plans; portal last_ (Recommended). 2. _Start the CRM foundation immediately_: conflicts with stabilization first. 3.
_Defer everything until a signed customer requires it_: the spouse field, the Fundraising spouse-pair key and the
flow-down engine then harden around the gap.

**Impact.** Chapter [12](#13-12-migration-and-implementation-implications) §3's milestone outline.

#### HC-D9. Naming formats as a closed list with per-household override, not a template language

**What it governs.** How household names, addressees and salutations are produced. Options: 1. _Three formats (`family`,
`names`, `primary_only`) with tenant defaults, a suffix and connector setting, per-household format choice and four
override fields_ (Recommended; NPSP's grammar and its revert defects are the cautionary case). 2. _A template language_
(Liquid) per tenant: flexible, but hard to validate; overrides fight regeneration. A template feature can be added later
as a Communications merge-field capability without changing storage.

### Internal choices (cheap to reverse; reported, not asked)

| Id    | Choice                                                                                                                                                           | Reversal cost                                |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| HC-I1 | A household may exist with no open member (import ordering); the console requires one at creation; a data-quality recipe lists empty households                  | a check constraint                           |
| HC-I2 | Household types and roles are configuration-id tables with `system_code`, seeded, deactivatable, deletable when unreferenced; code never requires a specific row | none                                         |
| HC-I3 | Household addresses are owned by the household and linked by reference into members (the fifth link source), never copied by default; D-25 disconnection applies | a default setting                            |
| HC-I4 | No inheritance of e-mail, phone, consent or preferences from a household; consent is per contact always                                                          | none (a standing decision, D-M2)             |
| HC-I5 | Symmetric relationship types are stored once in canonical order until Platform adds a `symmetric` flag                                                           | a backfill                                   |
| HC-I6 | Household merge is a third merge object type with an organization-shaped planner; contact merges warn, never refuse, when the two contacts' households differ    | planner code                                 |
| HC-I7 | Dissolve and reinstate are first-class operations with reasons; archive never ends member rows                                                                   | service code                                 |
| HC-I8 | Membership evaluates age rules itself against `crm.contacts.date_of_birth` until the Eligibility engine resumes; `term_start` is the default basis               | a configuration default                      |
| HC-I9 | The `/entitlements` answer is the only entitlement contract; Events, Commerce and the portal consume it rather than reading membership tables                    | a published contract (compatibility promise) |

### Decisions deliberately not made here

Membership's benefit catalogue; the Eligibility engine's resumption; record-level staff scoping (CRM-Q8); e-mail types
(CRM-Q6); exclusive contact roles (CRM-Q7); the one-receipt payment milestone's date; Marketing's print channel; the
Events package's consent signer (requested of Events' plan). Each is owned elsewhere and named in chapter
[13](#14-13-risks-and-unresolved-questions).

## 16. A1. Household Lifecycle Rules

## A1. Household Lifecycle Rules

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). The tables are in
[10](#11-10-proposed-data-model-and-service-boundaries); this chapter defines the behaviour: how members, roles, relationships,
names, addresses, life events, merges and deletion work. Every rule is enforced in the household or contact aggregate
inside one transaction (persistence standard) and proved at the integration level ([A5](#20-a5-testing-requirements)).

### 1. Household members

**Creating a household.** `POST /households/{t}` accepts the household fields and an optional `members` list. The
console requires at least one member, the person the user started from. The API allows none, so an import can create the
household before its members in a dependency-ordered run. A household with no open member is valid and appears in the
data-quality recipe "Households with no current members".

**Adding a member** (`POST /households/{t}/{id}/members`): the request names the contact, a role, optional dates,
optional `is_primary_contact`, `is_primary_household`, `is_manager`, and an optional `address_links` instruction (§3).
The save is refused if the contact is archived or has a released status (`inactive`, `deceased`), unless the row is
historical (ended); the contact is already an open member of this household (HH-3); there are more than 50 member rows
for the household, open and ended together, counting the one being added; the row would create a second open primary
contact (HH-1) or a second open primary household for the contact (HH-2); or a future start date is combined with
`is_primary_contact`. A primary contact must be current. A future member becomes primary on its start date only through
an explicit later save, as D-46 does for roles.

**Defaults.** The first member whose row is current (started and not ended) becomes the primary contact; a future-dated
row never does. A contact's first open household becomes their primary household. The member's role defaults to `adult`
when the contact has reached the tenant's age of majority (`crm_settings.age_of_majority`, default 18) or has no birth
date, and `child` otherwise. The console shows the default, and the user may change it.

**Editing a member** (`PUT /households/{t}/{id}/members/{memberId}`): the editable fields are role, flags, dates and
notes. Household and contact are immutable. To correct a wrong contact, end the row with reason `correction` and add the
right one. This preserves the history, following the affiliation precedent.

**Making another member primary contact** (`POST …/members/{memberId}/make_primary_contact`): one save moves the flag,
recomputes names (§4), and re-derives manager and guardian grants (A4) in the same transaction.

**Ending a member** (`POST …/members/{memberId}/end` with `end_date` and `end_reason`): the operation ends the row on
the given day, defaulting to the tenant's business date and never before the start. It clears the three flags, turns
every contact address or phone linked through this row into `disconnected` with reason `member_ended` (D-25 behaviour),
and recomputes names. If the ended row was the primary contact and other open members remain, the household would be
left with **no primary contact and the save is refused** unless the same request names the new primary
(`successor_member_id`). No successor is required when the household has no other open member. This is the rule the
market gets wrong: NetForum loses the address when the head changes; Common Ground errored when a head was deleted.
Rhythm asks once, in the same save.

**Removing a member** (`DELETE …/members/{memberId}`): deletion is for mistakes only. It is refused when any contact
information is linked through the row unless the request re-points, copies or removes those rows. It is also refused
when a beneficiary row references this member row as its basis (Phase 2); end the member row instead. History is
otherwise kept by ending, not deleting.

**Omitted means unchanged** (E-09, D-09, write contract W3): a household `PUT` that omits `members`, `addresses` or
`phone_numbers` leaves them unchanged; `[]` also changes nothing. Add, edit, end and remove are the explicit operations
above, each with its own permission key (`crm.household-members.add`, `.update`, `.end`, `.remove`).

### 2. Person-to-person relationships

**Primitive.** Relationships are Platform associations between two contacts
(`from_object_id = to_object_id = 'crm.contacts'`), dated, with a label per direction. CRM seeds six system association
types per tenant (`crm_spouse`, `crm_partner`, `crm_parent_child`, `crm_guardian_dependent`, `crm_sibling`,
`crm_grandparent_grandchild`) with the matching `relationship_type_settings` rows
([10 §1](#11-10-proposed-data-model-and-service-boundaries)). A tenant may add its own
(`Mentor of / Mentee of`, `Emergency contact for / Has emergency contact`) in Configuration › CRM › Relationship Types.
This is a CRM screen over the Platform association-types routes plus CRM's settings row.

**CRM's write path over Platform's.** The console and portal call CRM's relationship routes
(`POST /contacts/{t}/{id}/relationships`, `…/end`, `DELETE`). These routes validate CRM semantics, then call the
Platform associations service function inside the same transaction:

- A **symmetric** type (spouse, partner, sibling) is stored once in canonical order: the lower contact id is `from`. A
  request in the other order is normalised. A row that would duplicate an open one is refused with the existing row's id
  (HH-7). Until Platform adds a `symmetric` flag, CRM's settings row carries it and the generated query families expose
  both `crm_spouse` and `crm_spouse_reverse`. CRM's dataset hides the reverse family for symmetric types.
- A **directional** type (parent of / child of, guardian of / dependent of) keeps the direction the user chose. The
  other contact sees the reverse label.
- A self-relationship is refused; Platform already refuses it.
- A relationship with a released-status or archived contact is allowed for history, but that contact is never offered in
  pickers.
- `spouse` and `partner` are mutually exclusive for the same pair. A contact may have at most one open `spouse`
  relationship (`one_to_one` cardinality on the Platform type). A second is refused with the existing spouse named, as
  the interim rule does today.
- Dates are optional. Ending is preferred to deleting, following Platform's rule for dated types. Deleting an ended row
  corrects history.

**Spouse migration.** One backfill turns each reciprocal `spouse_id` pair into one `crm_spouse` association; half pairs
become a row with a data-quality flag. `spouse_id` becomes a read-only projection of the open spouse relationship. It
stays on the wire for the console and duplicate scoring until both read the relationship, then the column is retired.
The debt note `docs/debt/spouse-relationship-model.md` records this as the target; this proposal makes it CRM-M21a's
scope.

**Households and relationships stay independent.** Adding a member offers to record relationships to existing members.
The console suggests "spouse of the primary contact" for a second adult and "child of" each adult for a child; the user
confirms or skips. Ending a membership never ends a relationship. A divorce is a household change _and_ an ended spouse
relationship, two explicit facts.

**Reads.** Contact 360 shows a Relationships card with both directions, Current and Past, labelled. Household 360 shows
the relationships among current members as a read-only convenience. Queries use Platform's generated families
(`crm_parent_child` collection on contacts: "contacts who are parent of a contact under 12").

### 3. Addresses and contact information

**The household owns its addresses and phones.** It has one row per configurable type, with a preferred flag and
seasonal month/day windows, exactly as organizations ([10 §2](#11-10-proposed-data-model-and-service-boundaries)).

**A member's address is theirs unless they link it.** A contact address row of mode `linked` may name a household
address through the member row (fifth link source). Linking is explicit:

- When adding a member, the console offers "Use the household's Home address for this person's Home address". It is
  checked by default when the household has a Home address; the tenant setting
  `crm_settings.household_address_link_default` turns the default off.
- The household-side action "Apply household address to members" links the chosen household address to every current
  member's address of the chosen type that is not `owned` with a different value. It reports what it skipped.
- A member may hold an `owned` or `copied` address of the same type instead, such as the child who lives elsewhere part
  of the week, and have a second address type linked to the household (`Other` → household Home).
- Organization links and household links are different sources on different rows. A contact never has one row linked to
  both.

**When the household moves**, its address is edited once. Every linked member resolves to the new value through
`contact_addresses_effective`. `copied` rows do not change, which is what copied means, and the console says so.

**When a member leaves or the household address is removed**, linked rows become `disconnected` with reason
`member_ended` or `household_address_removed`. They keep the last known value and offer verify-as-own, relink or remove
(D-25). Nothing is ever silently propagated to a person who left (the RE7 divorce complaint).

**Phones** follow the same rules. **E-mail** is never shared: a household has no e-mail address. A shared mailbox is two
contacts with the same address value, which CRM's primary-e-mail uniqueness refuses, so the second person records it as
a non-primary slot. **Communication preferences** are per contact; a household has none.

**Region.** A household is assigned a region from its preferred address by the existing region rules, using the same
fingerprint mechanism as contacts. This lets "households in the Southeast region" work without a join.

### 4. Names and salutations

Four computed, stored, server-owned strings each have an override
([10 §2](#11-10-proposed-data-model-and-service-boundaries)):

| String                | Purpose                              | Default format `family`                                                                                                                                                            | Format `names`            | Format `primary_only`  |
| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---------------------- |
| `name`                | Record name in lists, search, links  | "The Rivera-Chen Family" (one shared last name: "The Smith Family"; several: hyphenated up to two, then "The Rivera Family and others")                                            | "Ana Rivera and Wei Chen" | "Ana Rivera Household" |
| `mailing_name`        | Envelope and label addressee         | "Dr. Ana Rivera and Mr. Wei Chen" (shared last name: "Dr. Ana and Mr. Wei Rivera" is **not** used; "Dr. Ana Rivera and Mr. Wei Rivera" or, with no prefixes, "Ana and Wei Rivera") | same                      | "Dr. Ana Rivera"       |
| `formal_salutation`   | "Dear …" in formal letters           | "Dr. Rivera and Mr. Chen" (shared: "Dr. and Mr. Rivera")                                                                                                                           | same                      | "Dr. Rivera"           |
| `informal_salutation` | "Dear …" in informal letters, e-mail | "Ana and Wei" (nickname first if set)                                                                                                                                              | same                      | "Ana"                  |

**Who is named.** Current members are named in this order: the primary contact; then members related to the primary
contact by a relationship whose type `counts_for_naming` (spouse, partner); then other members whose role
`counts_as_adult`, by start date then name. Children and dependents are never named in salutations. They appear in
`name` only under the `family` format through the shared last name. Deceased members are excluded. A member marked
`exclude_from_naming` on the member row is excluded from all four strings; the editor exposes this per-row flag,
following the NPSP pattern. More than two named adults use commas and "and" ("Ana Rivera, Wei Chen and Rosa Rivera"),
capped at three with "and family" after.

**Overrides.** An override replaces the computed value in every reader and is never regenerated. The computed value is
still stored and shown beside it ("Computed: The Rivera Family · Restore"). No "refresh all names" job touches
overrides; a data job may recompute computed values only.

**Tenant configuration.** The settings are `crm_settings.household_name_format` (default `family`),
`household_name_suffix` (default "Family"; tenants may prefer "Household"), `household_name_connector` (default "and"),
and the age of majority. A household may choose its own `name_format`. Formats are a closed list, not a template
language. NPSP's format grammar and its revert bugs are the cautionary case. A Liquid template for salutations can be
added later as a Communications merge-field feature without changing storage.

**When names recompute.** Names recompute on any member add, edit, end or remove; on a change to the primary contact; on
a contact save that changes name parts, prefix, nickname, status or death date for a contact who is an open member
anywhere (HH-5, inside the contact save); and on a relationship change of a naming type between members. They never
recompute on read.

### 5. Status, archive and dissolution

- **Status** `active | inactive` is an ordinary edit (deactivate). Inactive households leave pickers and default lists
  (the discovery rule) and keep every reference.
- **Archive** follows the contact and organization archive contract: `POST /households/{t}/{id}/archive` with a reason.
  It is refused while any write is pending. Memberships are unaffected because they belong to people. Beneficiary rows
  whose basis is one of this household's member rows raise an attention item (Phase 2). Archiving does not end members:
  the household is set aside, not dissolved.
- **Dissolve** (`POST …/dissolve` with `dissolved_on` and `dissolution_reason` from a closed list: `separation`,
  `death`, `moved`, `duplicate`, `other`): the operation ends every open member row on that date with reason
  `dissolved`, disconnects linked contact information, freezes the computed names as they stood just before the rows
  were ended (overrides kept), so the archived household keeps a readable name, then archives the household with reason
  `dissolved`. Beneficiary rows justified by this household's member rows are listed in the Dissolve wizard for a
  decision (Phase 2). The memberships themselves belong to people and are untouched. Relationships are also untouched.
- **Reinstate** (`POST …/reinstate`): the operation unarchives the household and reopens the member rows ended by that
  dissolution (same `dissolved_on`, reason `dissolved`) for contacts who are still active and whose reopened row would
  not create a second open primary contact or primary household. The others are listed as skipped (the Altru rule).
- **Delete**: today's hard delete is allowed only for a household with no member rows, no memberships, no activity links
  and no tasks. It is refused otherwise, like an organization with affiliations. With CRM-M20 it joins the
  deletion-impact contract: member rows `detach` with a tombstone, memberships `block` while current, activity links per
  D-44.

### 6. Life events, as operations

| Event                                      | What staff do                                                                                                                                                                                    | What the system does                                                                                                                                                                                                                                                                       |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Two contacts marry or partner              | Record a `spouse`/`partner` relationship; if either has a household, add the other to it (or create one)                                                                                         | Names recompute; naming order puts the spouse second; duplicate scoring treats the pair as negative evidence                                                                                                                                                                               |
| A child is born or adopted                 | Create the contact from the household ("Add new member"), role `child`, link Home address, record `parent_of` from each adult                                                                    | Guardian grants derive for adults with access-authorizing relationships (A4)                                                                                                                                                                                                               |
| A couple separates                         | End one adult's member row (reason `left`); end the spouse relationship; create a new household for them; add the children to both if custody is shared, choosing each child's primary household | Linked addresses of the leaver disconnect; both households' names recompute; a child's primary household drives their default mailing and portal view                                                                                                                                      |
| A member dies                              | Set the contact's status `deceased`                                                                                                                                                              | The member row stays open (history) but the person is excluded from names and mailings; if they were the primary contact, the save requires a successor when other members remain; manager and guardian grants end; Fundraising's recognition rules see the household as of each gift date |
| A child turns 18                           | Nothing in CRM                                                                                                                                                                                   | Guardian grants over the child end at midnight on their birthday (derivation re-run by the nightly sweep and on any touch); Membership coverage rules act at renewal or continuously per program (11)                                                                                      |
| A household moves                          | Edit the household address                                                                                                                                                                       | Linked members follow; region reassigns                                                                                                                                                                                                                                                    |
| A member moves out but stays in the family | End the member row? **No**: change their address to `owned` (or `copied`) and keep the row; a household is not an address                                                                        | Nothing else                                                                                                                                                                                                                                                                               |
| Two households are the same family twice   | Merge (§7)                                                                                                                                                                                       |                                                                                                                                                                                                                                                                                            |
| A household dissolves                      | Dissolve (§5)                                                                                                                                                                                    |                                                                                                                                                                                                                                                                                            |

### 7. Duplicates and merge

**Duplicate detection for households.** The seed set ships two rules, both `warn`: same computed `name` and same
preferred address postal code; same set of current member contact ids (a household created twice for the same people). A
shared address between two **contacts** stays negative evidence for contact duplicates, as today. A contact pair that
are open members of the same household is strong negative evidence: they are two people who live together. The scoring
gains this evidence alongside `spouse`.

**Merging households** uses the engine with a household planner. The survivor keeps its computed and override names
unless a scalar choice is made. Member rows are unioned by `(household_id → survivor, contact_id)`. Two open rows for
one contact collapse into the earliest, with the other ended with reason `merged`; two primary contacts require a
decision. Addresses and phones combine per type with staff choices. Linked member addresses re-point to the survivor's
address of the same type when one exists, otherwise they disconnect. Activity links and tasks reassign. Membership
(Phase 2) re-points the basis records of beneficiary rows that cite merged member rows. The alias answers 410 with the
survivor.

**Merging contacts** who belong to households: the survivor holds each household membership at most once (HH-9). A
relationship that would become a self-relationship is removed with a warning. Platform's participant re-points and
collapses the associations. When the two contacts were open members of different households, a warning names the
households ("Ana is in The Rivera-Chen Family; the merged record is in The Rivera Household") so staff can merge or
reconcile them afterwards. The merge is never refused for that reason (the eTapestry failure).

**Merging organizations** is unaffected: a household is not an organization.

### 8. Events published

The published events are `crm.household.created`, `.updated`, `.archived`, `.unarchived`, `.dissolved`, `.reinstated`,
`.merged`; `crm.household_member.added`, `.updated`, `.ended`, `.removed`; `crm.contact_relationship.created`, `.ended`,
`.removed`. CRM publishes the relationship events on top of Platform's `objects.association.*` so subscribers need not
know the storage.

Payloads carry ids and changed field names only, as every CRM event does. Membership subscribes to member events to
raise coverage attention items (11). Fundraising subscribes to recompute recognition on composition change if it caches
(it should not). The Integration Platform catalogues the events.

## 17. A2. Household 360 and the Console Experience

## A2. Household 360 and the Console Experience

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence:
[r5](#25-r5-console-and-portal-architecture-for-households-research-report) §1–§5 and (a). Every screen below composes existing console systems; nothing
introduces a new UI framework, and every console change needs Andrew's explicit direction and an approved milestone plan
(AGENTS.md console rule; R2-DEV-001; the UI change approval rule). RHYTHM-CRM-ARCH-001 09 §1's ruling applies: cards,
actions, forms, field editors, audit views and Explorer recipes go through the shared metadata-driven systems, never
household-only components. Every screen named in this appendix is designed and built in v0 under the V0 rules (Andrew,
2026-10-21: all UI screens through v0 unless he gives explicit permission otherwise); nothing here is hand-coded.

### 1. Where households appear

| Place               | What                                                                                                                                                                                                                                                                                                                                                                                                                                      | Built from                                                                                                                                                  |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Navigation          | CRM › **Households** beside Contacts and Organizations; the planned "Relationships" item is **removed** (relationships are a card on Contact 360, not a page)                                                                                                                                                                                                                                                                             | `lib/console/domains.ts` item; `lib/console/record-types.ts` noun `crm-households` with routes, label, icon and permission                                  |
| Quick Search (⌘K)   | Households found by name, number, member names; result opens Household 360                                                                                                                                                                                                                                                                                                                                                                | Platform record type `households`; `lib/quick-search/types.ts` union; the dataset's `quickSearch` block (title = name, subtitle = primary contact and city) |
| Households list     | Explorer over recipe `crm.households.with-members`: columns name, number, type, primary contact, city, current members, status, archived; presence chips (Active default, Archived); saved and pinned views; Ask; bulk actions (archive, add to list, export); row actions (open, log activity, create task, merge)                                                                                                                       | `defineRecipeExplorer`, the shared record-explorer, the dataset and recipes CRM registers                                                                   |
| Household 360       | §2                                                                                                                                                                                                                                                                                                                                                                                                                                        | record-view engine, `household-360.json`, cards                                                                                                             |
| Contact 360         | **Household** card (Overview tab): the contact's current households with role, primary flags and a link; "Add to household" and "Create household from this contact" actions. **Relationships** card replaces the placeholder tab: both directions, Current and Past, Add, End, Remove. The "Status & Relationships" field group loses `spouse_name` once the relationship card reads the association (interim: shows the derived spouse) | `defineCard` `contact.households`, `contact.relationships`; field catalog entries; the existing visibility rules                                            |
| Organization 360    | Nothing                                                                                                                                                                                                                                                                                                                                                                                                                                   |                                                                                                                                                             |
| Contact editor      | No household field on the contact form: household membership is edited from the Household card or Household 360, never as a contact property (it is a relationship collection, E-09). The spouse picker field is retired with `spouse_id`; the Relationships card is the editor                                                                                                                                                           | FormEngine definition `contact-admin.json` loses `spouseId`                                                                                                 |
| Configuration › CRM | **Household Types**, **Household Roles**, **Relationship Types** (over Platform association types plus CRM settings), the household naming settings on **CRM Settings** (format, suffix, connector, age of majority, address-link default)                                                                                                                                                                                                | `ConfigListScreen` + `ListToolbar` one-row standard; the configuration editors                                                                              |
| Data Quality        | Duplicate households queue (same engine, object type households); recipes: households with no current members, households with no primary contact (should be empty), contacts in more than one household, households whose primary contact is deceased                                                                                                                                                                                    | duplicates screens with object type; Explorer recipes with remediation actions                                                                              |
| Merge workspace     | Object type Households; the second-record picker (D-03) applies                                                                                                                                                                                                                                                                                                                                                                           | `crm/merges/new?objectType=households`                                                                                                                      |
| Pickers             | A dataset-backed household picker (`crmHouseholdPickerSource` over `crm.households`), inactive and archived never offered, held values labelled; used by Add to household, task and activity subject, Membership's beneficiary proposals (Phase 2)                                                                                                                                                                                        | `createPickerSource` + adapter; noun registry                                                                                                               |

### 2. Household 360

A JSON record view (`lib/record-views/views/household-360.json`, `recordType: "crm-households"`) on the same engine as
Contact 360, with a record-context builder `lib/record-context/household.crm.ts` reading `GET /households/{t}/{id}` (the
full record: resolved members with contact names, roles, flags, dates and status; addresses and phones with preferred
and seasonal state; computed and override names; type; region; archive and dissolution state; `sys_*`).

**Header** (`household.identity` card, modelled on `contact.identity`): name (override or computed, with a marker when
overridden), number, type, status, archived or dissolved banner with reason and date, merged-from notice, duplicate
warning, primary contact (link) and the mailing name, preferred address and phone, current member count, created and
modified attribution (D-23).

**Actions** (primary, secondary, more): Edit (opens the household editor: names and overrides, type, status, region,
notes, addresses, phones, custom fields when approved), Add Member, Log Activity, Create Task, Apply Household Address
to Members, Archive / Unarchive (`attribute archivedAt` rule), Dissolve (wizard), Reinstate (when dissolved), Merge
with…, Copy Link, Version History; later Delete (CRM-M20 impact panel). Send E-mail, Print Labels and Create Order
appear only when their capability is live (D-02, D-53). Every visible action has a real, authorised effect.

**Tabs and cards**:

| Tab            | Cards                                                                                                                                                                                                                                                                                                                                                                                                                                         | Owner               |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| Overview       | **Members** (§3); **Names** (`record.field-group`: computed and override of each of the four strings, with Restore); **Addresses** and **Phones** (the shared typed-collection cards, preferred and seasonal, "linked to N members" count per row); **Details** (type, status, region, notes); **Relationships among members** (read-only list of open associations between current members); **Custom fields** (if approved); `record.audit` | CRM                 |
| Memberships    | Memberships held by current members and memberships covering them: program, holder, tier, term, status, and each member's part (holder, or beneficiary with coverage role and basis); actions Renew, Manage beneficiaries, Spin off (Phase 2)                                                                                                                                                                                                 | Membership          |
| Finance        | Open invoices, recent payments and balance **per member** with a household total, each row linking to the member's record; a note that the household is never billed                                                                                                                                                                                                                                                                          | Accounting          |
| Giving         | Household giving total (de-duplicated by gift), gifts by any member with donor of record and recognition credits, giving societies, recognition name (Phase 3)                                                                                                                                                                                                                                                                                | Fundraising         |
| Events         | Registrations of members, grouped by event (Phase 3)                                                                                                                                                                                                                                                                                                                                                                                          | Events              |
| Activity       | Timeline of activities linked to the household **and** to its current members (toggle), tasks about the household                                                                                                                                                                                                                                                                                                                             | CRM                 |
| Communications | Sends to members grouped by household send (Phase 3)                                                                                                                                                                                                                                                                                                                                                                                          | Platform, Marketing |
| History        | Version history of the household, including member rows and merged-from history (D-21)                                                                                                                                                                                                                                                                                                                                                        | CRM                 |

A tab from another application is shown only when that application is licensed and its card is live (P-9); until then it
does not exist on the page (no "not available yet" tabs for a new entity). Header and first card render from one record
read; other cards load independently with their own empty, error and permission states.

### 3. The Members section

The affiliation editing pattern, reused layer for layer (r5 §3):

1. **Card** `household.members` (read-only on the 360 for viewers; editable in place for `crm.household-members.*`
   holders): rows ordered primary contact → other current adults → children and dependents → upcoming → past; counts "4
   Current · 1 Past"; an All / Current / Upcoming / Past filter driven by the service's `effective` value (D-10, D-12).
2. **Row**: contact name (link to Contact 360, "(Archived)" and status badges), role ("(inactive)" suffix for a
   deactivated role), flags as chips (Primary contact, Primary household, Manager), dates, status, the address links
   through this row ("Home linked"), and a menu: Edit, Make Primary Contact, Set as Primary Household, Grant/Revoke
   Manager, End, Remove (mistakes only).
3. **Add Member** (sheet, modelled on `AffiliationEditor`): the contact picker (existing contacts; **Create new
   contact** inline with the minimal fields: names, birth date, e-mail optional, opening the full editor afterwards),
   role (defaulted by age), start date (default business date), flags, "Use household Home address as this person's Home
   address" (default per tenant setting) with a preview of what it replaces (the `DefaultLinkPreview` component), and
   **Relationships to record** (suggested: "Spouse of Ana Rivera" for a second adult; "Child of Ana Rivera" and "Child
   of Wei Chen" for a child; each a checkbox).
4. **End Member** (dialog, modelled on `EndAffiliationDialog`): end date, reason, the consequences listed (addresses
   that will disconnect, manager and guardian grants that end, coverage rows Membership will flag), and, when the member
   is the primary contact and others remain, a required successor picker.
5. **Writes** are explicit directives with the household's `sys_version` (W1, W3); a 409 offers Reload or comparison
   (D-35); dirty edits survive session expiry (D-17).

### 4. Creating a household

Two entry points, one flow:

- **From a contact** (Contact 360 › Household card › "Create household from this contact"): the household is created
  with this contact as primary contact and primary household, role by age, names computed, the contact's preferred
  address offered as the household's Home address (copied to the household, then the contact's row linked to it, in one
  save); the user lands on Household 360 with Add Member open.
- **From the Households list › New**: type, optional name override, Home address, then Add Member for the first person
  (required by the console).

Both go through `POST /households/{t}` with the members and addresses in the one aggregate save (client UUIDv7, replay
digest, D-43).

### 5. Workflows staff do most

| Workflow                     | Clicks                                                                                                                                                                                                                                                   |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| New family walks up          | Contacts › New (first adult) → Save → Household card › Create household → Add Member (second adult, spouse checkbox) → Add Member › Create new contact (each child, Child of both) → Done. Four saves, one household                                     |
| Address change by phone      | ⌘K the family name → Household 360 › Addresses › Edit Home → Save. Linked members follow; the card lists who did not (copied or owned)                                                                                                                   |
| Divorce                      | Household 360 › Members › Wei › End (reason Separation, successor not needed since Ana is primary) → Relationships among members › Spouse of › End → Contacts › Wei › Household card › Create household → Add Member (Leo, Child, not primary household) |
| Death of the primary contact | Contact 360 › Change Status › Deceased → the save asks for the successor primary contact of each household where the deceased was primary → names recompute; mailing stops for the deceased                                                              |
| Two records for one family   | Data Quality › Duplicate households → Review → Merge (decide the primary contact, pick addresses) → Execute                                                                                                                                              |
| Who lives with whom          | Explorer › Households › filter "has a current member where role = Child and age < 12" (an `exists` criterion over the members collection) → Save view "Families with young children"                                                                     |

### 6. What belongs where

| Information                                | Household record          | Contact record                                  |
| ------------------------------------------ | ------------------------- | ----------------------------------------------- |
| Name parts, birth date, pronouns, status   |                           | ✔                                               |
| E-mail, consent, communication preferences |                           | ✔ (never on the household)                      |
| Home address                               | ✔ owned by the household  | ✔ linked, copied or owned per person            |
| Mailing name and salutations               | ✔ computed with overrides | personal `salutation` stays for individual mail |
| Role in the family, primary flags, manager | ✔ on the member row       | shown on the Household card                     |
| Relationships (spouse, parent, guardian)   | shown among members       | ✔ owned by the pair, shown on both              |
| Memberships held by or covering members    | read-side list (Phase 2)  | ✔ holder; beneficiary rows on the person        |
| Invoices, payments, gifts                  | rollup only               | ✔                                               |
| Custom fields                              | ✔ only if approved        | ✔                                               |
| Activities and tasks                       | ✔ about the household     | ✔ about the person                              |

### 7. Portal (last)

Portal household self-service is designed in [A4 §4](#19-a4-security-privacy-and-authorization) and built last on Andrew's
components (CRM-M22 and the Membership portal milestone). Nothing in this chapter is a portal screen.

## 18. A3. Cross-Application Integration

## A3. Cross-Application Integration

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence:
[r3](#23-r3-how-the-money-handling-and-engagement-applications-identify-the-person-or-organization), [r4](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships) §2, §6, §7. The rule of Part I 12 §1
(RHYTHM-CRM-ARCH-001 `12-cross-application-contracts.md`) holds: CRM owns household identity; every other application
stores household ids as plain text references, participates in merge and deletion, and reads households through
published views and service functions. The one new rule: **a household is never a money party**. Every place that
resolves a balance, a receipt, a tax position or a payment method names a contact or an organization; the household is a
grouping, a resolution rule or a reporting basis on top.

### 1. Ownership and contract per application

| Application          | Stores about households                                                                                                                                                               | Reads from CRM                                                                                                           | On household merge                                        | On household deletion (CRM-M20)                                                         | Phase |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----- |
| Platform             | Associations (relationships; endpoints are contacts, not households); derived assignments scoped `household:<id>`; attention items and audit naming households                        | Member rows and relationship settings through published reads for the derivation                                         | Participant: reassign scopes, re-point attention subjects | Grants end; attention items delete                                                      | 1     |
| Membership           | Nothing structural; `basis_record_id` on beneficiary rows may cite a `crm.household_members` row (soft)                                                                               | `household_members_current` (roles, birth dates) and relationships inside coverage rules; member and relationship events | Participant: re-point cited basis records                 | Open beneficiary rows citing the household's member rows: attention item, never a block | 2     |
| Commerce             | Nothing structural; `selling_context.household_id` for reporting only                                                                                                                 | Nothing (Membership proposes beneficiaries)                                                                              | Nothing                                                   | Nothing                                                                                 | —     |
| Accounting           | Nothing structural; `custom_metadata.household_id` on invoices for reporting                                                                                                          | Nothing                                                                                                                  | Nothing                                                   | Nothing                                                                                 | —     |
| Billing              | Nothing structural; consolidation groups by payer, lines by beneficiary                                                                                                               | Billing contact through Membership                                                                                       | Nothing                                                   | Nothing                                                                                 | —     |
| Fundraising          | Recognition credits with reason `household` naming the household id; `donor_profiles.household_recognition_name`, `household_opt_out`; giving society membership on basis `household` | Household membership as of a gift date (published function `crm.household_of_contact_on(contact, date)`)                 | Participant: re-point credits and society rows            | Credits retain with tombstone; society rows end                                         | 3     |
| Marketing            | Send audiences with a `household` recipient-relationship kind; per-send householding rule; recipients stay contacts                                                                   | Primary contact, mailing name and salutations, member consent through CRM functions                                      | Participant: nothing persistent beyond audience snapshots | Nothing                                                                                 | 3     |
| Events               | `registrations.registered_by_contact_id` (new), `registration_consents.signer_contact_id` (new), attendee grouping by household at checkout; guest-pass consumption                   | Household members, guardian authorizations (A4), Membership's entitlement answer                                         | Participant: nothing (registrations name contacts)        | Nothing                                                                                 | 3     |
| Data Engine          | Operations `crm.households/upsert`, `crm.household_members/upsert`; crosswalk entries                                                                                                 | —                                                                                                                        | Crosswalk keys re-point to the survivor                   | Items retain ids                                                                        | 1     |
| Integration Platform | Catalogue keys `crm.household.*`, `crm.household_member.*`, `crm.contact_relationship.*`                                                                                              | —                                                                                                                        | `merged` event with survivor                              | `deleted` event                                                                         | 1     |
| Portals              | Household manager and guardian pages (last)                                                                                                                                           | Everything through CRM and Membership portal routes                                                                      | —                                                         | —                                                                                       | last  |

### 2. Finance: no accounting ambiguity

- **Bill-to is always a contact or organization.** For a family membership it is the buyer (usually the holder, or a
  giver); for a family event registration it is the buyer; for a gift it is the payer
  ([11 §1](#12-11-membership-coverage-and-benefit-inheritance-strategy)). `BILL_TO_KINDS` stays closed. A household therefore has
  no balance, no statement, no aging line, no Stripe customer, no stored payment method and no tax position.
- **Payer identity is the person who paid**, recorded by Accounting as today (the bill-to, or the offline payer, or the
  card holder). "One receipt, many customers" (Decided 2026-10-19, Accounting milestone #11) is what lets one spouse pay
  invoices billed to the other; it is not a household feature and this proposal adds nothing to it.
- **Refunds and credits** follow the payer and the customer as today. A credit never crosses customers; a household
  cannot hold one.
- **Household finance views are read-side rollups**: the Household 360 Finance card and the "households with cumulative
  giving over $5,000" query sum per-member figures at query time, grouped by household membership **as of the period
  end** (so a person who left takes their history with them, and a report for 2025 uses 2025's composition). The ledger,
  the aging snapshot and the subledger never carry a household dimension.
- **Invoices for household products** may carry `custom_metadata.household_id` and the order `selling_context` the
  household id, so an invoice list can be filtered "for The Rivera-Chen Family" without a party change.
- **Shared payment methods** stay with one owner (Billing D-12). A spouse's card paying another member's invoice is the
  one-receipt case; auto-renew on a family membership stays with the payer who enrolled it, and an attention item is
  raised if that payer is removed as a beneficiary ([11 §7](#12-11-membership-coverage-and-benefit-inheritance-strategy)).

### 3. Events

The Events architecture (approved package 2026-10-20) already separates attendee, registrant and payer and models a
parent registering a child as "one order, one line, attendee ≠ buyer". this proposal asks for four additions in Events'
own plan:

1. **`registered_by_contact_id` on the registration** (today the registrant is only a history actor), and **a signer on
   `registration_consents`** (today no column says who consented). A guardian signing a minor's waiver is a consent row
   whose signer is the guardian, recorded with the guardian authorization it relied on (A4 §3).
2. **Family registration from a household**: at checkout a signed-in manager (or staff) picks attendees from the
   household's current members, each becoming a registration with the picker's `attendee_contact_id`; a minor cannot be
   registered by someone without guardian authorization; a covered child's own membership row gives the member rate
   through the existing `pricing_subject: recipient`. No "household registration" record: a household is a way to fill
   attendees, exactly as an organization fills a group.
3. **Guests versus family**: a guest stays a guest registration under a host (D-85); a family member is an attendee in
   their own right. The 1.0 "spouse/guest" package maps to "a named covered adult registers at the member rate" or "a
   guest registration", chosen by the registrant.
4. **Guest passes and check-in**: Events consumes the family's guest-pass pool (11 §4) at check-in; any covered member's
   card or lookup finds the household's active membership and the pool balance.

Eligibility by age (camps, youth programs) uses `crm.contact.age` in the Eligibility engine when it resumes; until then
Events' own age check on `date_of_birth`.

### 4. Marketing and communications

- **Consent is per contact, always** (D-M2, CRM owns the ledger). A household has no consent row; a "household opt-out"
  is every member opted out. A shared e-mail or phone records the contact who acted (the Marketing rule); this proposal
  adds nothing to that.
- **A household is an audience shape and a de-duplication rule.** Marketing's recipient-relationship kinds (`self`,
  `reference`, `organization_role`, `organization_affiliates`) gain `household_primary_contact` and `household_members`;
  the per-send option "Send once per address" generalises to **"Send once per household (to the primary contact; if they
  cannot receive it, the next mailable adult)"**. The piece is addressed with the household's `mailing_name` and
  `formal_salutation` or `informal_salutation`, exposed as merge fields (`household.*`) beside `contact.*`; a member who
  opted out of the channel is omitted from the salutation for that piece only when the tenant turns on "salute only
  consenting members" (default off: an opt-out governs delivery, not the family's name).
- **Direct mail**, when Marketing designs print, gets householding for free from the same rule; postal suppression
  (`do_not_mail`) is honoured per member, and a household whose every member is suppressed receives nothing.
- **E-mail is never sent to a household**: it goes to members, each under their own consent, with the same householding
  rule available so that a family newsletter reaches one adult once.
- **Communications (Platform)** sends to recipients that are addresses with optional contacts; CRM resolves "the
  household" into member recipients before any send, and a notice purpose that names a household record carries it as
  `records` for merge fields only.

### 5. Fundraising

The Fundraising proposal (ADR-16) computes household recognition from spouse pairs and stores the key nowhere. this
proposal replaces the key source and closes the three gaps it names, leaving the gift model intact:

- **Donor of record is a contact or an organization**, frozen on the gift; receipts go to the donor only. Unchanged.
- **Household recognition credits** (`gift_credits.credit_kind = soft`, `reason = household`) are written **explicitly
  at recording** for the other current members of the donor's household(s) **as of the gift date**, under the tenant's
  recognition rule (`household_credit_mode: none | soft_credit`, which members: adults only or all), as NPC's explicit
  rows rather than NPSP's regenerated roles. A later change in household composition does not rewrite past credits; a
  correction is a reversal.
- **Household giving total** = Σ gifts by any member while a member (household membership as of the gift date),
  de-duplicated by gift id, never summing soft credits with hard credits. Served by a published CRM function
  `crm.household_of_contact_on(contact_id, date)` and a Fundraising view, grouped at the household grain
  ([§7](#7-the-query-layer)).
- **Recognition name**: `households.mailing_name` (or an override) is the default `household_recognition_name`;
  Fundraising keeps its own override on `donor_profiles` for a donor whose recognition differs from their mailing name;
  `household_opt_out` added to `donor_profiles` as the proposal already names.
- **Giving societies and statements**: basis `household` resolves through the same function; a household giving
  statement is addressed to the primary contact with the mailing name, lists each member's gifts with their donor of
  record, and is never a tax document (receipts stay per donor).
- **Anonymity**: a household soft credit inherits the gift's `is_anonymous` (closing the proposal's gap).
- **Pledges and recurring gifts** stay with their donor; a payer in the same household is the ordinary payer field.

### 6. Portals

Designed in [A4 §4](#19-a4-security-privacy-and-authorization) and built last, on Andrew's components: a household manager's
household page (members, address, memberships, guest passes), guardian access to a minor's profile, renew and register
for the family, and nothing that lets one adult see another adult's personal record without that adult's consent.

### 7. The query layer

Households are a dataset (`crm.households`, source view `households_for_queries`), with these additions on existing
datasets, all within what Platform serves today (`collections` + `exists`, single-row picks, configured relationships,
computed view columns, Platform association families; no to-many join in Explorer; `count_distinct` in reports):

| Dataset                                                         | Addition                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `crm.households`                                                | fields: name, mailing name, salutations, type, status, primary contact (pick, proven by `household_members_one_primary_contact_uidx`), region, `current_member_count`, `member_count`, youngest and oldest member birth dates, dissolved, archived; collections: `members` (contact reference, role, flags, dates), `memberships_covering_members` (Phase 2: memberships held by or covering current members, through Membership's beneficiaries dataset), `addresses`, `phones`; relationships: `primary_contact.*` hops |
| `crm.contacts`                                                  | `primary_household` pick (proven by `household_members_one_primary_household_uidx`) with `primary_household.name` etc.; collection `households` (household reference, role, flags, dates) for `exists`; relationship families from Platform associations (`crm_parent_child`, `crm_spouse`, …)                                                                                                                                                                                                                            |
| `crm.household_members`                                         | a child dataset for lists of member rows themselves (exports, the 360 table)                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `membership.memberships`, `membership.membership_beneficiaries` | `origin = covered`, `billing_contact_id`; beneficiaries dataset with `coverage_role`, `basis`, `basis_record_type/id` (Phase 2)                                                                                                                                                                                                                                                                                                                                                                                           |
| Fundraising                                                     | gifts with `household_id_at_gift` computed through the published function (Phase 3)                                                                                                                                                                                                                                                                                                                                                                                                                                       |

The brief's six queries, in the request grammar's terms:

| Query                                                                            | Shape                                                                                                                                                                                                                                                                            | Double counting avoided by                                                 |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Households with active family memberships                                        | `crm.households` where `exists(memberships: program = Family and status_today in (active, grace))`                                                                                                                                                                               | `exists`, never a join                                                     |
| Households with children under 12                                                | `crm.households` where `exists(members: end_date is null and contact.date_of_birth > today − 12 years)` (or `youngest_member_date_of_birth > …` for the simple case)                                                                                                             | `exists`; the view column is one value per household                       |
| Households with a membership renewal due in 30 days                              | `crm.households` where `next_renewal_on between today and today + 30` (computed lateral over principal memberships, `LIMIT 1` on the earliest) or `exists(memberships: current_term.ends_on …)`                                                                                  | lateral `LIMIT 1`                                                          |
| Households where one person attended an event but another is the billing contact | `crm.households` where `exists(members: contact exists(registrations: event = X and attended))` and `exists(memberships: billing_contact_id ≠ that member)`; in practice two `exists` over members with different predicates, which the grammar allows at one nesting level each | `exists`                                                                   |
| Households with cumulative giving over $5,000                                    | Fundraising's household giving view (one row per household and period, Σ over gifts de-duplicated by gift id, grouped at household grain) joined many-to-one from `crm.households`; filter `lifetime_total > 5000`                                                               | the sum lives on a household-grain view, never a `sum` over a to-many join |
| Contacts who belong to multiple households                                       | `crm.contacts` where `household_count > 1` (a computed column on `contacts_for_queries`: count of open member rows), or `exists(households) group having count > 1` once the aggregate request shape ships                                                                       | computed column                                                            |

Explorer counts remain `count(DISTINCT base id)`; a report that joins households to members one-to-many documents the
repetition and uses `count_distinct`; sums are always declared on a household-grain or member-grain view, never on a
join. Relationship-aware criteria in the visual builder (E-17) are a later query-designer milestone; the `exists`
criteria above work in saved queries and recipes today.

### 8. Data Engine and integrations

- **Imports**: operations `crm.households/upsert` (fields, addresses, phones, overrides, external key) and
  `crm.household_members/upsert` (household source key, contact source key, role, flags, dates), with `dependsOn`
  `crm.contacts` and `crm.households` and the blocking behaviour of CRM's contact import (a missing parent blocks the
  row, retried when the parent lands, never rejected); relationships through `platform.associations/upsert` once it
  learns to block rather than reject and lists `crm.contacts` in its dependencies. Dependency-ordered groups follow the
  Data Run manifest (`depends_on`) until E-11 is built.
- **Exports**: `crm.households`, `crm.household_members` (a link dataset, the many-to-many convention of the tenant data
  export) and `crm.household_addresses`/`_phone_numbers`; relationships export with Platform's associations dataset.
- **Bulk update** on households (names, type, status) through the engine, ≤500 ids, validate then apply, rollback.
- **Duplicate resolution**: household merges are one-at-a-time merge jobs as contact merges are (bulk merge is Absent
  product-wide).
- **Sandbox copy**: Absent product-wide; when it exists, household tables copy with CRM's others (keys are
  `(tenant_id, id)`).
- **External CRM integrations** (Integration Platform): households and member rows are catalogued objects with
  create/update/merge/delete events; a connector mapping from NPSP's Household Account or Nonprofit Cloud's Party
  Relationship Group is a crosswalk from their household id to `crm.households.sys_external_id` and from their
  contact-household link to member rows ([12](#13-12-migration-and-implementation-implications)).
- **Integrity, audit and rollback**: household rows are tenant-keyed, RLS-forced, composite-FK'd inside CRM, history-
  captured, event-published and purge-classified like every CRM record; an import's rollback removes household rows and
  member rows it created and restores the addresses it linked.

### 9. Published reads CRM adds (CRM-ADR-05)

`crm.households_summary` (id, number, names, type, status, primary contact, preferred address, counts),
`crm.household_members_current`, `crm.household_of_contact_on(contact_id, date)` (the household(s) a contact was an open
member of on a date, primary first), `crm.household_primary_contact(household_id)`, `crm.guardians_of(contact_id)` (A4).
Each carries the compatibility promise; physical tables stay readable without it.

## 19. A4. Security, Privacy and Authorization

## A4. Security, Privacy and Authorization

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Evidence:
[r4](#24-r4-platform-primitives-for-households-person-to-person-relationships-and-family-memberships) §4, [r5](#25-r5-console-and-portal-architecture-for-households-research-report) §6–§7, [r6](#26-r6-rhythm-1-0-precedent-for-households-families-and-person-to-person-links) §1.3.

Household membership grants no authority. The management-companies lesson (deferred 2026-10-18) applies here: _a
relationship grants nothing; a grant grants a role; the grant lives only while the relationship is in force._

The governing Platform facts are unchanged: permissions are declared in the owner and resolved by Platform Security
(CRM-ADR-11); portal identity is one contact per login (`cid`); there is no impersonation and no delegation primitive.
The one reusable mechanism is a **contact-keyed derived assignment**, implemented through
`platform.sync_derived_assignments`, with sources `committees` and `eligibility` today.

### 1. Principles

1. **Household membership confers no authority.** Being in a household never lets anyone see, edit or act for another
   member. Managers and guardians require explicit, dated grants.
2. **A minor has no login.** A contact under the tenant's age of majority cannot link a portal user. A guardian acts
   under their own login through a grant scoped to the minor.
3. **An adult's personal record is theirs.** No household role lets one adult edit another adult's name, e-mail,
   consent, password or profile. The only exception is an adult's invitation to a household manager to update their
   contact information, expressed as an opt-in on their own record. This exception covers contact information, never
   identity or consent.
4. **Grants are derived, dated and revocable.** They are written as ordinary scoped security assignments, so every
   authorizer path, audit view and "who can see this" answer works the same way as for any role.
5. **Staff scoping is unchanged** (CRM-Q8 open). Console permissions on households remain tenant-wide keys in the CRM
   application until record-level security arrives in CRM-M23. At that point, a household is scoped like a contact, by
   the region or organization of its primary contact.

### 2. Console permissions

CRM declares the permissions through `definePermissions(crmService.app, 'households', …)`, `'household-members'`,
`'relationships'`, `'household-types'`, `'household-roles'` and `'relationship-types'`. They are catalogued and
published at deploy, then reconciled into the built-in CRM policies by tier:

| Key                                                                          | Tier                      | Grants                                                                           |
| ---------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------- |
| `crm.households.read`, `.query`                                              | read                      | Lists, 360, picker, Quick Search results                                         |
| `crm.households.create`, `.update`, `.archive`, `.merge`                     | user / admin as contacts' | Create, edit (including name overrides), archive and unarchive, merge            |
| `crm.households.dissolve`, `.reinstate`                                      | user                      | The lifecycle operations ([A1 §5](#16-a1-household-lifecycle-rules))                        |
| `crm.households.delete`                                                      | admin                     | Hard delete of an unreferenced household; the impact path with CRM-M20           |
| `crm.household-members.add`, `.update`, `.end`, `.remove`                    | user                      | Member operations (W3: each relationship mutation has its own key)               |
| `crm.household-members.grant-manager`                                        | admin                     | Set or clear `is_manager` (the one flag that produces portal authority)          |
| `crm.relationships.create`, `.update`, `.end`, `.remove`                     | user                      | Contact-to-contact relationships through CRM's routes over Platform associations |
| `crm.household-types.*`, `crm.household-roles.*`, `crm.relationship-types.*` | admin                     | Configuration                                                                    |
| `crm.households.history`                                                     | read                      | Version history                                                                  |

Every operation writes the household's revision and, for member rows, the contact's revision (`sys_source`, actor).
Configuration changes go to `platform.security_audit_events` under the new entity types `household_type`,
`household_role` and `relationship_type_settings`. Regenerate the full-list CHECK after merging master.

### 3. Portal authority: managers and guardians

A new Platform derivation source, `crm`, produces two derived roles. This requires a migration adding the source to the
`security_assignments` CHECK and the grantor role, as `eligibility` was added on 2026-10-18. CRM re-derives the roles
within the transaction of every change that affects them; a nightly sweep handles date-driven changes:

| Role                                   | Derived from                                                                                                                                                                                                                                   | Scope                      | Keys it carries (examples)                                                                                                                                                                                                                                                                          | Ends when                                                                                                                                                   |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Household Manager**                  | An open member row with `is_manager = true`, for a contact of active status who is at least the age of majority                                                                                                                                | `household:<household id>` | `crm.household.portal.read`, `.update-contact-information` (household addresses and phones), `.members.add` (new contacts only, or existing contacts who accept an invitation), `.members.end` (not self), `membership.household.manage`, `membership.household.renew`, `events.household.register` | The row ends, the flag is cleared, the contact's status is released, or the household is archived                                                           |
| **Guardian**                           | An open relationship of a type with `may_authorize_access` (parent of, guardian of) from an adult to a contact who is a **minor** (age from `date_of_birth` below the tenant's age of majority)                                                | `contact:<minor id>`       | `crm.contact.portal.read-as-guardian`, `.update-as-guardian` (profile fields except identity locks), `events.register-for`, `membership.covered.view`, `submissions.act-for`                                                                                                                        | The relationship ends, the minor reaches the age of majority (the derivation runs on the birthday), either contact's status is released, or staff revoke it |
| **Authorized representative** (adults) | An explicit, staff-recorded relationship of type `authorized_representative` (seeded, `may_authorize_access`, requires `confirmed_by_staff` on the settings row) between two adults, or a dependent adult's own consent recorded in the portal | `contact:<represented id>` | The guardian key set                                                                                                                                                                                                                                                                                | The relationship ends or is revoked; reviewed yearly (an attention item to staff)                                                                           |

The derivation enforces these rules:

- **A grant never unions identities.** The manager acts as themselves, under their own `cid`. Routes receive the
  household or contact id in the path and call `requireAccess` with the resource scope. The audit records both the
  acting contact and the represented record; Platform's `acting_for_user_id` audit columns are reserved for exactly
  this.
- **Minors are never managers and never managers' targets by membership alone.** A manager may register a minor only if
  they are also that minor's guardian. The console makes the distinction explicit: "Ana manages the household and is
  Maya's guardian; Rosa manages the household but is not a guardian of anyone".
- **Adult-to-adult access is never derived from a household.** Wei cannot read Ana's profile because they share a
  household or a spouse relationship. Only Ana's own opt-in, "Let household managers update my contact information",
  produces a narrow `update-contact-information-for` grant scoped to Ana. Ana can revoke it at any time.
- **Age is computed, never stored.** The calculation uses `date_of_birth` and the tenant's `age_of_majority`, which
  defaults to 18 and is configurable per tenant. Majority differs by jurisdiction, and some programs use 13 or 16 for
  account creation. A contact with no birth date is treated as an adult for access: no guardian grants are derived over
  them. On a household with a guardian relationship, the console flags that contact as "age unknown".
- **Consent and privacy for minors**: a minor's record shows a "Minor" badge, and their e-mail and phone are never used
  as a login identity. Marketing sends to a minor's address only under a guardian's recorded consent; the consent ledger
  refuses any other consent row for a minor, a rule Marketing's plan adopts. A minor's data is excluded from directories
  (CRM-Q4) by default, and their portal profile is reachable only by guardians and staff.
- **Revocation and review**: staff may end any grant's source by ending the relationship or clearing the flag. Every
  derived grant is visible on Contact 360's Access card: "Guardian of Maya Chen until 2033-03-12, from the Parent of
  relationship". An attention item lists guardian grants over contacts who reach the tenant's age of majority in the
  next 60 days.

### 4. Portal self-service (last)

Portal self-service is built after every other scheduled milestone, on Andrew's portal components, with the routes and
rules below. The `web-contacts` organization-roster rules are the model: detach never deletes, a manager cannot remove
themselves, and no route writes another adult's personal record:

| Page                | Who                                                                                        | Does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| My household(s)     | Any member                                                                                 | Sees the household name, address, members' names and roles (no personal details), the memberships held by or covering members; a member of several households switches between them                                                                                                                                                                                                                                                                                                                   |
| Manage household    | Household Manager                                                                          | Edits household address and phones (`PUT /households/{t}/portal/me/households/{id}/contact_information`); adds a **new** contact as a member (creates the contact at this household; the one place a portal user may create another person) or invites an **existing** contact by e-mail (the invitee accepts under their own login; never a silent link, the Wild Apricot and iMIS failure in reverse); ends a member row (`end`, never delete; not self); cannot change the primary contact (staff) |
| My profile          | The contact                                                                                | As CRM-M22 defines; plus the opt-in "Let household managers update my contact information" and a view of grants over me and grants I hold                                                                                                                                                                                                                                                                                                                                                             |
| A minor's profile   | Guardian                                                                                   | Reads and updates the minor's profile fields the tenant allows (allergies, emergency contact, interests), never identity locks or consent beyond the guardian's own consent rows                                                                                                                                                                                                                                                                                                                      |
| Family membership   | Holder; adult beneficiary (renew and pay only, when the package allows); Household Manager | Renews, adjusts beneficiaries within the package rules (proposals from household members and relationships), buys an additional beneficiary seat, sees the guest-pass balance; pays as buyer and bill-to                                                                                                                                                                                                                                                                                              |
| Register the family | Manager and guardian                                                                       | Registers self, covered adults who accepted the household (adults register themselves unless they opted in), and minors they guard; signs waivers as the guardian with the authorization recorded on the consent row                                                                                                                                                                                                                                                                                  |
| Access requests     | Any adult                                                                                  | Requests representative access to another adult (the 1.0 RB-2274 idea, delivered with its flow this time): the other adult accepts in their portal, a dated `authorized_representative` relationship is recorded, the grant derives                                                                                                                                                                                                                                                                   |

Every rule is enforced in the service (CRM, Membership, Events) under `requireAccess`. The portal's `mayManage` flags
are courtesies, not enforcement.

### 5. Tenant isolation and data protection

- Every household table is tenant-keyed with forced RLS. Cross-schema references are text ids verified at write. The
  derivation function runs per tenant inside the caller's transaction, as `sync_derived_assignments` does.
- Personal data classification: household names and salutations are personal data because they contain names; member
  rows are relationship data. `date_of_birth` stays sensitive, following the 1.0 directory leak, ZD 20897. The tenant
  purge removes household rows with history capture suppressed, as associations are.
- The SOC 2 controls applicable to the new component are tenant isolation (TC-TEN-001), authorization (TC-AZ-001..003),
  personal information (TC-PI-001, TC-PI-003) and logging (TC-LOG-001). They are listed with the module in services-crm
  `docs/compliance/control-coverage.md`.

### 6. What is deliberately not done

No impersonation; no login that switches between contacts; no household-wide consent; no "head of household" authority
by default; no guardian grant derived from household membership; no portal route that creates a login for a minor; no
directory exposure of minors; no delegation outside the three derived roles.

## 20. A5. Testing Requirements

## A5. Testing Requirements

Part of [Households, Constituent Relationships, and Membership Coverage](#1-households-constituent-relationships-and-membership-coverage-research-and-architecture-recommendation). Tests follow the Rhythm standards
(services-platform `docs/testing-standards.md`; RHYTHM-CRM-ARCH-001 `16` §1): unit for pure rules, table integration for
database invariants, service integration for business rules and concurrency, contract kits for participants, end-to-end
for routes, deployed-dev for wiring, console component tests, and product-acceptance Playwright journeys run on the
fleet in the acceptance workspaces. Every invariant in chapter [09 §6](#10-09-recommended-domain-model) and every 1.0
defect pattern in chapter [03](#4-03-rhythm-1-0-functionality-and-lessons-learned) becomes a named test that must exist before the
milestone that touches it is accepted.

### 1. Relationships (CRM over Platform associations)

| Test                                                                                                                                                                                 | Level                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- |
| Seed set creates the eight system association types and settings rows once per tenant; a renamed type keeps its `system_code`; a deactivated one is skipped by naming and derivation | Service integration  |
| A symmetric type stored once in canonical order; the reverse order is normalised; a duplicate open pair is refused naming the existing row (I-6)                                     | Service integration  |
| A directional type reads with the forward label on one contact and the reverse on the other                                                                                          | E2e                  |
| A second open `crm_spouse` for a contact is refused naming the existing spouse; `spouse` and `partner` exclusive for a pair                                                          | Service integration  |
| Ending versus deleting: a dated type's open row is ended, an ended row may be deleted; dates ordered; self-relationship refused                                                      | E2e                  |
| Spouse backfill: reciprocal pairs become one association; half pairs flagged; the projection `spouse_id` equals the open spouse; idempotent re-run                                   | Data job integration |
| Contact merge: Platform's participant re-points, collapses colliders, removes a would-be self-relationship with a warning; CRM's warning names the households when they differ       | Contract + service   |
| Contact delete (today's path) applies `platform.apply_endpoint_deletion`; a `restrict` type refuses with the message                                                                 | Service integration  |
| Query families: `crm_parent_child` collection answers "contacts who are parent of a contact under 12" with one row per contact                                                       | Query integration    |

### 2. Households and members

| Test                                                                                                                                                                                                                                                                | Level                             |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| Every constraint and index by name (UUIDv7 check, number uniqueness, status list, override checks, dissolution pair, archive pair, flags-open check, dates, the two partial unique indexes, the exclusion constraint), the tenant policy, the normalization trigger | Table integration                 |
| A direct insert of a second open primary contact, a second open primary household, or an overlapping member row is refused (I-1, I-2, I-3)                                                                                                                          | Table integration                 |
| Ending the primary contact with other open members requires a successor in the same save; with no other member it is allowed; the deferred trigger refuses a committed household with members and no primary                                                        | Service integration               |
| First member becomes primary contact; first household becomes the contact's primary; a save may say otherwise                                                                                                                                                       | Service integration               |
| Role default by age from birth date and the tenant's age of majority; no birth date → adult                                                                                                                                                                         | Unit                              |
| Ending a member row disconnects linked addresses and phones (`member_ended`), clears flags, recomputes names, re-derives grants, in one transaction; a failure rolls everything back (I-4)                                                                          | Service integration               |
| Removing a member with linked contact information is refused unless re-pointed, copied or removed in the same request                                                                                                                                               | Service integration               |
| Omitted `members`, `addresses`, `phone_numbers` unchanged; `[]` changes nothing (I-12)                                                                                                                                                                              | E2e                               |
| 50-member limit; a concurrent 51st add is refused under the row lock                                                                                                                                                                                                | Service integration (two writers) |
| Two concurrent saves of one household: the second gets 409 `StaleObject` with `latest_change`; a no-op save writes no version, stamp or revision                                                                                                                    | Service integration               |
| Archive refused while a write is pending; archive never ends member rows; unarchive restores                                                                                                                                                                        | E2e                               |
| Dissolve ends every open row with `dissolved`, disconnects links, recomputes names, archives; reinstate reopens eligible rows and lists skipped ones                                                                                                                | Service integration               |
| Hard delete refused while any member row, activity link or task names the household                                                                                                                                                                                 | Service integration               |
| History: every member change appears in the household's and the contact's revisions with actor and source                                                                                                                                                           | Service integration               |
| Business events published for every operation with ids only                                                                                                                                                                                                         | E2e                               |

### 3. Names and addresses

| Test                                                                                                                                                                                            | Level               |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| Naming: one shared last name, two last names, three or more adults, nickname, prefix, deceased exclusion, `exclude_from_naming`, each format, tenant suffix and connector, per-household format | Unit (table-driven) |
| Overrides survive member changes and contact renames; "restore" returns to the computed value; no job touches overrides                                                                         | Service integration |
| A contact save that changes name parts, status or death date recomputes every household it is an open member of, inside the save (I-5); an import through the same aggregate does too           | Service integration |
| Household address edit resolves on every linked member's effective address; copied and owned rows unchanged                                                                                     | Query integration   |
| Removing a household address turns linked rows `disconnected` with `household_address_removed`; the three remedies work                                                                         | Service integration |
| Apply-household-address-to-members links the right rows and reports skips                                                                                                                       | E2e                 |
| Region assignment from the preferred address                                                                                                                                                    | Service integration |

### 4. Merge and duplicates

| Test                                                                                                                                                                                                                                                                                                                             | Level                              |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| Household merge: union of members with collisions collapsed (earliest kept, other ended `merged`), one primary contact decided, addresses per type, linked member addresses re-pointed or disconnected, activity links and tasks reassigned, alias and 410, zero references to the merged id in every participating schema (I-8) | Contract + service + registry walk |
| Executed merge equals the preview field by field; a participant blocker rolls back everything                                                                                                                                                                                                                                    | Service integration                |
| Contact merge: the survivor is an open member of each household at most once; beneficiary rows re-point `contact_id` (Phase 2)                                                                                                                                                                                                   | Service integration                |
| Coverage guard: a new column named `household_id` or `*household_member_id` without a registry entry fails CI                                                                                                                                                                                                                    | Unit (guard spec)                  |
| Duplicate rules: same name and postal code; same member set; a shared household is negative evidence for contact pairs                                                                                                                                                                                                           | Unit + service                     |

### 5. Membership coverage (Phase 2)

| Test                                                                                                                                                                                                                                                                               | Level                                        |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| Every constraint on `membership_beneficiaries` and `package_coverage_rules` by name; the open-row uniqueness                                                                                                                                                                       | Table integration                            |
| Rule evaluation for each predicate kind against CRM facts (household member by role, related by type and direction, affiliated by role with cascade, designated, any contact), each cap, each age window and basis                                                                 | Service integration (fixtures per predicate) |
| Designation: proposals list household members first, then related, then affiliated; a sixth dependent refused unless an add-on prices it; a person already covered twice in one program refused                                                                                    | Service integration                          |
| Derivation: an affiliation ending ends the row and the child membership on the event; a sweep catches a missed event; a disabled rule stops and does not revoke; evaluation is idempotent and diff-gated (the 1.0 defect list, RB-5496, RB-6105, RB-7487, RB-2259, as named tests) | Service integration                          |
| `follows_predicate = false` raises an attention item and keeps coverage to the term's end; `true` ends it on the member row's end date                                                                                                                                             | Service integration                          |
| Age-out: `term_start` keeps a child covered through the term and drops them at renewal with the reason; `continuous` ends the row on the birthday with the notice sent 60 days before                                                                                              | Service integration (clock-controlled)       |
| Every open beneficiary row has exactly one open child membership mirroring the parent's term; suspension and termination cascade; cancellation ends rows `membership_ended` (I-9)                                                                                                  | Service integration                          |
| `/entitlements` answers holder, beneficiary with basis, two programs, none, as-of dates; identical for individual, family and organizational memberships (I-10)                                                                                                                    | E2e                                          |
| Spin-off creates a principal membership for the leaver with the program's package and proration policy; transfer (D-43) moves the holder and keeps rows                                                                                                                            | Service integration                          |
| Who may renew: holder, adult beneficiary when allowed, manager grant, staff; coverage changes refused to an adult beneficiary                                                                                                                                                      | Service integration (authorization)          |
| Membership merge participant re-points `contact_id` on beneficiaries and the covered child rows; delete-impact answers block for open rows                                                                                                                                         | Contract                                     |
| No household id anywhere in Membership, Commerce or Accounting tables (I-11)                                                                                                                                                                                                       | Unit (schema scan)                           |

### 6. Authorization and portal (last)

| Test                                                                                                                                                                                              | Level                                  |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| Manager grant derives from the flag for an adult active member; ends on flag clear, row end, released status, archive; never for a minor                                                          | Service integration                    |
| Guardian grant derives from an access-authorising relationship while the other contact is a minor; ends on the birthday, relationship end, released status, revocation; none without a birth date | Service integration (clock-controlled) |
| No grant from household membership alone; an adult's record is not editable by another adult without the opt-in; the opt-in grant is narrow and revocable                                         | Service integration + e2e              |
| Portal routes refuse without the grant (403), refuse removing self (409 with advice), never delete a person, never create a login for a minor                                                     | E2e                                    |
| Invitation of an existing contact requires the invitee's acceptance; verification state is explicit                                                                                               | E2e                                    |
| Audit rows name the acting contact and the represented record                                                                                                                                     | Service integration                    |

### 7. Queries and reporting

| Test                                                                                                                                                           | Level                                       |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| The six brief queries ([A3 §7](#18-a3-cross-application-integration)) return one row per household or contact, counts equal direct SQL, no duplicate rows from member joins | Query integration                           |
| Household giving total equals Σ distinct gifts by members as of the gift date; a member in two households is counted once under the primary-household mode     | Query integration (Phase 3)                 |
| Every Explorer filter offered on households is indexed; p95 under 1 s per page on a 1,000,000-contact, 300,000-household tenant                                | Performance (release-candidate environment) |
| The dataset permission key is in Platform's list (a non-staff user can query households)                                                                       | Unit (Platform spec)                        |

### 8. Data import and interoperability

| Test                                                                                                                                                                                                                                      | Level                            |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| Validate and apply give identical per-row outcomes for households, members and relationships; outcomes sum to input rows; a missing parent blocks, never rejects; a rollback leaves no household rows and restores converted address rows | Engine integration               |
| NPSP and Nonprofit Cloud sample exports map to households, members, links, overrides and relationships as chapter [12 §2](#13-12-migration-and-implementation-implications) says                                                                        | Mapper unit + engine integration |
| Crosswalk: a second run updates; a merge re-points source keys                                                                                                                                                                            | Engine integration               |
| Tenant lifecycle: purge, reset and export cover every household table (completeness test)                                                                                                                                                 | Lifecycle integration            |

### 9. Tenant isolation

| Test                                                                                                                                              | Level               |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| Every household table has the forced policy; a cross-tenant read or write under the application role returns nothing or fails (RLS coverage spec) | Table integration   |
| Published functions refuse a tenant other than the transaction's (`42501`)                                                                        | Service integration |
| Derivations run per tenant and never touch another tenant's assignments                                                                           | Service integration |

### 10. Acceptance journeys (guided, in `acme-acceptance-guided`, after a green cumulative run)

Phase 1: create a household from a contact, add an existing spouse and two new children with relationships, link
addresses, move the household, separate the couple into two households with a child in both, merge two duplicate
households, configure a relationship type, run the "families with young children" query, import a spreadsheet of
families. Phase 2: sell a family membership with proposals, scan a child's entitlement at the gate, renew and age out a
child, spin off a separated spouse, derive employees for a corporate membership and watch a leaver lose coverage, read
the entitlement explanation for each. Portal (last): a manager updates the household address and invites an adult; a
guardian registers a child and signs the waiver; an adult tries and fails to edit another adult's profile.

## 21. R1 — Rhythm 2.0 CRM macroservice: what a Households / Relationships / Family-membership proposal can build on

## R1 — Rhythm 2.0 CRM macroservice: what a Households / Relationships / Family-membership proposal can build on

Written 2026-10-21 04:30 ET. Read-only research; nothing in any repository was changed.

### 0. What was read, and which commit the citations point at

- **Primary checkout** `~/rhythm2/services-crm` is on `master` at **`eac3df30`** (2026-10-18 21:07 ET) and is **255
  commits behind `origin/master`** (`git rev-list --count master..origin/master` = 255; `origin/master` = `eee6a5f4`,
  2026-10-21 00:39 ET, last fetched 2026-10-21 04:19 ET). The files that matter here changed in those 255 commits
  (archive columns, `disconnected` contact information, reciprocal spouse, D-09 "omitted affiliations are unchanged",
  record locks, create digests, organization history:
  `git diff --stat eac3df30 eee6a5f4 -- modules/entities/db lib/db modules/merges/references docs` = 29 files,
  +1,555/−339).
- So **every citation below is to the worktree `/Users/andrew/rhythm2/worktrees/services-crm/element-ids-land`, which is
  clean and at exactly `eee6a5f4` = `origin/master`** (branch `crm-m1-handoff`). Written below as `W/…`. Line numbers
  are from that commit.
- The architecture document RHYTHM-CRM-ARCH-001 was read at
  `/Users/andrew/rhythm2/worktrees/services-platform/crm-households/docs/proposals/crm/` (written as `ARCH/…`): 03, 04,
  05 §5, 07, 08, 09, 12, 13 §3.2, 14 rows RL-01/RL-02 and evidence/e03. Its evidence is pinned to services-crm
  `a980b553` (2026-10-19); several of its "designed, not built" statements are now built on `eee6a5f4` and are flagged
  below as **newer than ARCH**.
- Platform facts come from `~/rhythm2/services-platform` at `0614c54cf` (2026-10-20 12:22 ET; `origin/master` is
  `b4b9e6d7d`, 2026-10-21 04:23 ET), written as `P/…`. react-console was only listed (`ls`/`grep -l` on file names),
  never modified; it is at `13e4bcee9`.

Labels: **Implemented** (verified in code, DDL or a named test), **Decided** (Andrew, dated, where recorded),
**Planned** (in a proposal or milestone record), **Absent**.

---

### 1. Contacts and organizations

ARCH 04 §3.1 already tabulates both records; this section adds what it does not say or what changed since `a980b553`.

#### 1.1 `crm.contacts` — Implemented

`W/modules/entities/db/contacts.table.ts`

- Key `(tenant_id, id)` (`:142`), `number` unique per tenant (`:143`, allocated from `platform.number_seeds`, scope
  `:66`).
- **Statuses** `CONTACT_STATUSES = ['active','duplicate','deceased','inactive']` (`:52`), CHECK `contacts_status_check`
  (`:159`). **Newer than ARCH:** `RELEASED_CONTACT_STATUSES = ['inactive','deceased']` (`:60`) — such a contact leaves
  pickers, gets no automatic role and releases its primary e-mail (CRM-M1 decision **M1-D1, Andrew 2026-10-19 17:36
  ET**, `W/docs/milestones/CRM-M1-contacts-and-organizations.md:34`; migration `20261019223631`).
- **Name composition** (`:86-91`): `name` = first, middle initial, "nickname", last, suffix, ", credentials";
  `full_name` = `name` with `prefix` in front; `name_last_first` = `Last, First M. "Nick"`. All three are server-owned
  (`SERVER_OWNED_CONTACT_COLUMNS` `:248-256`), recomputed on every write by `contactNames` in
  `W/modules/entities/services/persistence/contacts.ts`, and a client may not send them (`contact-api-mapper.ts`).
  First-or-last name required: `contacts_first_or_last_name_check` (`:165-168`, added NOT VALID in migration 0023). Name
  parts: `prefix, salutation, first_name, middle_name, last_name, maiden_name, nickname, suffix, credentials`
  (`:75-83`). **`salutation` exists as a stored text column (`:76`); there is no addressee / "informal name" /
  household-name / mailing-name column and nothing composes one — Absent.**
- Field limits (M1-D3): names 200, notes 10,000, e-mail 254 (`W/modules/entities/db/field-limits.ts:10-16`).
- **`spouse_id`** (`:118`): composite FK `contacts_spouse_fk (tenant_id, spouse_id) → contacts (tenant_id, id)` with no
  delete action (`:144`), index `contacts_spouse_idx` (`:148`), `contacts_spouse_not_self_check` (`:161`). How it is
  written and whether it is symmetric: see §4.
- **Archive columns** (newer than ARCH 04 §3.3 "being added" — now Implemented):
  `archived_at, archived_by_id, archive_reason, unarchive_reason` from `W/modules/entities/db/archive-columns.ts:32-37`,
  CHECKs `<table>_archive_check` (`:40-45`) and `<table>_unarchive_reason_check` (`:48-52`); migrations
  `20261019203536_contacts_organizations_archive.sql` and `20261020130415_unarchive_reason.sql`; contract
  `W/docs/archive.md:9-38`. Archive is not a status (`archive.md:18`). A save never writes them
  (`withoutArchiveColumns`, `archive-columns.ts:55-59`).
- `sys_*` columns: `snakeSystemColumns()` (`:139`: `sys_version`, `sys_created_at/_by_id`,
  `sys_last_modified_at/_by_id`, `sys_locked`, `sys_external_id`) plus two bookkeeping columns that never reach the
  wire: `sys_region_address_fingerprint` (`:103`) and `sys_create_digest` (`:110`, D-43 create retry,
  `W/lib/db/create-replays.ts`).
- Other scalars: districts (`:112-117`), gender/pronouns/dates/socials (`:120-131`),
  `do_not_mail/do_not_call/email_opt_out` (`:133-136`), `communication_preferences_last_confirmed` (`:137`),
  `origin_service(_record_id)` (`:132-133`).
- **The contact-owned table pattern** every owned collection reuses: `contactOwnedColumns()` (`:194-198`) and
  `contactOwnership()` (`:205-217`) = PK `(tenant_id,id)`, composite FK to the contact **`ON DELETE CASCADE`**, owner
  index.
- **UUIDv7 ids**: a contact the client names no id for gets a UUIDv7 (header `:15-17`); a client-chosen id is accepted
  when it is a canonical UUIDv7 (`UUID_V7_PATTERN`, `W/lib/db/owned-collections.ts:38`, D-22) and a create is
  replay-safe by digest (D-43, `:105-109`). **No CRM record table carries a database `uuidV7Check`** (grep of
  `W/modules/*/db/*.ts` and `W/db/migrations/*.sql` finds none) — Absent at the database, as ARCH 04 §1 row X-03 says.
  Configuration tables are checked by `<table>_id_configuration_id_check` (e.g.
  `W/modules/entities/db/affiliation-types.table.ts:51-59`).

#### 1.2 `crm.organizations` — Implemented

`W/modules/entities/db/organizations.table.ts`

- `ORGANIZATION_STATUSES = ['active','duplicate','inactive']` (`:47`), `name NOT NULL` (`:63`), `acronym`,
  `description`, `notes`, `website`, `domain_name`, `alternate_domain_names[]`, `flags[]`, `tax_exempt`, `logo_url`,
  socials (`:66-88`); `parent_organization_id` (`:74`) with FK `organizations_parent_fk` (`:99`, no delete action) and
  `organizations_parent_not_self_check` (`:115`); archive columns and checks (`:93`, `:121-122`);
  `contact_role_compliance_status` server-owned (`:91`).
- Organization-owned pattern `organizationOwnership()` (`:155-167`, cascade). **No organization e-mail table;
  organization phone numbers have no preferred flag** (ARCH e03 §6) — Absent.
- Number scope separate from contacts' (`:54`, `:131-135`).

#### 1.3 Custom-field value tables — Implemented

`W/modules/entities/db/contact-custom-field-values.table.ts:17-21` and
`W/modules/entities/db/organization-custom-field-values.table.ts:15-19`: built by Platform's factory
`customFieldValuesTable(crmSchema, '<table>', { tenantId, id, foreignKeyName })` from
`@rhythmsoftware/macroservice-custom-fields/custom-field-values`; the binding
`CONTACT_CUSTOM_FIELDS = { objectId, table, ownerLabel }` (`:29-35`). **Newer than ARCH:** migration
`20261019224351_drop_activity_and_contact_role_custom_field_values.sql` dropped the activity and contact-role value
tables, so today only contacts and organizations carry custom fields in CRM (consistent with R2-ARCH-001, §6 below).

---

### 2. Affiliations (the primary candidate primitive for "household membership")

ARCH 04 §3.1 and e03 §5 describe the model; `W/docs/affiliations/README.md` (accepted 2026-10-14) is the binding design.
Below is the DDL-level precision the proposal needs, and what is **organization-specific**.

#### 2.1 `crm.contact_organization_affiliations` — Implemented

`W/modules/entities/db/contact-organization-affiliations.table.ts:52-119`

Columns (`:54-70`): `tenant_id, id, contact_id` (owned columns), `organization_id NOT NULL`,
`affiliation_type_id NOT NULL`, `is_primary` (default false), `job_title`, `start_date`, `end_date` (inclusive last
day), `notes`, `sys_automatic_roles_due` (server bookkeeping, D-46), `sys_*`.

Constraints, quoted:

```ts
// :74  owner key, cascade with the contact, no owner index (the contact+type index serves it)
...contactOwnership('contact_organization_affiliations', t, { owner_index: false }),
// :77  what a role's composite key names
unique('contact_organization_affiliations_contact_uq').on(t.tenant_id, t.id, t.contact_id),
// :80  what a linked contact address/phone names: affiliation + its contact + its organization
unique('contact_organization_affiliations_contact_organization_uq').on(t.tenant_id, t.id, t.contact_id, t.organization_id),
// :81-89  composite FKs, NO delete action: an organization or type an affiliation names cannot be deleted
foreignKey({ name: 'contact_organization_affiliations_organization_fk', columns: [t.tenant_id, t.organization_id], foreignColumns: [organizations.tenant_id, organizations.id] }),
foreignKey({ name: 'contact_organization_affiliations_affiliation_type_fk', columns: [t.tenant_id, t.affiliation_type_id], foreignColumns: [affiliationTypes.tenant_id, affiliationTypes.id] }),
// :92-94  D1 "one primary"
uniqueIndex('contact_organization_affiliations_one_primary_uidx').on(t.tenant_id, t.contact_id).where(sql`${t.is_primary}`),
// :112  D2 a primary is open-ended
check('contact_organization_affiliations_primary_open_check', sql`NOT ${t.is_primary} OR ${t.end_date} IS NULL`),
// :114-117  D3
check('contact_organization_affiliations_dates_check', sql`${t.end_date} IS NULL OR ${t.start_date} IS NULL OR ${t.end_date} >= ${t.start_date}`),
```

D4, the **exclusion constraint for overlapping periods**, lives only in SQL (drizzle cannot express it). Created in
`W/db/migrations/0065_contact_organization_affiliations.sql:162-165` and re-keyed in
`W/db/migrations/0067_address_phone_type_order_and_affiliation_type_defaults.sql:345-351`:

```sql
CREATE FUNCTION rolodex.overlap_exclusion_key(VARIADIC parts text[]) RETURNS text
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path = pg_catalog AS $$ SELECT parts::text $$;
ALTER TABLE "rolodex"."contact_organization_affiliations" ADD CONSTRAINT "contact_organization_affiliations_no_overlap_excl" EXCLUDE USING gist (
	rolodex.overlap_exclusion_key("tenant_id", "contact_id", "organization_id", "affiliation_type_id") WITH =,
	daterange("start_date", "end_date", '[]') WITH &&
) DEFERRABLE INITIALLY IMMEDIATE;
```

(The schema was renamed `crm` by migration 0075.) The same shape guards roles:
`contact_organization_roles_no_overlap_excl` over `(tenant_id, affiliation_id, contact_role_id)` (`0067:352-356`). The
key is an expression so no read can pick the GiST over the btree indexes (`0067:336-344`). The organization of an
affiliation is **immutable** after insert: trigger `contact_organization_affiliations_organization_immutable_trigger`
(`W/db/migrations/0066_contact_information_sources.sql:123-134`). Limit `MAX_AFFILIATIONS = 100` (`:135`);
effective-on-day helper `effectiveOn` (`:142-145`).

Indexes (`:96-110`): `(tenant,contact,type)` INCLUDE organization/is_primary/dates/id; `(tenant,organization,contact)`;
`(tenant,organization) WHERE end_date IS NULL` (current members, the hot path); `(tenant,type,organization)`; the D-46
due index.

#### 2.2 `crm.contact_organization_roles` — Implemented

`W/modules/entities/db/contact-organization-roles.table.ts:41-80`:
`affiliation_id, contact_id, contact_role_id, start_date, end_date`; FK
`(tenant_id, affiliation_id, contact_id) → affiliations (tenant_id, id, contact_id)` **ON DELETE CASCADE** (`:57-65`);
FK to `contact_roles` with no delete action (`:66-70`); `MAX_AFFILIATION_ROLES = 25` (`:87`); D5 "role dates within the
affiliation" is a deferred constraint trigger (header `:17-20`).

#### 2.3 Affiliation types — Implemented

`W/modules/entities/db/affiliation-types.table.ts:36-68`: `id` (configuration id, CHECK `:51-59`), `name` not blank
(`:62`), `description`, `is_active`. No layout slots, no `applies_to`, no "system" flag. Seeded per tenant: `employer`,
`school`, `other` (`W/modules/tenant-lifecycle/services/participants/affiliation-type-seeds.ts:30-35`); none is locked
and a tenant may delete any (`:8-11`). The tenant default for the plain Organization field is
`crm_settings.default_affiliation_type_id` (`W/modules/entities/db/crm-settings.table.ts:46,55,60-62`).

#### 2.4 `crm.affiliation_type_contact_info_defaults` — Implemented

`W/modules/entities/db/affiliation-type-contact-info-defaults.table.ts:58-131`: per type, `kind` (`address`|`phone`,
`:50`), source and destination type ids as **two pairs of nullable columns each with its own composite FK** (`:77-101`,
`ON DELETE RESTRICT`), the `_kind_columns_check` requiring exactly the pair of the row's kind (`:120-128`), one mapping
per destination type (`_destination_address_uq`, `_destination_phone_uq`, `:103-104`), max 20 per type (`:54`). Applied
when an affiliation is created and on explicit "re-apply" (README §0b.2 item 6,
`W/docs/affiliations/README.md:159-166`); existing rows never overwritten.

#### 2.5 How contact addresses/phones link to organization addresses — Implemented (incl. `disconnected`, newer than ARCH)

`W/modules/entities/db/contact-addresses.table.ts`:

- `SOURCE_MODES = ['owned','copied','linked','disconnected']` (`:64`); disconnected reasons (`:67-70`); columns
  `source_affiliation_id, source_organization_id, source_organization_address_id, copied_from_organization_id, copied_at, disconnected_from_organization_id, disconnected_at, disconnected_reason`
  (`:121-131`).
- FKs:
  `contact_addresses_source_affiliation_fk (tenant_id, source_affiliation_id, contact_id, source_organization_id) → affiliations (tenant_id, id, contact_id, organization_id)`
  (`:172-180`) and
  `contact_addresses_source_organization_address_fk (tenant_id, source_organization_address_id, source_organization_id) → organization_addresses (tenant_id, id, organization_id)`
  (`:182-185`), both `DEFERRABLE INITIALLY IMMEDIATE` (migration 0066).
- `contact_addresses_source_check` (`:195-211`) — a four-way CASE: linked rows carry exactly the three source ids;
  copied rows exactly the provenance pair; disconnected rows exactly the three disconnected columns; owned rows none.
- `contact_addresses_linked_values_check` (`:216-221`): a linked row holds **no value of its own**; the value is
  resolved on read through `crm.contact_addresses_effective`
  (`W/modules/entities/db/contact-information-effective.view.ts`, migration 0070).
- Phones mirror it exactly (`W/modules/entities/db/contact-phone-numbers.table.ts:41-126`, `phone_number` NULL iff
  linked `:120-123`).
- Removing an organization address turns linked rows into `disconnected` (D-25; `W/docs/disconnected-information.md`,
  newer than ARCH 04 §3.1 which said "being built").

**What is organization-specific in all of this.** (a) `organization_id` is a composite FK to `crm.organizations` and
immutable (`0066:123-134`); (b) the exclusion key names `organization_id`; (c) the linked-information FKs name
`source_organization_id` and `organization_addresses`/`organization_phone_numbers` rows, so "a household supplies the
member's Home address" would need either a household that _is_ an organization row with organization addresses, or a
third source kind; (d) the type's contact-information defaults map **organization** address/phone types
(`applies_to: 'organizations'`) to contact types; (e) the automatic role rule runs on "joins an organization" and counts
"other contacts with an affiliation to that organization"
(`W/modules/entities/services/contacts/role-assignment/contact-role-assignment.ts:3-9`); (f) the compliance
recalculation handler reacts to affiliation changes per organization type (`W/docs/change-events.md:240`); (g) Quick
Search, the primary-organization pick and the per-type configured relationship all target `organizationsObject`. Nothing
in the table forbids an organization whose type is "Household"; everything that reads the row assumes an organization.

#### 2.6 The aggregate save path — Implemented

- `W/lib/db/owned-collections.ts`: the generic owned-collection replace (`replaceOwnedElements` `:120`,
  `upsertOwnedRows` `:257`, `loadOwnedElements` `:99`); semantics in the header `:7-30` (element with id = update in
  place; without = create; missing = removed; client UUIDv7 ids allowed where `client_ids`; locks honoured; deferrable
  one-per-slot uniques; `vacates` ordering for partial unique indexes). The `OwnedCollection` descriptor interface is
  `:47-90`.
- The contact aggregate declares four owned collections in `W/modules/entities/services/persistence/contacts.ts`
  (`email_addresses :213`, `phone_numbers :227`, `addresses :246`, `communication_preferences :264`), and affiliations
  are the fifth, split into `W/modules/entities/services/persistence/contact-affiliations.ts`
  (`prepareAffiliationsWrite` / `writeAffiliations`, header `:1-50`): base list (D-09: omitted = unchanged — **Decided
  Andrew 2026-10-19, built, newer than ARCH 04 §4 I-2 "not built"**), `set_primary_organization` command, primary rules,
  lifecycle (ending an affiliation ends its open roles), derived job title and automatic roles, refusals (ownership,
  dates, D4/D6 overlaps, limits). Written only through the contact: "no route, service or job writes an affiliation on
  its own" (`:4-7`; `W/docs/entities-api.md:450-453`).
- **Automatic role assignment** (`contact-role-assignment.ts:1-45`): on the affiliation that becomes primary when the
  contact joins an organization, every active `assign_automatically` role and, when no other contact has an effective
  affiliation to that organization, every `assign_automatically_to_first_contact` role
  (`W/modules/entities/db/contact-roles.table.ts:48-53`); future-dated affiliations are marked due and reconciled every
  five minutes (D-46, **Decided 2026-10-19**, built: `sys_automatic_roles_due`,
  `W/docs/affiliations/README.md:418-446`). Released statuses get none (M1-D1).

---

### 3. Contact information

- **Address types / phone number types** share one shape (`W/modules/entities/db/collection-types.ts:37-46`): `id`
  (configuration id), `name` (unique per tenant `:60`), `applies_to IN ('contacts','organizations','both')` (`:30`,
  `:42`), `display_order`. `applies_to` is guidance for lists (`:7-11`, Andrew 2026-10-11) **except** that since **M1-D8
  (Andrew 2026-10-19)** an organizations-only type on a contact is refused naming the field
  (`W/modules/entities/services/persistence/contacts.ts:704-708`). Seeds: addresses `main, billing` (organizations),
  `work, home, school, seasonal` (contacts)
  (`W/modules/tenant-lifecycle/services/participants/collection-type-seeds.ts:35-42`).
- **Preferred flags**: contacts have one preferred address (`contact_addresses_one_preferred_uidx`,
  `contact-addresses.table.ts:139-141`) and one preferred phone (`contact-phone-numbers.table.ts:65-67`); a seasonal
  address can never be preferred (`:166-168`).
- **Seasonal addresses** (newer than ARCH): `seasonal_start`/`seasonal_end` are recurring `MM-DD` (E-06, Andrew
  2026-10-19; `MONTH_DAY_PATTERN :90`, checks `:153-160`), plus `seasonal_preferred` (`:118`, check `:162-164`);
  migration `20261019210543_seasonal_addresses_month_day.sql`.
- **E-mail**: three ordered slots, position 1 primary (`contact-email-addresses.table.ts:36,46,52`); primary uniqueness
  is now a constraint trigger that ignores released-status holders (`:11-23`, migration `20261019223631`), under the old
  index name `PRIMARY_EMAIL_UNIQUE_INDEX` (`:39`). No e-mail type (CRM-Q6 open, ARCH 13 §3.5). Organizations have no
  e-mail — Absent.
- **Communication preferences** (`contact-communication-preferences.table.ts:21-41`): `communication_type_id` is **free
  text with no FK** (`:25`), `enabled`, `subscription_email_address`,
  `subscription_status IN (subscribed, digest, unsubscribed)` (`:19`), `subscription_last_updated`; one row per type per
  contact (`:34`). Gap: no communication-types configuration table (`W/docs/debt/communication-type-check.md`; ARCH 04
  §3.2 "Missing"). Note the memory rule "CRM owns consent ledger" (Andrew 2026-10-20) — any household-level mailing
  preference would sit beside this table, which today is per contact only.

---

### 4. Contact-to-contact relationships

- **Only `spouse_id` exists.** No `relationship_types` or `contact_relationships` table, no route, no module (grep of
  `relationship_type|relationshipType|household` across `W/` finds only the duplicate-detection fixtures and benchmark
  data; the prototype's `rolodex.relationship_types` was dropped in `W/db/migrations/0000_retire_prototype.sql:35`).
  ARCH e03 §1 records `rolodex-relationships` / `rolodex-relationship-types` as unported;
  `W/docs/affiliations/README.md:233` ("Rhythm 1.0 generic relationships: not part of this model"). — **Absent.**
- **Spouse is written reciprocally — Implemented, newer than ARCH 14 row CO-05.** `keepSpouseReciprocal`
  (`W/modules/entities/services/persistence/contacts.ts:1743-1810`, called at `:2054`): linking A→B sets B→A in the same
  transaction (B's version bumped, its own revision written); clearing or relinking clears the old partner; linking to a
  contact already recorded as a third contact's spouse is refused 400 (`:1802-1804`); a partner locked against updates
  refuses 409 (`:1781`); a save that leaves the spouse unchanged writes nothing to the other side. **Decided: D-19,
  Andrew 2026-10-19** (`W/docs/milestones/CRM-M6-access-and-audit.md:32`; DEF-CRM-0601), recorded as _interim_ in
  `W/docs/debt/spouse-relationship-model.md:1-27`. Tested by
  `W/modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js`.
- **Delete** clears `spouse_id` on every contact naming the deleted one, stamped as the system actor
  (`contacts.ts:1725-1737`).
- **Merge**: `contacts.spouse_id` is policy `reassign` (`W/modules/merges/references/crm-references.ts:228-233`); a
  participant naming another participant is cleared with a warning, two different spouses is a scalar conflict
  (`W/docs/merging/relationship-coverage.md:29`; `W/docs/merging/architecture.md:588-589`). The debt note (`:19-21`)
  admits a merge can leave a third contact naming the survivor alone.
- **Duplicate scoring treats a spouse pair as negative evidence** (`['spouse', -30]`,
  `W/modules/duplicates/services/matching/evidence.ts:78`; `scoring.ts:333-338` reads either record's `spouse_id`).
- **Imports** carry `spouse_source_key` (`W/modules/data-jobs/services/operations/contact-import.schema.ts:34,60`).
- **The target the debt note states** (`spouse-relationship-model.md:29-36`): a tenant-owned `crm.contact_relationships`
  table (two composite keys to contacts, cascade with either), typed by `crm.relationship_types` with a label per
  direction, optionally dated; households "as a relationship type or a grouping record of their own"; `spouse_id`
  becomes a derived read-only projection kept on the wire, backfilled, then retired. ARCH 05 §5 extends this to
  organization↔organization (non-hierarchy) and recommends households as an organization of system type "Household" with
  "Household member" affiliations; ARCH 13 §3.2 **CRM-Q2 is open** (options 1 recommended / 2 / 3), blocking CRM-M21
  (ARCH 14 rows RL-01, RL-02; roadmap milestone M21). — **Planned, undecided.**
- **A relationship primitive that already exists outside CRM — Implemented in Platform, newer than ARCH e05 §15
  ("nothing built").** Platform's **associations** module is on services-platform master (`P/modules/associations/`
  incl. `aws/api/associations.routes.ts`, last commit `367348afd` 2026-10-19 22:52 ET; contract
  `P/docs/associations.md`). `platform.association_types` relate **any two registered objects, the same object twice
  allowed ("Contact mentors Contact")**, with `forward_label`/`reverse_label`, cardinality
  `one_to_one|one_to_many|many_to_many`, `dated`, `allow_primary`, and per-side delete rules
  (`P/docs/associations.md:22-44`); `platform.associations` rows carry `from/to_object_id`, `from/to_record_id`
  (verified through the endpoint seam, no FK), `is_primary`, inclusive `start_date/end_date`, `position`, `notes`,
  cardinality keys and partial unique indexes (`:44-80`); rules `:81-115` (end rather than delete for dated types; 1,000
  per record per type). CRM's delete already calls `platform.apply_endpoint_deletion` inside its own transaction when
  `CRM_OWNER_DELETION_RULES` is on (`W/docs/entities-api.md:1552`; contract `P/docs/associations.md:193-237`), and
  Platform's **merge participant reassigns association endpoints** on contact and organization merges (`:239-260`). The
  custom-objects architecture explicitly keeps affiliations as the contact↔organization model and makes associations
  "the model for every relationship a tenant defines, including contact-to-contact"
  (`P/docs/proposals/custom-objects/03-recommended-architecture.md:343-356`, approved 2026-10-18 D-04/D-09/D-12).
  **Nothing in CRM reads or exposes associations today** (no card, no query field, no CRM route) and ARCH 05 §5 / CRM-Q2
  do not mention them — this is the largest fact ARCH does not account for.

---

### 5. Types, hierarchy, "system" records

- **Contact types** (`W/modules/entities/db/contact-types.table.ts:30-82`): configuration id, `name`, `description`,
  `is_active`, nullable `display_order` (`:44`), five page-layout/form slots (`layoutAssignmentColumns()` `:51`, proved
  against `platform.layout_assignments` by trigger). No "system" flag — Absent.
- **Organization types** (`W/modules/entities/db/organization-types.table.ts:52-116`): configuration id, `name`,
  `is_active`, `display_order`, `restrict_contact_roles` + `applicable_contact_role_ids[]` (`:66-67`, check `:98-101`),
  compliance switches (`:69-75`) and the child table `organization_type_compliance_rules` (`:118-147`), layout slots
  (`:81`); the compliance notification template is a composed property stored by Platform
  (`W/docs/composed-properties.md`). No "system" flag — Absent.
- **Hierarchy — Implemented**: `parent_organization_id` FK + not-self check (`organizations.table.ts:99,115`); the
  descendant loop refusal is the trigger `organizations_parent_not_descendant` with a per-tenant advisory lock
  (`W/db/migrations/0018_organization_parent_not_descendant.sql`, whole file; refusal mapped at
  `W/modules/entities/services/persistence/organizations.ts:414-416`, lock at `:529-546`). Ancestors by recursive CTE
  (`organizations.ts:743-762`), descendants (`:789-815`). Delete of an organization with children or affiliated contacts
  is refused (`:713-724`). **Hierarchy is not in the query layer** (ARCH e03 §4, §20 item 12) — Absent.
- **"System"/well-known types mechanism**: the only worked example is activity types: `system_code`
  (`W/modules/activities/db/activity-types.table.ts:11-17,115`), one per code per tenant (`:137-140`), immutable by
  trigger, and the persistence service refuses deleting one (`IllegalOperation`,
  `W/modules/activities/services/persistence/activity-types.ts:13-21`). Seeds are versioned seed sets applied once per
  tenant through the tenant-lifecycle ledger (`affiliation-type-seeds.ts:8-15`, `contact-role-seeds.ts:1-9`) and are
  **deliberately not locked**. `crm.protected_records` (D-08, `W/modules/entities/db/protected-records.table.ts:17-27`;
  migration `20261019194906`) exists for "protected system records" the product relies on, read-only to the application
  role (`:66-68` of the migration) — **but nothing inserts into it yet** (grep finds no INSERT in seeds or migrations) —
  table Implemented, seeding Absent. "Well-known objects" (`W/db/seeds/well-known/well-known-objects.ts:1-20`) are
  dev/test fixture rows, not a system-type mechanism.

---

### 6. Custom fields registration

- CRM declares the capability on the object itself:
  `defineObject(contacts, { id: 'crm.contacts', capabilities: { customFields: true, configurableForms: true, configurablePageLayouts: true }, … })`
  (`W/modules/entities/metadata/contacts.object.ts:25-27`); the same on `crm.organizations`
  (`organizations.object.ts:22-23`). All objects are gathered into one manifest by `crmRegistrationManifest()`
  (`W/aws/crm.metadata.ts:33-58`) and sent to `platform_metadata_registration` at deploy.
- Platform refuses `customFields` on any object not in the code list `APPROVED_CUSTOM_FIELD_OBJECTS`
  (`P/modules/metadata/capabilities/approved-custom-field-objects.ts`: `crm.contacts`, `crm.organizations`,
  `committees.committees`, `committees.nominations`, `membership.memberships`, `membership.organizational_units`,
  `membership.applications`; `P/docs/metadata.md:181-200`; standard **R2-ARCH-001 v1.0, Andrew 2026-10-19 ~18:24 ET**,
  `P/docs/standards/custom-field-capability.md:1-12`). Memory: "Custom fields opt-in per entity: only CRM Contacts/Orgs,
  Committees, Membership Memberships/Org units; others purpose-built screens" (Andrew 2026-10-19).
- **To register a new entity** (`crm.households`) with custom fields: (1) Andrew adds it to the standard's section-2
  table and the code list; (2) CRM creates `household_custom_field_values` with `customFieldValuesTable` (as
  `contact-custom-field-values.table.ts:17-21`); (3) the object declares `capabilities.customFields: true` (requires the
  other two); (4) the dataset declares `customFieldValues` (`contacts.query.ts:198`) and the query grants follow
  automatically (`W/aws/crm.query-grants.ts:60-76`); (5) the capability owes the obligations of the standard §4
  (`custom-field-capability.md:127`). **Without the list change Platform answers 400.** A Household that is an
  _organization of a type_ inherits organization custom fields with no registration at all.

---

### 7. Duplicate detection and merge

ARCH 07 is current for the engine; details a Household module must honour:

- **Merge object types are closed**: `MERGE_OBJECT_TYPES = ['contacts','organizations']`
  (`W/modules/merges/db/merge-object-types.ts:11`, "a third one … is added here first"); aliases, plans and merges are
  keyed by it (`merged-record-aliases.table.ts:44,61,75`).
- **CRM's typed reference registry** `W/modules/merges/references/crm-references.ts` (916 lines) holds one entry per
  `(table, column)` with `policy`, `label`, `singular_label` (`:45-60` require labels). Relevant entries:
  `contact_organization_affiliations.contact_id` `owned` (`:148-153`), `.organization_id` reassign-dedupe with
  combination (`:444-449`; rule in `W/modules/merges/services/planning/affiliation-combination.ts:1-20`: same
  organization + type + overlapping/touching periods combine), `contacts.spouse_id` `reassign` (`:228-233`),
  `organizations.parent_organization_id` `reassign` (`:458-463`), `tasks.*` and `activity_links.*` `reassign`
  (`:188-223`), custom-field `reference_value` `reassign` by row (`:238-253`). **Coverage guard**:
  `tests/integration/db/merge-coverage.integration.spec.ts` fails on any column named `*contact_id`, `*organization_id`,
  `parent_organization_id`, `spouse_id`, `record_id`+type, or FK to contacts/organizations without a registry entry
  (`W/docs/merging/architecture.md:392-420`); second-order references (`source_affiliation_id` etc.) are classified in
  `W/modules/merges/references/second-order-coverage.ts`.
- **The participant contract other apps implement** (`architecture.md:355-390`; `W/docs/cross-service-functions.md`):
  two `SECURITY DEFINER` SQL functions in the participant's own schema,
  `merge_plan_<object>(survivor_id text, merged_ids text[]) returns jsonb` and
  `merge_apply_<object>(merge_id text, survivor_id text, merged_ids text[], decisions jsonb) returns jsonb`, `EXECUTE`
  granted to `rhythm_crm_app`; discovered from `pg_proc` + `has_function_privilege`
  (`W/modules/merges/services/persistence/merge-participants.ts:7-9,119-130`), half pairs or wrong signatures are
  blockers (`:173-198`); `REQUIRED_PARTICIPANT_SCHEMAS = { contacts: ['platform'], organizations: [] }` (`:91-94`). The
  shared package is `@rhythmsoftware/macroservice-record-merge` (policies, `defineMergeReferences`, coverage guard).
- **Aliases** `crm.merged_record_aliases` (`W/modules/merges/db/merged-record-aliases.table.ts:39-79`): PK
  `(tenant, object_type, merged_id)`, survivor, merge id, the merged record's number/origin/external id, chain
  compression; reuse guard triggers `contacts_not_merged_id_trigger` / `organizations_not_merged_id_trigger` (`:19-21`);
  410 on every route (D3).
- **Linked contact information in an organization merge** is re-pointed by slot decision (`relink`,
  `relink-organization`, `copy-on-merge`; `architecture.md:573`, `relationship-coverage.md:45`).
- **Duplicate rules/scoring** per ARCH 07 §2; a household address shared by two people scores low, not high
  (`W/modules/duplicates/tests/unit/services/matching/contact-scoring.spec.js:355`; `duplicate-rule-seeds.ts:11`).
- **What a new Household entity must implement to be mergeable / to participate in contact merges:**
  1. If a household is **its own record kind**: add `'households'` to `MERGE_OBJECT_TYPES`, a household merge
     model/planner (the organization pair `organization-merge-model.ts` / `organization-merge-planner.ts` is the
     template), an alias row kind, a reuse guard trigger, the 410 route behaviour, a plan/preview vocabulary, and
     registry entries for every column naming a household (the coverage guard will not discover `household_id` by name —
     the naming patterns in `architecture.md:400-405` do not include it — so the guard's patterns must be extended or
     the column must FK to the household table, which the guard reads from `pg_constraint` only for
     contacts/organizations today).
  2. For **contact merges**, every column holding a contact id in the household tables (`household_members.contact_id`,
     "head of household"/"primary member", "mailing addressee") must get a registry entry with a policy (`owned` /
     `reassign` / `reassign-dedupe` with a collision rule such as "two memberships of one household collapse") and be
     handled by the apply path; the plan hash must bind the moved rows (D-31).
  3. If a household is **an organization of type Household** with **Household-member affiliations**: nothing new —
     organization merges already move every affiliation and combine same-type stays (`relationship-coverage.md:45`),
     aliases/410 exist, and the contact merge already unions affiliations. The only new work is a merge-plan _warning_
     when two members' households differ (today that is just two affiliations kept side by side).
  4. If relationships use **Platform associations**: Platform's participant already reassigns and de-collides them
     (`P/docs/associations.md:239-260`).

---

### 8. CRM 360 data and record views

- **Contact 360 is metadata-driven** from `react-console/lib/record-views/views/contact-360.json` (the folder holds only
  `contact-360.json`, `committee-360.json`, `order-360.json` — names only); Organization 360 is bespoke (ARCH 09 §5; e2e
  `e2e/dev/organization-360.dev.spec.ts`). CRM has **no summary/overview route**: the 360 header and cards read
  `GET /contacts/{t}/{id}` (full record with resolved collections, `primary_organization_id/_name`, `effective` per
  affiliation; `W/docs/entities-api.md:196-232,434-470`),
  `GET /organizations/{t}/{id}/affiliations?effective=current|past|future|all&as_of=` (`:585-600`, limit 1–100, cursor),
  the history routes (`/history`, `/history/actors`, `/history/{revisionId}`, `:1167-1218`), and the activities module's
  `/activity_timelines/{t}/contacts/{id}[/summary]` (ARCH e03 §10). Layout/forms per type are the five slots on the type
  record and the tenant defaults `/crm_settings/{t}` (`:995`) with `/effective_definitions/{t}` (`:1045`).
- **Entities API doc** `W/docs/entities-api.md` (1,650 lines) is the contract; its route table is `:1-60`.
- **Composed properties** (`W/docs/composed-properties.md`, `composed-property.ts:1-40`): the pattern for a record
  property stored by another macroservice (today: the compliance template in Platform communications). A household
  "mailing name" stored elsewhere is not a candidate; this pattern is for Platform-owned content keyed by the CRM
  record.
- Cards from other applications are contributed through Platform layout metadata (ARCH 09 §1, §5); today only Accounting
  does.

---

### 9. Semantic queries and Explorer

- Registration: `crmQueryManifest()` (`W/aws/crm.queries.ts:29-51`) lists datasets `crm.contacts`,
  `crm.contact_addresses`, `crm.contact_phone_numbers`, `crm.contact_organization_affiliations`, `crm.organizations`,
  the three type datasets, tasks, activities; sent to `platform_queries_registration` after the metadata manifest.
  Grants to `rhythm_query_reader` are derived from the two manifests (`W/aws/crm.query-grants.ts:1-28,47-121`); CRM's
  migrations must grant exactly those columns (unit test `tests/unit/query-grants.spec.ts`).
- **How organizations and affiliations appear in contact queries today**
  (`W/modules/entities/queries/contacts.query.ts:186-300`):
  - `primary_organization` is a **pick** over affiliations `WHERE is_primary` (`:220-226`), proven single-row by D1,
    exposing `primary_organization.organization_id.name` etc.;
  - `configuredRelationships.affiliation_types` generates one "Organization" reference field per tenant affiliation type
    from the view `contact_organization_affiliations_current` (`:230-248`; §7.2 policy order primary → latest start →
    id); `address_types` and `phone_number_types` likewise (`:250-277`);
  - `collections.affiliations` / `current_affiliations` (with nested `roles`) and `addresses` back `exists` criteria
    (`:279-300`), never joined; the Affiliations dataset itself reads `contact_organization_affiliations_for_queries`
    with a computed `effective` column
    (`W/modules/entities/db/contact-organization-affiliations-for-queries.view.ts:1-22`);
  - `discovery: { field: 'status', discoverable: ['active'], archivedField: 'archived_at' }` (`:196`) drives default
    criteria, pickers and Quick Search; `customFieldValues` (`:198`); `source: contactsForQueries` adds
    `last_activity_at`/`activity_count` (`W/modules/entities/db/contacts-for-queries.view.ts:1-20`).
  - Organizations get `contact_count` and `child_organization_count` as computed view columns (`:474-481`).
- **Platform capability status** (`P/docs/queries/semantic-query-layer.md`): configured relationships from another
  relation [Built 2026-10-13], collections/`exists` [Built 2026-10-13], tenant-wide generated families [Built
  2026-10-18] (`:822-823, 899-1060`). **Aggregation**: the structured request carries `grouping` (`groupBy` +
  count/sum/avg…) (`:60-63`), but §7.6 says "the first release is record-level only; aggregation (Phase 4) is a separate
  request shape `POST …/aggregate` using only declared measures" (`:1829-1834`); measures declared per §6.12
  (`:891-897`) — **declared, not served**; the only counts CRM offers are precomputed view columns. A "members in
  household" count would follow the `contact_count` view-column pattern.
- Eligibility condition types CRM registers (`W/aws/crm.eligibility.ts:1-27`) compile criteria over the same datasets
  (status, type, address picks, age, affiliations in force `:56-71`); a household-membership condition would be a new
  `sql` type over a dataset.
- Recipes: `W/aws/crm.recipes.ts` loads `modules/<module>/queries/recipes/*.recipe.json`.

---

### 10. Search and saved views

- **Quick Search**: CRM owns no index; the remote change-event handler `quick-search-indexer` on the `contact` and
  `organization` streams hands every event to `platform_quick_search_indexer`
  (`W/modules/entities/services/quick-search/quick-search-indexer.ts:1-30`; `W/docs/change-events.md:244`). What is
  indexed is declared on the dataset:
  `quickSearch: { display: { title, subtitle, status, number, image }, defaultFields: [{ field, tier }], neverIndexed: {…} }`
  (`contacts.query.ts:333-356`), validated by `W/tests/unit/quick-search-declaration.spec.ts`. Platform contract
  `P/docs/quick-search.md` (discovery rule read from the dataset `:90-92`; field ids as the owner declares them
  `:104-107`). A new record type needs: its own change-event stream with the indexer handler, a `quickSearch` block, and
  Platform accepting the type (DEF-CRM-0119 freshness defect per ARCH 09 §2 still open).
- **Saved views / lists**: Platform's explorer and saved queries (ARCH 09 §3–4); CRM contributes datasets, recipes and
  the Explorer definitions in the console. CRM's own list routes are cursor-paged (`limit` 1–100 default 25, opaque
  `next_cursor`, `W/docs/entities-api.md:1555-1556`) with `organization_id` and `search` filters on contacts (`:12`).

---

### 11. Audit and change tracking

- **Record revisions** `crm.record_revisions` (`W/modules/entities/db/record-revisions.table.ts:110-165`): written only
  by the commit-time settle (`crm.record_revisions_write`, SECURITY DEFINER), application role SELECT-only;
  `record_type` is the dataset id (`:118`: "a second entity adds rows, not a table"); fields `changes` jsonb,
  `changed_fields[]`, `transaction_id xid8`, `sys_actor_id/_label`,
  `sys_source IN (console, portal, public, integration, job, maintenance)` (`:74-81`), `sys_job_id/_batch_id`. History
  is kept for **contacts, activities and (newer than ARCH e03 §14) organizations**:
  `W/db/change-event-installs.ts:112-133` declares the contact stream with `history: CONTACT_HISTORY` and child tables
  addresses, e-mails, phones, custom-field values and affiliations (roles fold into the contact's event);
  `ORGANIZATION_HISTORY` added in CRM-M1 (`W/docs/record-history.md:265-285`, migration
  `20261020130548_organization_history`). Adding history for a new entity is the five-step recipe at
  `record-history.md:267-283`.
- **Change events**: per-entity `<entity>_change_events` tables, settle trigger, dispatcher, handlers; handlers
  registered in `W/aws/crm.change-events.ts` (table of six at `W/docs/change-events.md:234-245`); adding a stream is
  `:332-346`.
- **Business events** (`W/docs/business-events.md:12-45`, `W/aws/crm.events.ts:26-45`): only `crm.contact.merged` is
  published; `crm.contact.created/updated/deleted`, `crm.organization.*` are in the Integration Platform catalogue but
  **unpublished** (ARCH e03 §14). Manifest registration `:111-118`.
- **Record stamps**: `snakeSystemColumns()`; a no-op save changes no version, stamp or revision
  (`W/lib/db/no-op-saves.ts`, `entities-api.md:1455-1459`); 409 `StaleObject` carries `details.latest_change` (M1-D6,
  `:1538-1549`).
- Record locks: `sys_locked` per row, lock audit `crm.record_lock_changes`, protected records (D-08;
  `W/docs/record-locks.md`).

---

### 12. Lifecycle

- **Archive — Implemented (newer than ARCH 08 §3 "building now")**: `POST …/archive` / `…/unarchive` for contacts and
  organizations, permissions `crm.contacts.archive` / `crm.organizations.archive` at tier admin
  (`W/aws/crm.service.ts:116-126,174,212`); writes refused 409 `RecordArchived` while archived
  (`entities-api.md:198-201`); discovery = status discoverable AND not archived (`contacts.query.ts:174-176,196`);
  affiliations keep an archived organization's identity and answer `organization_archived_at` / `contact_archived_at`
  (D-07, `W/docs/archive.md:88-96`).
- **Safe delete — Planned, deferred to CRM-M20** (ARCH 08; Platform design now on services-platform master at
  `P/docs/proposals/record-lifecycle/01…08`). **Decided Andrew 2026-10-19 ~16:50 ET**: today's hard delete stays until
  then.
- **Cascade / SET NULL on contact deletion today**: every contact-owned table cascades (`contacts.table.ts:205-217`:
  e-mails, phones, addresses, communication preferences, affiliations, custom-field values); roles cascade with their
  affiliation (`contact-organization-roles.table.ts:57-65`); `activity_links` cascade (ARCH e03 §10, to change under
  D-44); `tasks.contact_id` SET NULL (ARCH 04 §3.1); `spouse_id` on other contacts is cleared by code, not by the FK
  (`contacts.ts:1725-1737`; the FK has no action); revisions, aliases, lock changes, duplicate overrides, data-job items
  are `preserve-history` (no FK). Platform associations naming the contact are applied by
  `platform.apply_endpoint_deletion` inside the delete when the switch is on (`entities-api.md:1552`; on in dev, off
  elsewhere). An organization with children or affiliated contacts cannot be deleted (`organizations.ts:713-724`).

---

### 13. API conventions and adding a module

- Routes are `/<resource>/{tenantId}[/{id}]` in snake_case, e.g. `/contacts/{tenantId}/{id}`,
  `/organizations/{tenantId}/{id}/affiliations` (`W/docs/entities-api.md:1-60`); tenant from path + execution context,
  never the body.
- Validation: zod schemas per route (`modules/entities/aws/api/*.schemas.ts`), string trimming standard
  (`@rhythmsoftware/macroservice-records` 0.3.0, `entities-api.md:1507-1515`), refusals as `ObjectInvalid` naming the
  path.
- **Write contract**: `PUT` = whole record with `sys_version` (400 missing, 409 `StaleObject`); `PATCH` = JSON Patch led
  by `test /sys_version`; `POST` creates with client UUIDv7 and replay digest; D-05/E-09 (`entities-api.md:1435-1500`,
  **Decided Andrew 2026-10-19**); owned collection omitted = emptied **except** `affiliations`/`roles` (D-09) and the
  four layout ids (`:1527-1535`). Every success is 200 (no 201/202, `docs/debt/created-status-code.md`).
- Paging: `limit` 1–100 (default 25), opaque `cursor`/`next_cursor` (`:12,592-597,1555`); limits per collection in ARCH
  04 §5.
- **Adding a module**: CRM does **not** depend on `@rhythmsoftware/macroservice-architecture` and its `add-module`
  command (grep of `AGENTS.md`, `README.md`, `docs/stack-layout.md` finds no `add-module`; ARCH e03 §2 "CRM does not
  depend on macroservice-architecture"). The layout is the stack table `W/docs/stack-layout.md:15-24` (`crm-ci`,
  `crm-resources`, `crm-logic`, per-module `crm-<module>-resources`, `crm-<module>-logic`, `r2-crm-<module>-api`) and
  the module table `:69-80` (entities, configuration-types, merges, regions, tasks, duplicates, activities, data-jobs;
  tenant-lifecycle in core). A module is
  `modules/<name>/{aws/<name>.module.ts, aws/api, aws/lambda, db, metadata, queries, services/persistence, tests}` and
  is wired by hand into `aws/crm.service.ts` (permissions), `crm.metadata.ts`, `crm.queries.ts`, `crm.change-events.ts`,
  `crm.export-datasets.ts`, the tenant-lifecycle participant registry and the CDK stacks. **Rule (2026-10-17,
  `stack-layout.md:82-88`): nothing new lands in `crm-entities-logic`; a new module's routes go under its own path
  (`/households/{tenantId}`), never under `/contacts` or `/organizations`.**

---

### 14. Tests (names)

- Affiliations:
  `W/modules/entities/tests/integration/services/persistence/contact-affiliations{,.queries,.business-dates}.integration.spec.js`,
  `…/tests/integration/db/contact-organization-affiliations.table.integration.spec.js`,
  `…-for-queries.view.integration.spec.js`, `…/affiliation-type-contact-info-defaults.table.integration.spec.js`,
  `…/tests/unit/services/persistence/contact-affiliations.spec.js`, `…/tests/e2e/contact-affiliations.e2e.spec.js`,
  `…/affiliation-types.e2e.spec.js`, `…/tests/benchmark/affiliations-query-layer.benchmark.spec.js`,
  `W/tests/integration/db/affiliations-conversion-migration.integration.spec.ts`,
  `seed-affiliation-adoption.integration.spec.ts`.
- Spouse: `W/modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js`.
- Merge: 50+ specs under `W/modules/merges/tests/` (e.g. `integration/services/merges/merge-execution`,
  `organization-merge-execution`, `linked-contact-information`, `required-participants`, `toy-participant`,
  `overlapping-merges`, `approval-binding`, `activity-links-merge`; e2e `merges`, `merged-records`,
  `merged-from-history`, `business-events`), plus `W/tests/integration/db/merge-coverage.integration.spec.ts`.
- Deployed-dev:
  `W/tests/e2e/dev/{contacts,organizations,contact-information-sources,contact-history,contact-role-compliance, duplicates,merges,custom-fields,data-jobs,regions,contact-photo,cors-preflight}.dev.spec.ts`.
- react-console Playwright suites referencing Contact 360 (names only; `acceptance/crm/playwright/journeys/`):
  `contacts/CRM-ACC-0001, 0018, 0025, 0045, 0050, 0055, 0061`; `affiliations/CRM-ACC-0170, 0191, 0212, 0213`;
  `auditing/CRM-ACC-0605, 0606, 0611–0615, 0624`; `configuration/CRM-ACC-0347, 0348, 0361`;
  `cross-application/CRM-ACC-0690, 0691, 0694, 0698, 0705, 0717, 0719, 0723–0725, 0729`;
  `data-operations/CRM-ACC-0640, 0648, 0669`; `explorer/CRM-ACC-0390, 0409, 0410, 0414`; `merging/CRM-ACC-0270, 0286`;
  `organizations/CRM-ACC-0119, 0148`; `security/CRM-ACC-0547, 0548, 0550`; `adversarial/CRM-ACC-0761, 0771`;
  `walkthrough/CRM-WT-0001`; and `e2e/{controlled,dev}/record-views.*.spec.ts`, `e2e/dev/contact-edit.dev.spec.ts`,
  `e2e/dev/organization-360.dev.spec.ts`, `e2e/mock/affiliations.mock.spec.ts`.

---

### 15. What a Household module could reuse unchanged / reuse with change / must add

**Reuse unchanged (if a household is an organization of a tenant type "Household" and members are affiliations of a type
"Household member", the ARCH 05 §5 option 1):**

- The whole affiliation aggregate: dated membership, one primary per contact, overlap exclusion, roles on the membership
  (e.g. "Head of household", "Billing contact" as contact roles), job title repurposable or ignored, D-09 explicit
  mutations, D-46 future-dated joins, history in `record_revisions`, change events and compliance handlers (§2).
- Organization record: number, status, archive/unarchive, custom fields (already approved), addresses with preferred
  flag, phones, hierarchy (a household under a chapter), layouts/forms per type, Organization 360 cards, history (new),
  Quick Search, the organizations dataset with `contact_count`, eligibility types, exports, tenant lifecycle.
- Linked contact information: a member's Home address can be `linked` to the household's Main address through the
  membership affiliation; the type's contact-information defaults can make that automatic for new members;
  `disconnected` handles a removed household address (§2.5).
- Merge: organization merges already move and combine member affiliations, re-point linked addresses, alias and 410;
  contact merges already union affiliations (§7).
- Deletion today (refused while any affiliation names it; archive is the path) and the future deletion-impact contract.

**Reuse with change:**

- A _system_ organization type "Household" per tenant: there is no system-type flag on `organization_types`; either
  reuse the activity-type pattern (`system_code` + immutable trigger + delete refusal, §5) or seed a protected row in
  `crm.protected_records` (table exists, no writer yet). Same for the "Household member" affiliation type (seed set
  versioning exists; locking does not).
- `applies_to` on address/phone types is contacts/organizations/both: a household wants organization-style types
  ("Main") offered for a household but probably not "Billing"; either accept `both` or add a per-type-of-organization
  filter (small).
- Automatic role assignment keys on "joins an organization"; the "first contact" rule gives the first member e.g. Head
  of household — fine, but the rule is primary-affiliation-only (ARCH e03 §7), so a member whose _employer_ is primary
  gets no household role automatically: the rule needs an "always on this affiliation type" variant.
- Queries: `contact_count`-style view columns for members; a per-type configured relationship already yields "Household"
  as a generated organization field on contacts (no code); a household→members list is the Affiliations dataset filtered
  by type (exists). "Mailing addressee" / household name composition needs a computed column on the organizations view.
- Quick Search: organizations already indexed; a household's display subtitle (members) needs a declaration change only.
- Console: Organization 360 is bespoke; a Household page needs the metadata-driven treatment ARCH 09 §5 asks for anyway.
- Membership/Commerce: can bill an organization today (bill-to kind organization), so "family membership billed to the
  household" needs no new party kind — ARCH 05 §5's stated reason for option 1.

**Must add (in either model):**

- Person-to-person relationships: no table, type table, route, query field, merge policy or history exists (§4). Choose
  between (a) a CRM `crm.relationship_types` + `crm.contact_relationships` pair per the debt note (new module, new merge
  registry entries, new history stream, new dataset/collection, spouse backfill and retirement), or (b) Platform
  associations, which already give typed, both-way-labelled, dated, cardinality-checked contact↔contact links with merge
  and delete participation — but have no CRM card, no query exposure and no spouse backfill today, and are governed by
  the custom-objects architecture, not CRM.
- Household-specific fields (household name / salutation / addressee / informal name, "primary member", "mailing
  member"): absent on both contacts and organizations; only `salutation` exists on contacts (§1.1).
- A "members of the same household" semantic in duplicate scoring (today only `spouse` is negative evidence, §4) and in
  merge previews (warn when two merged contacts belong to different households).
- Business events for household changes (only `crm.contact.merged` is published, §11).
- If a household is a **new record kind** rather than an organization: a new module (§13), the contact-owned link table
  and its composite FKs, archive columns, history stream, dataset + grants + Quick Search, custom-field value table and
  the R2-ARCH-001 list change (§6), a third `MERGE_OBJECT_TYPES` entry with alias/guard/planner (§7), registry entries
  for every `household_id` column plus coverage-guard pattern extension, tenant-lifecycle purge/reset/export
  participants and datasets (`W/aws/crm.export-datasets.ts:70-108`), eligibility condition types, and recipes — i.e.
  everything option 1 gets for free.

---

### 16. Questions the code could not answer

1. **Which model Andrew wants (CRM-Q2)** — the code supports option 1 cheaply and option (b) in §15 is already built in
   Platform, but neither ARCH 05 §5 nor the debt note weighs Platform associations; a decision is needed on whether
   contact↔contact relationships live in CRM tables or in `platform.associations`.
2. **Whether a "Household" organization may be a contact's _primary_ affiliation.** D1 allows one primary; if a member's
   employer is primary, the household is an additional affiliation and today gets no automatic roles and no
   "Organization" column. The code has no notion of "primary per affiliation type".
3. **Whether the one-primary-e-mail rule and `applies_to` guidance should apply to household records** — no code path
   distinguishes an organization type's purpose.
4. **Whether a household address should be `linked` or `copied` into members** — both exist; the choice changes what
   happens when the household moves (linked follows; copied does not) and when a member leaves (link resolves to nothing
   once the affiliation ends; §0b.2 item 5).
5. **How a family membership is modelled in Membership** — `services-membership/docs` has no "household" or "family"
   text (grep), so whether Membership bills the organization or each contact is undetermined here.
6. **Whether a spouse pair should also be a household**, and whether `spouse_id` is retired or derived (debt note says
   derived, then retired; no decision recorded).
7. **Aggregation**: whether "members per household" and "households with ≥2 members" are needed in Explorer — Platform
   serves no `aggregate` request yet; only precomputed view columns exist.
8. **Who seeds and protects the system types** — `crm.protected_records` has no writer; the convention for
   Rhythm-protected configuration rows is designed (D-08) but not exercised.
9. **The local checkout** `~/rhythm2/services-crm` is 255 commits behind; any proposal written from it would restate
   gaps that are closed (archive, disconnected, reciprocal spouse, D-09, organization history). Whether the primary
   checkout should be fast- forwarded is Andrew's call (not done here: read-only).

## 22. Rhythm 2.0 Membership: what exists today, read for the household-membership proposal

## Rhythm 2.0 Membership: what exists today, read for the household-membership proposal

Written 2026-10-21 from `~/rhythm2/services-membership` master **fcbddf2** (primary checkout, branch master), the
Membership architecture proposal at `~/rhythm2/services-platform/docs/proposals/membership-architecture/`, the
member-pricing proposal, the Platform eligibility docs, and the worktree
`~/rhythm2/worktrees/services-platform/mem-registrations-2` (branch `mem-manifest-mirror`). Read-only: nothing was
changed.

**How to read the labels.** _Implemented_ = the code on services-membership master does it (table, constraint, service,
route). _Decided_ = Andrew decided it (the date is given) but the code does not show it yet, or shows only part.
_Planned_ = the proposal recommends it and nobody has decided or built it. _Absent_ = neither code nor proposal has it.

**The worktree holds nothing newer.** `mem-manifest-mirror` differs from master only in chapters 04 and 07 of the
proposal, and master is the newer side: master carries the 2026-10-20 Submissions Architecture paragraph (applications
are governed outcomes of a process; D17, D7) that the worktree lacks, plus prettier table reflows
(`/Users/andrew/rhythm2/services-platform/docs/proposals/membership-architecture/04-lifecycle.md:111`,
`07-commerce-and-finance.md:131`). Everything below cites master.

**Where the product stands (context for every finding).** The service is built through phases P0, P1, P2/P2b, P3, P6b,
P7b, P7c, P2c, P1-E and the export provider, each with a record under
`/Users/andrew/rhythm2/services-membership/docs/migrations/<name>/README.md`; Andrew accepted the build on 2026-10-18
and the product-acceptance catalog's first cycle (311 journeys) answered "not ready" (196 passed, 82 failed, 30
blocked), after which Membership was **stopped on Andrew's 2026-10-19 15:40 ET order** so the fleet works only for CRM,
Accounting and Integrations (`/Users/andrew/rhythm2/briefs/membership/MORNING-SUMMARY-2026-10-19.md:8-13`,
`/Users/andrew/rhythm2/services-membership/docs/acceptance/README.md:48-66`). Three outbound seams are still stubs that
refuse: Commerce order placement, CRM contact update, Submissions acknowledgement (details in §6, §8). There is no
`docs/milestones/` directory in services-membership and no `MEM-M*` milestone record anywhere under `briefs/` (§12).

---

### 1. Programs, types, packages, terms, dues, prices

**Three-level configuration: Implemented.** Program → type → package, exactly as the proposal's D-04
(`/Users/andrew/rhythm2/services-platform/docs/proposals/membership-architecture/02-domain-model.md:82-103`).

- **Program** (`membership.programs`, configuration-string id): `member_kind` `contact | organization | either`
  (`/Users/andrew/rhythm2/services-membership/modules/configuration/db/programs.table.ts:34`), `unit_binding`
  `none | affiliation | identity` (`:35`), `exclusivity_group` (`:37`), `grace_days` (`:39`), `grace_confers_benefits`
  default true (`:40`, D-07), `continuity_gap_days`, `conflicting_membership_preference` `principal | granted` (`:42`),
  `cancellation_refund` `prorated_days | prorated_months | no_refund` (`:47`), `ordering_profile_id` (`:55`),
  `qualifies_same_order` (`:61`, member pricing's same-cart switch), `number_scope` `tenant | program | owner_number`.
  Vocabularies:
  `/Users/andrew/rhythm2/services-membership/modules/configuration/db/configuration-vocabularies.ts:7,11,61`.
- **Type** (`membership.membership_types`, program-scoped): `confers_benefits` (`.../membership-types.table.ts:28`), a
  grace override (`:29`), and the flow-down trio `flowdown_enabled`, `flowdown_type_id`, `flowdown_cascades` (`:30-32`,
  CHECK `:55`), with two child lists of CRM configuration ids: `membership_type_flowdown_contact_roles.contact_role_id`
  (`.../membership-type-flowdown-contact-roles.table.ts:15`) and `..._flowdown_organization_types.organization_type_id`
  (`.../membership-type-flowdown-organization-types.table.ts:14`). Types carry no price and no term.
- **Package** (`membership.membership_packages`): **the package IS the Accounting product** (Andrew's D-1 of 2026-10-18;
  the row's id is the product's id, `product_type_id` fixed to `membership_package`,
  `/Users/andrew/rhythm2/services-membership/modules/configuration/db/membership-packages.table.ts:1-19,40`). It carries
  the term rule and renewal settings: `term_basis` `anniversary | calendar | none`, `term_length`, `term_unit`,
  `calendar_start_month`, `sell_for_next_year_after_mmdd`, `sell_for_next_month_after_day`, `allow_mid_period_end`,
  `renewal_continues_from_end` (default true), `renewal_window_days`, `reinstatement_days`, `renews_into_package_id`,
  `auto_renew_default`, `change_policy_set_id`, `assigned_seat_limit`, `reinstatement_audience` `renewing | new`,
  `enrollment_proration` `none | remaining_months | remaining_days` (`:43-60`). Children: `package_fixed_term_ends`
  (fixed term ends by purchase window), `package_grants` (further memberships in other programs, `term_rule`
  `same_as_primary | own`, `.../package-grants.table.ts:24-26`), `package_unit_rules` (per unit type `minimum`,
  `maximum`, `dues_required`, `.../package-unit-rules.table.ts:20-21`).

**Term engine: Implemented** as `modules/terms`, built inside Membership per Andrew's D-45 choice of 2026-10-17 ("build
inside Membership, extract later",
`/Users/andrew/rhythm2/services-platform/docs/proposals/membership-architecture/12-decision-register.md:65`).
`computeTermPeriod` is 1.0's `libraries-dates` algorithm with all 74 spec cases
(`/Users/andrew/rhythm2/services-membership/modules/terms/services/term-rules.ts:1-31`): a renewal continues from the
current end whatever the purchase date; anniversary vs calendar; sell-for-next-period thresholds; reinstatement window.
Grace is the type's override else the program's; status is derived, never stored
(`/Users/andrew/rhythm2/services-membership/modules/terms/services/status.ts:12-27`). Four deliberate departures from
1.0 are listed at `term-rules.ts:32-45`.

**Proration: Decided to live in Commerce, Implemented as policy only.** Membership never computes money; `prepare`
returns a `pricing_policy` with an operation audience (`new | renewing | rejoining`), the first-term proration method
and basis `original_price`, and the service period
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/services/prepare/pricing-policy.ts:1-33,58,87,101`;
member-pricing D-16,
`/Users/andrew/rhythm2/services-platform/docs/proposals/member-pricing/11-decision-register.md:30`). Tier-change
proration is a change policy row (`proration`, `unused_value_basis`, `downgrade_treatment`,
`/Users/andrew/rhythm2/services-membership/modules/configuration/db/change-policies.table.ts:32-41`) that **nothing
reads until P4** (`:4`).

**How prices reach Commerce: Implemented on the read side, Absent as a deployed path.** Prices live in
`accounting.product_prices` by audience and pricing period (member-pricing D-05); Membership reads which audiences a
product's grid names through the one Accounting products port, wired to the real tables
(`/Users/andrew/rhythm2/services-membership/modules/structure/ports/accounting-products.ts:1-36`). Commerce prices the
line from the plan's `pricing_policy`; but Commerce itself is not deployed, so the order-placement port is a refusing
stub (`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/ports/commerce-orders.ts:10`).

### 2. The membership record

**Table `membership.memberships`: Implemented**
(`/Users/andrew/rhythm2/services-membership/modules/memberships/db/memberships.table.ts`). Columns (`:43-58`):
`tenant_id`, `id` (UUIDv7), `number` (bigint, null until fulfilment numbers it), `program_id`, **`owner_kind`
`contact | organization`**, **`contact_id`**, **`organization_id`**, `identity_organizational_unit_id`, `origin`
`principal | flowdown | assigned`, `granted_by_membership_id`, then the term engine's holding column set
(`original_join_date`, `original_join_date_source`, `continuous_since`, `current_term_id`, `status_cache`,
`status_cache_as_of`, `suspended_at`, `suspension_reason_id`, `do_not_renew`, `do_not_drop`, `auto_renew`,
`auto_renew_payment_method_id`, `commerce_fence`, `commerce_fence_ref`;
`/Users/andrew/rhythm2/services-membership/modules/terms/db/holding-columns.ts:28-52`), `directory_opt_out`,
`migration_quality`, `notes`, system columns.

**Who the member is: a two-column exclusive owner, not a polymorphic holder.** The CHECK `memberships_owner_check`
requires
`(owner_kind = 'contact' AND contact_id NOT NULL AND organization_id NULL) OR (owner_kind = 'organization' AND organization_id NOT NULL AND contact_id NULL)`
(`memberships.table.ts:105-109`). Both ids are soft references into `crm` (no cross-schema FK). The owner is also the
Commerce recipient: `prepare` derives the owner from `item.recipient.kind` and refuses anything but `contact` or
`organization` with `recipient_kind`
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/services/prepare/prepare-purchase.ts:323-324,347`).
Vocabulary `OWNER_KINDS = ['contact', 'organization']`
(`/Users/andrew/rhythm2/services-membership/modules/memberships/db/membership-vocabularies.ts:7`).

**Identity and concurrency (D-02, Decided by Andrew 2026-10-17, Implemented).** A principal membership is unique per
`(tenant, program, owner_kind, contact_id, organization_id, identity_organizational_unit_id)` with NULLS NOT DISTINCT
(`memberships_principal_identity_uidx`, `memberships.table.ts:86-89`); a granted one per
`(tenant, granted_by_membership_id, contact_id, organization_id)` (`:90-92`). So one contact may hold **many memberships
across programs** and, in an `identity`-bound program, one per unit; **never two principal memberships of the same
identity**. Across programs sharing an `exclusivity_group`, `prepare` refuses a term that would overlap
(`prepare-purchase.ts:411-412,499`). A principal and a granted membership in one program may coexist; the program's
`conflicting_membership_preference` says which supplies benefits (`02-domain-model.md:216-218`).

**Terms (`membership.membership_terms`): Implemented.** `membership_id`, `program_id` (carried so composite FKs hold
type and package to the membership's program), `membership_type_id`, `package_id` (nullable: a flow-down, assigned or
granted term holds a type, not a purchase), then `starts_on`, `ends_on` (inclusive; null = never ends),
`planned_ends_on`, `state` `pending | committed | cancelled | terminated | superseded`, `operation`
(`enroll | renew | reinstate | rejoin | upgrade | downgrade | transfer | grant | flowdown | assigned | import | staff`),
`committed_at`, `closed_*`, `superseded_by_term_id` / `supersedes_term_id`, `commerce_order_id`,
`commerce_order_line_id`, `commerce_authorization_id` (unique, the fulfilment idempotency key), `commerce_amendment_id`,
`source_term_id` (`/Users/andrew/rhythm2/services-membership/modules/memberships/db/membership-terms.table.ts:30-37`,
`/Users/andrew/rhythm2/services-membership/modules/terms/db/term-columns.ts:29-77`). No two effective or pending terms
of one membership overlap: a persistence check under the membership row lock plus a GiST exclusion in the migration
(`membership-terms.table.ts:11-15`;
`/Users/andrew/rhythm2/services-membership/modules/memberships/services/operations/term-writes.ts:1-16`).

**Status lifecycle: Implemented, derived.**
`suspended > active | active_without_benefits > grace > lapsed > future > pending > none`, computed from terms on the
tenant's business date; `status_cache` is for lists only and a midnight sweep refreshes rows at date boundaries
(`status.ts:12-23`; `04-lifecycle.md:37-80`).

**Renewal records: Implemented as new terms, never edits.** A renewal is a pending term starting the day after the
current end, committed at fulfilment; the current term is untouched (`04-lifecycle.md:115-125`; staff version
`/Users/andrew/rhythm2/services-membership/modules/memberships/services/operations/staff-operations.ts:8-16,308`).

**Upgrade / downgrade: Decided (D-23), Absent in code.** `prepare` refuses an in-term
`upgrade | downgrade | transfer | change_affiliation` with `requires_amendment` because `prepare_change` / `modify` are
P4, blocked on Commerce amendments and Accounting M3
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/services/prepare/eligibility.ts:20-22`;
`/Users/andrew/rhythm2/briefs/membership/PLAN.md`, row P4). An `at_term_end` type change is a renewal into the other
package and does work. **Transfer of an affiliation between units: Implemented** when no dues apply
(`POST .../affiliations/{id}/transfer`,
`/Users/andrew/rhythm2/services-membership/modules/memberships/aws/api/memberships.routes.ts:293`). **Owner transfer
(D-43): Planned**, phase 2; its key is deliberately not declared
(`/Users/andrew/rhythm2/services-membership/docs/migrations/scope/README.md:40-42`).

**Cancellation and refunds: Implemented against the contract.** `prepare_cancellation` → `cancel`: a pending term is
cancelled and its claim closed (delete-if-first removes a membership the claim created and nothing else holds); a
committed term not yet started is cancelled; a started term is terminated with `ends_on = e - 1`; granted terms follow
the parent (`followParent`); children re-derived; self-service may cancel only what has not started
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/services/cancellation/cancellation.ts:8-23`).
Membership answers only a `refundable_fraction` from the program's `cancellation_refund` policy
(`.../cancellation/refund-policy.ts:28-42`); money is Commerce's and Accounting's.

### 3. Organizational memberships: the closest analogue of a household

Two different things wear the word "organization" here; keep them apart.

**(a) Organizational units are NOT organizations and NOT holders.** `membership.organizational_units` is the
association's own structure (National, Region, Zone, Chapter, Section), a tree with `parent_organizational_unit_id`, a
closure table and parent history
(`/Users/andrew/rhythm2/services-membership/modules/structure/db/organizational-units.table.ts:35-42`). A unit relates
to CRM organizations only through typed, dated links `organizational_unit_organizations` (`organization_id`,
`relationship_type_id` seeded `legal_entity | operating_organization | dues_beneficiary | financial_recipient`,
`starts_on`/`ends_on`), at most one open `legal_entity`; the file header states "A link never affiliates the unit's
members with the organization and never drives billing" (`.../organizational-unit-organizations.table.ts:5-7,24-26`;
`.../organizational-unit-relationship-types.table.ts:14`). Members relate to units through **term affiliations**
(`membership_term_organizational_units`: `membership_term_id`, `membership_id`, `organizational_unit_id`,
`organizational_unit_type_id`, `is_primary`, `starts_on`, `ends_on`, `source`, `dues_order_line_id`, `dues_product_id`,
`transferred_from_affiliation_id`;
`/Users/andrew/rhythm2/services-membership/modules/memberships/db/membership-term-organizational-units.table.ts:30-45`)
or, for `identity`-bound programs, through `memberships.identity_organizational_unit_id`. A unit can carry its own dues
product (`organizational_unit_dues_products`, Accounting type `chapter_dues`, per package or catch-all,
`.../organizational-unit-dues-products.table.ts:25,33-34`) and an ordering profile
(`.../organizational-unit-financial-settings.table.ts:23`). **A unit never owns a membership.** This is the proposal's
D-10/D-15
(`/Users/andrew/rhythm2/services-platform/docs/proposals/membership-architecture/05-organizational-hierarchy.md:126-151`).

**(b) A CRM organization CAN be the membership owner, and that membership covers people by creating child memberships.**
This is the real analogue. Implemented end to end (`04-lifecycle.md:238-249`, D-19):

- The organization's membership is a `memberships` row with `owner_kind = 'organization'`, `organization_id` set, and a
  CHECK that it is always `origin = 'principal'` (`memberships_organization_principal_check`,
  `memberships.table.ts:110`).
- **Flow-down ("benefits flow to employees")**: on every write to the parent's terms, and on every CRM contact or
  organization change event, `syncOrganizationChildren` recomputes who should hold a child membership: the contacts
  affiliated _today_ with the organization (and, when `flowdown_cascades`, with its descendant organizations of the
  listed types to depth 10 through `crm.organizations.parent_organization_id`) who hold one of the type's flow-down
  contact roles, read directly from `crm.contact_organization_affiliations` and `crm.contact_organization_roles`
  (`/Users/andrew/rhythm2/services-membership/modules/memberships/services/flowdown/organization-children.ts:1-20,60-66`;
  `/Users/andrew/rhythm2/services-membership/modules/memberships/services/persistence/crm-people.ts:15,25-40`;
  `/Users/andrew/rhythm2/services-membership/modules/memberships/services/flowdown/crm-change-events.ts:1-17`).
- **Assigned seats ("designated roster")**: the contacts named on the organization's decided application
  (`application_assigned_contacts.contact_id`,
  `/Users/andrew/rhythm2/services-membership/modules/applications/db/application-assigned-contacts.table.ts:15`; payload
  `assigned_contacts`,
  `/Users/andrew/rhythm2/services-membership/modules/commerce-participant/contracts/application-payload.ts:59`), capped
  by the package's `assigned_seat_limit` (`organization-children.ts:67-70`). The seat list is read through
  `assignedContactsOfMembership`, i.e. from applications whose `membership_id` is the parent
  (`/Users/andrew/rhythm2/services-membership/modules/applications/services/persistence/applications.ts:19-31`). There
  is no separate "seat" table and no roster edit route: seats change only through an application.
- **Each covered person holds a real membership row** of the same program with `origin = 'flowdown' | 'assigned'`,
  `granted_by_membership_id` = the parent, `contact_id` = the person, its own number, and one term per parent term
  (`source_term_id`) of the type's `flowdown_type_id`, mirroring the parent's dates and state; a child's dates never
  exceed the parent's; a person who leaves the set has the child term ended today (or cancelled if not started);
  children are never billed; suspension follows the parent (`organization-children.ts:6-15`; `suspendChildren`,
  `staff-operations.ts:534`; enforcement `.../enforcement/enforcement.ts:77,112`).
- **Limits on covered people**: flow-down is unbounded (every role holder); assigned seats are bounded by
  `assigned_seat_limit` (null = unlimited).
- **Key contacts / roles on the org membership**: none stored on the membership. The _roles_ that matter are CRM's
  contact roles named on the type (`membership_type_flowdown_contact_roles`). There is no "primary contact",
  "administrator" or "billing contact" column on `memberships`.
- **Billing contact on an org membership: Absent.** Billing-run notices go to "the member: a contact's first e-mail
  address"; for an organization-owned membership the notice is skipped with `no_recipient` because "an organization's
  billing contact is CRM's to name and is not resolved here"
  (`/Users/andrew/rhythm2/services-membership/modules/billing/services/execution/billing-notices.ts:9-11`). The renewal
  order's buyer is simply the owner (`buyer: recipient`,
  `/Users/andrew/rhythm2/services-membership/modules/billing/services/execution/execute-action.ts:146-168`).
- **Contact edits**: the officer's `PATCH .../memberships/{id}/contact` refuses an organization's membership ("its
  contact is changed in CRM",
  `/Users/andrew/rhythm2/services-membership/modules/memberships/services/access/member-contact.ts:40`).

### 4. Benefits and entitlements

**Benefits are a boolean on the type, plus grace policy: Implemented.** `membership_types.confers_benefits`
(`membership-types.table.ts:28`) and `programs.grace_confers_benefits` (`programs.table.ts:40`). There is no benefits
catalogue, no per-tier entitlement rows, no "benefit" table (`02-domain-model.md:220-229` lists what is deliberately
absent).

**"Is this contact a member / entitled" for another app.** Three paths exist or are planned:

1. _Direct table reads of `membership.memberships`_ under `rhythm_table_reader`, as every macroservice reads any table
   (the convention Membership itself uses to read CRM, `crm-people.ts:2-5`). The status must be computed from terms, not
   `status_cache` (`04-lifecycle.md:60-76`). The SQL functions `membership.membership_status(tenant, membership, D)`,
   `membership.current_term(...)`, `membership_episodes`, `memberships_as_of(tenant, as_of, known_at)` exist
   (`/Users/andrew/rhythm2/services-membership/docs/migrations/membership-core/README.md:103-109`). **Implemented.**
2. _The query layer_: dataset `membership.memberships` with computed `status_today`, `current_type`, `episode_count` and
   collections `primary_unit_by_type`, `ancestor_unit_ids`
   (`/Users/andrew/rhythm2/services-membership/modules/memberships/queries/memberships.query.ts:32-66`).
   **Implemented.**
3. _The Platform Eligibility engine_: the member-pricing decision (D-14/D-07, Andrew 2026-10-17) makes the `member`
   audience an Eligibility definition whose `membership.*` condition types are **provided by Membership from its
   resolver**, and the tenant edits the rule in Membership Settings (Q-10)
   (`/Users/andrew/rhythm2/services-platform/docs/proposals/member-pricing/04-domain-model.md:308-317`;
   `11-decision-register.md:21,28,43`). The condition catalogue names `membership.status`, `.program`, `.tier`,
   `.chapter`, `.section`, `.tenure`, `.continuity`, `.expiration`, `.statusReason`
   (`/Users/andrew/rhythm2/services-platform/docs/proposals/eligibility/05-condition-registry-and-providers.md:163`),
   and Platform's 1.0-criteria migration already expects them over dataset `membership.memberships`
   (`/Users/andrew/rhythm2/services-platform/modules/eligibility/services/migration/migration-catalogue.ts:101-114`).
   **Decided, Absent in Membership's code**: services-membership has no `macroservice-rules` dependency and registers no
   condition type (grep of `package.json` and `aws/` finds none); the engine's `eligibility` hooks are stubs that answer
   `eligible` (`/Users/andrew/rhythm2/services-membership/modules/memberships/services/membership-engine.ts:7-8,35`).

**Coverage from an organization membership is exposed exactly like a direct membership: Implemented by construction.**
Because a flow-down or assigned person holds a genuine `memberships` row (`origin` tells you how), every path above sees
it; the only difference is the `conflicting_membership_preference` tie-break when the same person also holds a principal
membership (§2). The eligibility proposal notes the same for pricing: "Organization membership flowing to employees" is
a definition over the affiliation path with `membership.status`, not an implicit rule
(`/Users/andrew/rhythm2/services-platform/docs/proposals/eligibility/07-commerce-pricing-integration.md:95`).

**"Member since" / status on the CRM contact: Absent as a composed property.**
`services-crm/docs/composed-properties.md` has no membership mention. Membership instead registers `contact_id` /
`organization_id` as `reference` fields to `crm.contacts` / `crm.organizations`, which "is how Membership, the owner of
the referencing records, opts into the reverse joins from them (a contact's memberships)"
(`/Users/andrew/rhythm2/services-membership/modules/memberships/metadata/memberships.object.ts:1-9,52-53`). The V0
Members explorer shows "Member Since" and "Continuous Since" from `original_join_date` / `continuous_since` on the
membership (`/Users/andrew/rhythm2/briefs/membership/V0-SCREENS.md:128-131`).

### 5. Eligibility to join

**Purchase eligibility is a pure state machine inside `prepare`: Implemented.** From the stated operation, the effective
mode and the membership's state on the business date (`none | current | grace | reinstatable | lapsed | never_ends`) it
answers the operation as it will be fulfilled or refuses with the supported one; plus owner kind vs `member_kind`,
suspension, fence, pending term, `do_not_renew`, `renewal_window_days`, exclusivity groups and unit rules
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/services/prepare/eligibility.ts:1-27,61`;
`prepare-purchase.ts:8-18,411`). It is evaluated **at purchase (and again authoritatively at placement in Commerce's
transaction)**, not continuously; the only continuous checks are the midnight sweep's status detections and the
flow-down re-derivation on CRM changes.

**Platform eligibility engine for "who may join": Decided, wired only as a column.**
`application_processes.eligibility_binding` holds "the rules-package binding of its eligibility (null until the engine's
W2 wires it)"
(`/Users/andrew/rhythm2/services-membership/modules/applications/db/application-processes.table.ts:3-5,25`). Nothing
reads it.

**Age-based rules: Absent in Membership; the building blocks exist in Platform.** No `date_of_birth`, age or age-out
logic anywhere in services-membership's production code (the only hits are the contact-field allow-list tests refusing
`date_of_birth` edits). CRM contacts carry `date_of_birth` and `date_of_death`
(`/Users/andrew/rhythm2/services-crm/modules/entities/db/contacts.table.ts:105-106`), and the Eligibility reader can
express "at least 18 on the as-of day" with `{ "$asOf": { "shift": { "amount": -18, "unit": "years" } } }`
(`/Users/andrew/rhythm2/services-platform/docs/eligibility-reads.md:39-53`); the catalogue plans `crm.contact.age`
(`05-condition-registry-and-providers.md:162`). A continuous age-out (a child turning 26 losing coverage) would need a
sweep nobody has designed.

### 6. Billing

**Who creates the money documents: Decided and Implemented as the contract.** Commerce creates orders; Accounting
creates invoices, credits, schedules; Membership never writes either and never computes an amount
(`/Users/andrew/rhythm2/services-platform/docs/proposals/membership-architecture/07-commerce-and-finance.md:7-27`).
Membership's own order creations (the dues order after an approved application, a billing run's renewal order, an
officer's order) go through one port whose real implementation waits for Commerce
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/ports/commerce-orders.ts:1-13`).

**Bill-to contact vs member: Absent in Membership.** The checkout envelope has `buyer`, `acting_user_id` and an opaque
`bill_to: z.unknown()` that Membership never reads
(`/Users/andrew/rhythm2/services-membership/modules/commerce-participant/contracts/participant-envelope.ts:59-61`). The
line's `recipient` IS the owner (§2). The proposal's §9a records the _employer-paid_ case as Accounting's open D-10: the
payer's invoice and collections notices go to the payer, the member sees only "awaiting payment"
(`07-commerce-and-finance.md:247-250`). Member-pricing R-05 confirms "Purchaser and beneficiary are distinct; the
purchasing subject is explicit" as a Commerce concept
(`/Users/andrew/rhythm2/services-platform/docs/proposals/member-pricing/11-decision-register.md:13`).

**Recurring / auto-renew: Implemented up to the port.** `memberships.auto_renew` and `auto_renew_payment_method_id` (a
soft reference to Accounting's stored method, never a token; `holding-columns.ts:43-47`); billing templates per program
with `filters` and `actions` (`renewal_order | auto_renewal_order | notice | reminder | expiring_card | drop`,
`/Users/andrew/rhythm2/services-membership/modules/billing/db/billing-templates.table.ts:23-25`,
`.../billing-vocabularies.ts:12`); runs select committed terms of **principal** memberships ending in a range (granted
and flow-down children renew with their parent), skip renewed, never-ending, suspended ones, classify `renewing` vs
`lapsed` by grace, plan the actions, execute on durable jobs with stale-term refusal
(`/Users/andrew/rhythm2/services-membership/modules/billing/services/planning/run-plan.ts:7-18,33-41`); recurring runs
on tenant-zone schedules (`.../recurring/recurring-billing-runs.ts:28`). The auto-renewal order passes
`payment_method_id` to Commerce (`execute-action.ts:168`).

**Installments: Absent in Membership** (Accounting's billing module is receivable-centric; `07:209`). **Proration on
tier change: Decided (D-24: `remaining_days`, `original_price`, downgrade → credit), Absent in code (P4).** **Credits on
cancellation: Decided**: Membership reports a fraction; Commerce/Accounting issue the memo (`07:169-180`; §2 above).

**Payment failure: Decided (Accounting D-6, 2026-10-18), Absent in code.** Finance collects invoices, never memberships;
Membership will consume `accounting.payment.failed`, `invoice.past_due`, `collections.case_closed` and release the
pending term on `uncollectible`; about two agent-days, not briefed (`07-commerce-and-finance.md:211-255`). Today a
failed renewal payment simply leaves the pending term pending and the current term runs into grace.

### 7. Renewals

- **Window and timing: Implemented.** `renewal_window_days` opens a renewal; `reinstatement_days` separates reinstate
  from rejoin; the renewal term starts after the current end (`eligibility.ts:9-26`; `04-lifecycle.md:115-136`).
- **Notices: Implemented to the Communications seam.** Purposes `membership.renewal.notice`, `.reminder`,
  `membership.payment_method.expiring`, requested through Platform Communications with the membership and term as data;
  recipient = the member's first CRM e-mail; organizations and address-less contacts skipped `no_recipient`
  (`billing-notices.ts:1-28`). Staff attention: the `membership.expiring_memberships` item addressed to holders of the
  `membership.renewals` responsibility, scoped by the member's unit chain
  (`/Users/andrew/rhythm2/services-membership/modules/memberships/services/attention/expiring-memberships.ts:1-20`).
- **Portal renew flow: Planned, Absent.** The proposal's `GET /memberships/{t}/me` returns "eligible operations" and the
  portal posts a checkout item (`08-api.md:53-56,109`). The built `/me` answers the member's own memberships, terms,
  affiliations, status and history under `membership.self.read`
  (`/Users/andrew/rhythm2/services-membership/modules/memberships/aws/api/memberships.routes.ts:424`;
  `ownMembershipViews`, `.../reads/membership-views.ts:18,139`), with no eligible-operations or checkout step; the
  portal screens exist only as V0 designs and "no portal work is directed"
  (`/Users/andrew/rhythm2/briefs/membership/PLAN.md`, 2026-10-18 12:35 entry).
- **"Renew for others": Implemented for officers only.** `POST .../memberships/{id}/renew` (`membership.terms.renew`)
  and `POST .../enrollments` (`membership.memberships.enroll`) let a unit officer place a renewal or enrolment order for
  a member in their scope, with the officer as `acting_user` and the member as recipient and buyer
  (`/Users/andrew/rhythm2/services-membership/modules/memberships/services/operations/officer-orders.ts:1-17,104-131`;
  routes `:351,373`). There is no "a family member renews for another family member" path; the only non-owner actors are
  staff and committee-derived officers. CRM's legacy `portal_delegates` (one contact acting for another in the portal)
  was **deliberately not carried over** (Andrew 2026-10-08,
  `/Users/andrew/rhythm2/services-crm/docs/debt/portal-delegates.md:1-12`).

### 8. Applications and the Submissions contract

**Typed application rows: Implemented** (`membership.application_processes`, `applications`, `application_affiliations`,
`application_assigned_contacts`, `application_process_packages`). A process has `kind`
`enroll | renew | reinstate | rejoin | change_affiliation`, `program_id`, `eligibility_binding`, a window,
`outcome_kind` `membership.enroll | .renew | .reinstate`, exactly one `submission_definition_id`, `review_required`
(`/Users/andrew/rhythm2/services-membership/modules/applications/db/application-processes.table.ts:23-35`;
`.../application-vocabularies.ts:8,12`). An application names `contact_id` **or** `organization_id`, `membership_id`
(null until the decision creates one), `submission_id` and `decision_id` (unique; the decision handler's idempotency
key), `commerce_order_id` (`.../applications.table.ts:30-43`). Custom fields on `membership.memberships` are an approved
opt-in entity (R2-ARCH-001; `MEMBERSHIP_CUSTOM_FIELD_OBJECT_ID`, `membership-vocabularies.ts:19`).

**Submissions seam: Implemented against frozen contracts, stub outbound.** Subject kinds `membership.membership_types`
(a join is about the type applied for) and `membership.memberships` (renew / reinstate / change), decisions delivered to
service function `membership_submission_decisions`
(`/Users/andrew/rhythm2/services-membership/modules/applications/contracts/subject-kinds.ts:1-17,78-81`); the
acknowledgement port refuses until the engine's P1 is deployed (`.../ports/submissions.ts:1-9,26`).

**"Create a membership" as a Submission-compatible operation (R2-ARCH-002): Decided 2026-10-20, Planned.** The standard
names "create a membership" as the first example of an operation that must be published or recorded as deliberately not
(`/Users/andrew/rhythm2/services-platform/docs/standards/submission-compatible-operations.md:16`). The proposal's 04
§5.1 now says the authoritative shape is the Submissions Architecture: a membership application is an application opened
by a governed outcome of a process, Membership keeps a typed row only when a review exists (D17), and **Membership no
longer places the dues order on a decision (D7)** (`04-lifecycle.md:111`). The code still places it
(`decision-handler.ts:196-218`, `applications.commerce_order_id`), so code and decision diverge here.

### 9. Query and semantic exposure

**Implemented.** Metadata objects `membership.memberships`, `membership.membership_terms`,
`membership.lifecycle_events`, `membership.term_affiliations` with references to `crm.contacts`, `crm.organizations`,
the structure objects and the memberships object itself (`granted_by_membership_id`)
(`memberships.object.ts:16-19,52-59`). Query datasets with the computed picks and unit collections (§4). Export datasets
for the tenant data export: memberships, terms, term affiliations, programs, types, packages, change policies, status
reasons, units, unit types, unit versions, applications, billing, snapshots, lifecycle events, record history, versions,
import crosswalk (`modules/*/export/*.dataset.ts`; relationships to `crm_contacts` / `crm_organizations` at
`/Users/andrew/rhythm2/services-membership/modules/memberships/export/memberships.dataset.ts:28-29`). Snapshot tables
`membership_snapshot_members` (with `member_kind`, `member_id`, `origin`, `granted_by_membership_id`,
`/Users/andrew/rhythm2/services-membership/modules/snapshots/db/membership-snapshot-members.table.ts:43-52`),
`_affiliations`, `_dimensions`, `_counts`. **Aggregates:** `episode_count` is the only computed aggregate; "members by
unit" is a roster join over affiliations and closure (`05-organizational-hierarchy.md:176-191`).

### 10. Portal

- **Built**: one route, `GET /memberships/{t}/me` under the tenant-level key `membership.self.read`, and the built-in
  policy `policy:membership:portal-member` that Platform's Portal Member role is to gain on reconcile (Platform's side
  not done) (`/Users/andrew/rhythm2/services-membership/aws/membership.service.ts:20-22,216,333`;
  `docs/migrations/scope/README.md:44-56`). Officer routes are `access: portal` because officers are portal users.
- **Planned**: `/me/units`, unit rosters, the directory, eligible operations and checkout (`08-api.md:53-57`), the
  "Member Center successor and Chapter Center successor" in P9 (`11-implementation-phases.md:161-173`), V0 designs
  generated but unmounted.
- **Decided**: portal is always the last milestone of every plan and Andrew builds portal components himself (Andrew
  2026-10-19 ~18:10 ET, memory `portal-always-last`); for Membership specifically "no portal work is directed"
  (`PLAN.md`, 2026-10-18 12:35) and P9 wiring "waits for your word" (`MORNING-SUMMARY-2026-10-19.md:34-36`).

### 11. Merge participation and delete impact

**CRM contact / organization merge: Absent in Membership.** CRM's merge discovers participants from the catalog: a
schema is a participant when it publishes `<schema>.merge_plan_<object>` and `merge_apply_<object>`, and "unknown
references block by default: a merge that might strand data does not run"
(`/Users/andrew/rhythm2/services-crm/docs/merging/architecture.md:353-372,407-409`). services-membership publishes no
`merge_plan_*` / `merge_apply_*` function (grep of `db/migrations` and `modules` finds none) and does not depend on
`@rhythmsoftware/macroservice-record-merge`. So a contact merge that touches a contact with a membership is either
blocked or, if the coverage guard is not aware of the `membership` schema, leaves `memberships.contact_id` pointing at
the merged- away contact. The proposal assumed participation ("CRM contact change events (flow-down recomputation, owner
merges)", `07-commerce-and-finance.md:265`) and Committees was told to write one
(`06-committees-and-authorization.md:129`); CRM's merge architecture reserves a `consolidate` policy for "future
memberships" (`architecture.md:302`). **Unit merges, by contrast, are Implemented** (affiliations moved, identity-bound
memberships re-pointed unless the owner already holds the survivor's,
`/Users/andrew/rhythm2/services-membership/modules/memberships/services/affiliations/unit-merge.ts:1-27,181`).

**Delete impact: Absent across the boundary.** CRM's `listContactDeleteImpacts` counts only CRM-owned rows (collections,
affiliations, roles, custom fields, spouse references;
`/Users/andrew/rhythm2/services-crm/modules/entities/services/persistence/contacts.ts:1295-1330`); Membership has no
delete-impact provider (grep `delete.?impact` in services-membership finds nothing). A CRM contact delete would strand
`memberships.contact_id`, which is a soft reference.

**CRM change events into Membership: Implemented on Membership's side, registration pending.** The handler
`membership_crm_change_handler` re-derives organization memberships on contact and organization events
(`crm-change-events.ts:1-17`); CRM's own subscription of it is CRM's change (`:8`).

### 12. Milestone state, and anything about families

**Milestones.** No `docs/milestones/` exists in services-membership and no `MEM-M*` record exists under `briefs/`.
Membership predates the R2-DEV-001 milestone standard: its state is "Accepted by Andrew on 2026-10-18 (13 sections; the
README's acceptance checkpoint at a35ec56)", then acceptance cycle 1 "not ready", then **stopped 2026-10-19 15:40 ET**
(`MORNING-SUMMARY-2026-10-19.md:8-13,38-50`). Phase state per `PLAN.md`: P0, P1, P2/P2b, P3 (+ Submissions seam), P5,
P6a/P6b, P7a/P7b, P8a/P8b on master; P7c brief ready, not submitted; P8c to brief; **P4 amendments blocked on Commerce
P7 and Accounting M3**; **P9 console/portal wiring Andrew-directed, not started**; V0 designs done. The proposal's
finish line is `11-implementation-phases.md:7-16`.

**Families, households, spouses, dependents, joint memberships, children, age-outs: Absent everywhere.** A grep of
`household|family|spouse|dependent|joint|beneficiar|age-out|birth` across services-membership (modules, docs,
migrations, tests) and both proposals finds no family concept: the only hits are job "families" (durable-jobs
vocabulary), the `dues_beneficiary` unit link, "independent chapter" programs, and `date_of_birth` in the field
allow-list tests. The 1.0 membership, type and package schemas on disk have no such field either. The only kinship fact
in Rhythm 2.0 is CRM's `contacts.spouse_id`, a single self-referencing composite FK with a not-self CHECK, cleared when
that contact is deleted (`/Users/andrew/rhythm2/services-crm/modules/entities/db/contacts.table.ts:100-101,125,142`);
CRM has no household, relationship-type or dependent table (`ls services-crm/modules/entities/db/`).
`briefs/households/` was created empty on 2026-10-21 (this research is its first file).

---

### (a) The four household roles against what exists today

| Role                                       | Individual membership (`owner_kind = contact`)                                                                                                                             | Organizational membership (`owner_kind = organization`)                                                                                                                                                                                                                        | Label                                                                                                                                                                                                                                         |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Holder** (owns the membership)           | `memberships.contact_id`; identity `(program, contact[, unit])` (`memberships.table.ts:49,86-89`)                                                                          | `memberships.organization_id`, always `origin = principal` (`:50,110`); the organization is a `crm.organizations` row                                                                                                                                                          | Implemented; the owner is one of exactly two kinds (`OWNER_KINDS`, `membership-vocabularies.ts:7`; CHECK `:105-109`; `prepare` refuses other recipient kinds, `prepare-purchase.ts:347`)                                                      |
| **Payer** (responsible for payment)        | The owner, as Commerce `recipient` and `buyer` (`execute-action.ts:146-168`, `officer-orders.ts:94`); the stored method on the membership (`auto_renew_payment_method_id`) | The organization, as buyer; no billing contact anywhere in Membership; notices skipped `no_recipient` (`billing-notices.ts:9-11`)                                                                                                                                              | Absent as a distinct role. Commerce's `bill_to` is passed through unread (`participant-envelope.ts:61`); the employer-paid split is Accounting's open D-10 (`07:247-250`)                                                                     |
| **Covered member** (entitled to benefits)  | The holder, through `confers_benefits` on the type of the current term, grace per program                                                                                  | Each flow-down / assigned person holds a **child `memberships` row** (`origin`, `granted_by_membership_id`, `contact_id`), with mirrored terms, never billed, suspension following the parent (`organization-children.ts:1-20`)                                                | Implemented for organizations. Membership of the covered person is first-class and visible to every reader (query layer, eligibility, snapshots) exactly like a principal membership                                                          |
| **Authorized manager** (may administer it) | The holder in the portal (`membership.self.read`, read only); staff; a unit officer with derived authority (`officer-orders.ts`, `member-contact.ts`)                      | Nobody from the organization's side: no key contact, administrator or seat-manager role on the membership; seats are set on the application only (`assignedContactsOfMembership`, `applications.ts:19-31`); contact edits refused for org memberships (`member-contact.ts:40`) | Absent as a role on the membership. Authority in Rhythm 2.0 comes from Platform assignments (tenant roles, committee-derived unit scopes), never from a column on a membership; CRM portal delegation was dropped (`portal-delegates.md:7-8`) |

### (b) Household as holder: generalise the organization path, or add a holder kind?

**What the organization path gives for free, and what it binds you to.** If a household were a CRM organization (an
`organizations` row of a household type, with the family as `contact_organization_affiliations` carrying roles such as
`head`, `spouse`, `child`), then _nothing in Membership changes_: the household's membership is
`owner_kind = 'organization'`, a type with `flowdown_enabled` and the household roles in
`membership_type_flowdown_contact_roles` covers every family member with a real child membership, `assigned_seat_limit`
caps a "family of up to 5" package, suspension/cancellation/renewal cascade already, snapshots and the query layer
already show `origin` and `granted_by_membership_id`, and CRM change events already re-derive coverage when someone
joins or leaves the household (`crm-change-events.ts:10-14`). The cost is semantic and lands in CRM, not Membership: CRM
organizations model employers and legal bodies ("CRM's `parent_organization_id` hierarchy models corporate parentage",
`02-domain-model.md:172`), the member-pricing and eligibility rules treat `primary.affiliations[].organization` as an
employer path (`eligibility/07:95`), directories and the Members explorer would show a household as an "organization
member" with `member_kind = organization`, and the cascade walks `parent_organization_id` to depth 10
(`crm-people.ts:15,58-62`), which a household never needs but would be subject to. The four roles map poorly: the
household itself becomes the payer (there is no person to charge a card to; `auto_renew_payment_method_id` belongs to
the owner row), and "authorized manager" still has no home.

**What a third holder kind costs, concretely.** Every place below is where `contact | organization` is spelled out and
would need a `household` branch (or a generalised `holder_kind` / `holder_id` pair):

1. `memberships.owner_kind` enum, `contact_id` / `organization_id` columns and the CHECK `memberships_owner_check`
   (`memberships.table.ts:48-50,105-109`); the two identity indexes that enumerate both id columns (`:86-92`) and the
   `memberships_organization_principal_check` (`:110`), which decides whether a household membership may itself be
   granted (it should not be: the household is always principal, like an organization).
2. The vocabularies `OWNER_KINDS` (`membership-vocabularies.ts:7`) and `MEMBER_KINDS` on programs
   (`configuration-vocabularies.ts:7`; `programs.table.ts:34`) and the `prepare` owner derivation and refusal
   (`prepare-purchase.ts:323-324,347`), plus `findPrincipalMembership` / `ownerMembershipsInGroup`, which branch on
   `owner.contact_id` (`memberships.ts:96-145`).
3. The Commerce contract: `recipient.kind` is typed `'contact' | 'organization'` in Membership's own port and checkout
   builder (`commerce-orders.ts:21,36`, `purchase-checkout.ts`); Commerce's `party.kind` is an open string
   (`participant-envelope.ts:29`), so Commerce would also have to know what a household recipient is, who pays
   (`bill_to`) and whose card is stored, which is the payer role.
4. Flow-down reads CRM affiliations and roles by `organization_id` (`crm-people.ts:17-42`) and
   `syncOrganizationChildren` requires `parent.organization_id` (`organization-children.ts:60`); a household holder
   needs its own "who is covered" source (a CRM household membership table, or Membership-owned seat rows) and its own
   `wantedPeople`.
5. Applications: `applications.contact_id | organization_id` (`applications.table.ts:31-32`) and the `assigned_contacts`
   payload, which today is the only seat-naming mechanism; a household needs a seat list that survives beyond the
   application (add a child mid-term) and a manager who may edit it.
6. Snapshots `member_kind` enum over `OWNER_KINDS` (`membership-snapshot-members.table.ts:43`), the memberships metadata
   references (`memberships.object.ts:52-53`), export relationships (`memberships.dataset.ts:28-29`), the
   `memberships_for_queries` view columns, the eligibility condition types over `membership.memberships` by `contactId`
   (`migration-catalogue.ts:101-114`), and the member contact edit refusal (`member-contact.ts:40`).
7. Billing: `run-plan.ts` filters principal memberships (fine) but `execute-action.ts:146-148` and
   `billing-notices.ts:39-42` resolve the recipient only for `contact_id`, else organization; a household needs "the
   payer's e-mail and stored method", which is the role Membership does not model.

**Assessment.** The schema points that bite are the same in both routes: _payer_ and _authorized manager_ are absent
today for both individuals and organizations, and either route must add them (a `payer` reference on the membership or
on the term's purchase, and a manager relation that Platform authorization can evaluate). Where the routes differ is
_coverage_: the organization path gets coverage for free by reusing flow-down (roles on CRM affiliations) but puts the
household into CRM's organization model; a holder kind keeps the household out of CRM organizations at the price of
touching every `contact | organization` branch above (about a dozen files plus one migration on a NULLS-NOT-DISTINCT
unique index, and the Commerce recipient contract). A middle path the existing schema already suggests: keep
`owner_kind = 'contact'` (the holder is the head of household, a person, which keeps payer defaults, stored method,
notices and portal `/me` working), and model the household as **a Membership-owned seat roster on a principal contact
membership** (a new `membership_covered_contacts` table: `membership_id`, `contact_id`, `relationship`, dated), reusing
the granted- membership machinery (`origin` gains `household`, `granted_by_membership_id` = the holder's membership,
terms mirror through `followParent` / `syncOrganizationChildren` generalised to "children of a principal membership"),
with `assigned_seat_limit` as the cap. That changes `MEMBERSHIP_ORIGINS` and `memberships_organization_principal_check`
(a contact-owned membership would become a parent, which the CHECK permits today since it only constrains
organizations), leaves `owner_kind` alone, and gives the Eligibility engine the same `membership.status` answer for a
covered child as for anyone else. It still needs the payer and manager roles added explicitly.

### (c) Questions the code could not answer

1. **Who is the payer when a household holds a membership**, and is the payer a role on the membership, on the order
   (Commerce `bill_to` / purchasing subject, R-05), or on the Accounting customer? Membership passes `bill_to` through
   unread; Accounting's D-10 (payer ≠ beneficiary notices) is open.
2. **Where does the household itself live**: a CRM organization of a household type, a new CRM entity, or a
   Membership-owned roster? CRM today has only `contacts.spouse_id`; there is no household or relationship table and the
   CRM owner has not been asked.
3. **What "authorized manager" means in Platform terms**: a derived `security_assignments` row keyed by contact (as
   Committees does) scoped to a membership resource, or a portal delegation (which Andrew declined for CRM on
   2026-10-08)? Membership's keys scope by `organizational_unit` only (`scope/README.md:20-33`).
4. **Age-outs**: is coverage of a child time-limited by `date_of_birth`, and if so is it evaluated at purchase, at
   renewal, or continuously by the sweep? Nothing in Membership reads a birth date; the Eligibility engine can express
   the rule but no provider for `crm.contact.age` exists yet.
5. **Pricing of a household package**: one price for the household (a package with `assigned_seat_limit`), per-seat dues
   lines (the chapter-dues child-line shape), or a member-pricing audience for covered children? The grid is
   Accounting's and the audience rule is the Eligibility engine's; neither has a household audience.
6. **Does a covered family member keep an independent membership identity** (own number, own `original_join_date`, own
   history, able to convert to a principal membership when they leave the household), as flow-down children do today, or
   is coverage a benefit without a record? The current design strongly prefers "a real membership row per person".
7. **Merge and delete**: Membership has no CRM merge participant and no delete-impact provider; a household design that
   adds contact references (payer, manager, covered contacts) multiplies the stranded-reference surface. Is the merge
   participant to be built first?
8. **Milestone framing**: Membership is stopped on Andrew's 2026-10-19 order and has no R2-DEV-001 milestone record;
   whether a households milestone is a Membership milestone, a CRM milestone (the household entity), or both, is not
   recorded anywhere.

## 23. R3. How the money-handling and engagement applications identify the person or organization

## R3. How the money-handling and engagement applications identify the person or organization

Research for the Households proposal (households = groups of related people, with household memberships). Question
answered: where does each application name a customer, payer, donor, buyer, recipient or consenting party today, and
where could a household appear as a party without making an invoice, a payment, a gift receipt or a consent ambiguous?

Read-only. Written 2026-10-21 04:30 ET.

### Sources and their state

| Source               | Checkout                                                                     | HEAD        | Note                                                                                                                                                        |
| -------------------- | ---------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| services-accounting  | `/Users/andrew/rhythm2/services-accounting`, master                          | `17cca211`  | the live Accounting macroservice (schema `accounting`)                                                                                                      |
| services-finance     | `/Users/andrew/rhythm2/services-finance`                                     | `17cca211`  | **a symlink to services-accounting** (`services-finance -> services-accounting`); not a separate service, nothing of its own                                |
| services-commerce    | `/Users/andrew/rhythm2/services-commerce`, master                            | `bd62910a`  | live (schema `commerce`)                                                                                                                                    |
| services-marketing   | `/Users/andrew/rhythm2/services-marketing`, master                           | `2e5a442`   | **README only** (one commit, no tables, no migrations); nothing built                                                                                       |
| services-crm         | `/Users/andrew/rhythm2/services-crm`, master                                 | `eac3df30`  | read for merge, delete and consent facts                                                                                                                    |
| services-platform    | `/Users/andrew/rhythm2/services-platform`, master                            | `0614c54cf` | proposals: commerce-orders, payment-methods-and-collection, licensing-entitlements-billing, pricing-promotions, member-pricing, events, events-architecture |
| fundraising worktree | `/Users/andrew/rhythm2/worktrees/services-platform/fundraising-architecture` | `9975e9f34` | `docs/proposals/fundraising/` (31 chapters; ADRs "recommended", 30 §3 treats them as taken)                                                                 |
| marketing worktree   | `/Users/andrew/rhythm2/worktrees/services-platform/marketing-architecture`   | `ed285d0ea` | `docs/proposals/marketing/` (31 chapters + appendices + `decisions-2026-10-20.md`)                                                                          |
| billing worktree     | `/Users/andrew/rhythm2/worktrees/services-platform/billing-architecture`     | `d3379e7c8` | `docs/proposals/billing-architecture/` (15 chapters; 13 decisions RULED)                                                                                    |

Labels: **Implemented** (in a service's tables or code on master), **Decided** (Andrew ruled it; may or may not be
built), **Planned** (proposed, recommended, not yet ruled or built), **Absent** (nothing in code or proposal). Paths are
absolute; `ACC` = `/Users/andrew/rhythm2/services-accounting`, `COM` = `/Users/andrew/rhythm2/services-commerce`, `CRM`
= `/Users/andrew/rhythm2/services-crm`, `PLAT` = `/Users/andrew/rhythm2/services-platform/docs/proposals`, `FUND` =
`/Users/andrew/rhythm2/worktrees/services-platform/fundraising-architecture/docs/proposals/fundraising`, `MKT` =
`/Users/andrew/rhythm2/worktrees/services-platform/marketing-architecture/docs/proposals/marketing`, `BILL` =
`/Users/andrew/rhythm2/worktrees/services-platform/billing-architecture/docs/proposals/billing-architecture`.

The one sentence that matters most: **every money party in every service is a closed two-value kind, `contact` or
`organization`, stored as a (kind, id) pair pointing at CRM with no cross-schema foreign key.** There is no accounting
customer table, no billing account, no fundraising constituent table, no household anywhere; the only household-like
concept in any proposal is Fundraising's recognition rule, which computes a "household key" from CRM `spouse_id` pairs
and writes soft credits (never money) to the spouse.

---

### ACCOUNTING / FINANCE

#### 1. The "customer" concept — Implemented: no customer table; documents reference CRM parties directly

- **No accounting customer table.** Accounting reads CRM's `crm.contacts`, `crm.organizations`, their addresses, e-mail
  addresses and affiliations directly, as declarations it does not own: `ACC/db/crm/parties.ts:1-7` ("Accounting reads a
  contact itself rather than calling CRM"), tables at `:34-50`, affiliations at `:95-101`.
  `ACC/docs/accounting/customers.md:46-47`: "a customer is CRM's record, not Accounting's".
- **The customer kind vocabulary is two values, defined in the contracts package:**
  `BILL_TO_KINDS = ['contact', 'organization']`
  (`ACC/node_modules/@rhythmsoftware/macroservice-accounting-contracts/dist/invoices.js:25`, `BillToSchema` at `:27`),
  re-exported to the database layer at `ACC/modules/transactions/db/document-vocabularies.ts:7-9`.
- **Columns.** Every receivables document carries `bill_to_kind` + `bill_to_id` (NOT NULL) and most carry a
  `bill_to_name` snapshot: invoices `ACC/modules/transactions/db/invoices.table.ts:122-124`, with the rule at `:18-21`
  ("CRM's contact or organization id, no cross-schema foreign key by Platform's rule; `bill_to_name` and
  `billing_address` are snapshots"). Constraint: `oneOfCheck('invoices_bill_to_kind_check', …, BILL_TO_KINDS)` at
  `:206`; index `invoices_bill_to_idx` at `:264`. The same pair + CHECK appears on payments
  (`payments.table.ts:73-75, 146`), credits (`credits.table.ts:57-59, 107`), refunds (`refunds.table.ts:81-83, 136`),
  credit/debit memos, write-offs, reversals, settlements, payment intents, credit applications, receivable events
  (`receivable-events.table.ts:100-101, 112`), installment schedules
  (`ACC/modules/billing/db/installment-schedules.table.ts:70-72, 124`).
- **There is no `customer_contact_id` / `customer_organization_id` pair in base tables.** Those two names exist only as
  derived columns of the query-dataset views so that entity-aware links can resolve ("the polymorphic pair cannot link;
  these two can"): `ACC/modules/transactions/db/customer-names.ts:40-55` (`CUSTOMER_REFERENCE_COLUMNS`), with the
  customer NAME joined live from `crm.contacts` / `crm.organizations` by kind at `:22-33`.
- **Bill-to vs ship-to.** Bill-to is a party; **ship-to is an address, not a party**: `invoices.ship_to_address jsonb`
  (`invoices.table.ts:126`; rule at `:20-21`: the address lines that `ships` are taxed at). Lines carry `ships boolean`
  (`invoice-lines.table.ts:21`). No ship-to party on an invoice.
- **"Billing contact on an organization customer" — Absent in Accounting's schema.** An invoice billed to an
  organization has no billing-contact column. The invoice e-mail goes "to the bill-to contact's primary e-mail address
  in CRM … an invoice billed to an organization is refused the same way (1.0 failed reading the absent contact)"
  (`ACC/modules/transactions/services/invoices/invoice-emails.ts:8-11`, refusal at `:89-91`); the caller may pass
  explicit `to` addresses (`ACC/docs/accounting/invoices.md:33`). CRM seeds a `billing_contact` role on affiliations
  (`PLAT/payment-methods-and-collection/rhythm-2-payment-methods-and-collection-architecture.md:236`), and the Billing
  proposal plans to resolve statement recipients through `organization_billing_contact`
  (`BILL/09-statements-and-communications.md:17`) — Planned, not Implemented.
- **"Customer type" kinds.** Only `contact | organization`. The ledger subledger keeps 1.0's shape: `customer_type`
  (enum `SUBLEDGER_CUSTOMER_TYPES = ['contact','organization']`, `ACC/modules/batches/db/subledger-entries.table.ts:41`)
  with separate `contact_id` and `organization_id` columns (`:76-78`); revenue schedules likewise
  (`revenue-recognition-schedules.table.ts:66-68`). Tax exemption is read from `crm.organizations.tax_exempt` for an
  organization bill-to only (`invoices.table.ts:21-22`, `parties.ts:44-45`).
- **Constraints that forbid both/neither.** The pair is NOT NULL + enum CHECK on every document (above); the optional
  pairs are "both or neither": payer `check('payments_payer_check', (payer_kind IS NULL) = (payer_id IS NULL))`
  (`payments.table.ts:148`); the collection remitter generated columns are set together (`invoices.table.ts:35-42`).
- **Per-party settings, not a customer master:** `accounting.customer_terms` — one optional row per `party_kind` +
  `party_id` (`CUSTOMER_TERMS_PARTY_KINDS = ['contact','organization']`,
  `ACC/modules/transactions/db/customer-terms.table.ts:1-12`, enum near `:36`), holding default payment terms,
  `collection_status`, `collection_option_ids`, `remits_for_others` (`ACC/docs/accounting/customers.md:63-80`). A soft
  reference, "never checked against CRM".
- **Stripe customer mapping is per CONTACT only**: `accounting.stripe_customers (tenant_id, contact_id) -> customer_id`
  (`ACC/modules/transactions/db/stripe-customers.table.ts:1-7, 17-21`); an organization has no Stripe customer; setup
  intents take `contact_id` (`ACC/docs/accounting/stripe.md:67`).

#### 2. Invoices — Implemented

- **Who is invoiced:** the single bill-to party (§1). Source of the invoice (`source`, `order_id`, billing run,
  installment) never changes the party (`invoices.table.ts:28-35`).
- **Line-item customer: Absent.** `invoice_lines` has no party column (header at `invoice-lines.table.ts:1-40`; only
  `order_line_item_id`, product, GL snapshot). Every line belongs to the header's bill-to.
- **Multi-customer allocation ("one payment, many customers", Andrew 2026-10-19) — Decided, not built.** Today "a
  payment pays only its own bill-to's invoices: an allocation to another customer's invoice is refused 409
  `payment_customer_mismatch`" (`ACC/docs/accounting/payments.md:130`; code
  `ACC/modules/transactions/services/allocation-plan.ts:53, 93`; message `payment-refusals.ts:37`). The decision: "a
  payment is the one receipt … and its allocations apply to invoices of any customers; the customer summary, statements
  and aging attribute by allocation, not by payment; the unapplied remainder stays on the one payment … The remittance
  fan-out retires" (`ACC/docs/debt/payment-model-one-receipt-many-customers.md:17-23`). Milestone #11 "decided, not
  planned" (`ACC/docs/milestones/README.md:33`). Recorded design intent for it: refunds of the remainder go to the
  payer; statements/summary/aging attribute by allocation; "Credits never cross customers; only money does"
  (`ACC/docs/milestones/plans/ACCT-M2-plan.md:42-50`, items 3a-3e).
- **Invoice recipient / e-mail:** bill-to contact's primary CRM address or explicit `to` (≤10)
  (`invoice-emails.ts:8-11, 36`). The rendered document exposes `bill_to_contact` / `bill_to_organization` as the
  bill-to "as CRM has it" (`ACC/modules/transactions/services/invoices/invoice-document-data.ts:18, 40`).
- **Statements:** a document template of kind `statement` names its recipients: `send_to` = `all_contacts` |
  `contact_roles` + `contact_role_ids` (CRM role ids, no FK)
  (`ACC/modules/books/db/document-templates.table.ts:18-21, 49, 86-87`; `ACC/docs/accounting/document-templates.md:23`).
  The statement's merge data is `customer`, `bill_to_is_contact`, `contact`, `organization`, aging buckets, open
  invoices/credits (`document-templates.md:137`). Statement RUNS are not in Accounting (Billing owns them, §10).
  Statements are per customer party; nothing folds a household.
- **The one place an organization's items fold into a contact's (the analogue a household rollup would copy):** the
  portal "payable description" lists "a contact's open invoices with its primary organization's" for paying them
  (`ACC/docs/accounting/invoices.md:31`; `ACC/modules/transactions/services/invoices/invoice-payability.ts:18-19`). The
  customer 360 summary explicitly does NOT fold: "A contact's figures are its own, never its primary organization's"
  (`ACC/docs/accounting/customers.md:48-49`).

#### 3. Payments — Implemented (payer optional, offline only)

- **Payer vs invoice customer.** `payments.bill_to_kind/id/name` is "the customer"; `payer_kind`, `payer_id`,
  `payer_name` are "who actually sent the money when it was not the bill-to (an employer remitting for its members)"; a
  payer may be a CRM party or a name alone; "All null: the bill-to paid. Reporting and reconciliation data: the payment
  still allocates only to its bill-to's invoices and no ledger reads it. Written once" (`payments.table.ts:34-38`,
  columns `:73-78`, CHECKs `:146-148`). Only an **offline** payment may carry a payer; a processor payment refuses one
  ("its payer is the card's or the account's holder") (`ACC/docs/accounting/payments.md:136-140`). The payer uses the
  same `BILL_TO_KINDS` enum (`:147`).
- **A payment by one contact applied to another's invoice — refused today** (409 `payment_customer_mismatch`, §2),
  allowed after milestone #11. The built workaround is the **remittance**: one header with `remitter_kind/id/name` that
  fans out "one ordinary offline payment per bill-to of its lines, each with the remitter as its payer", remainder
  parked on the remitter (`ACC/modules/transactions/db/remittances.table.ts:1-7, 14-16`; lines carry the invoice's
  `bill_to_kind/id`, `remittance-lines.table.ts:8`). Decided to retire (debt record above).
- **Payment methods per customer: Absent.** `accounting.payment_methods` is tenant configuration (card / bank_debit /
  offline behaviours, `payments.table.ts:22-25`); no stored-instrument table exists in Accounting, Commerce or Platform
  (grep of all `*.table.ts` for stored/vaulted methods: none). The only per-party vault link is `stripe_customers` per
  contact (§1). Commerce stores `instrument_ref` (a stored-method id or token reference, never card data) on tenders
  (`COM/modules/orders/db/order-tenders.table.ts:14-15`). Billing proposal D-12 plans stored methods with an "owner
  (contact or organization)" (`BILL/15-stored-payment-methods.md:13`; `BILL/13-decisions.md:197`) — Planned.
- **Refunds to whom.** "money returned to the customer" from a payment's unapplied/allocated money or a credit; the
  refund carries the customer snapshot `bill_to_kind/id/name` (`refunds.table.ts:1-5, 29, 81-83`). No payee column
  today; the one-receipt milestone intends "the refund records a payee defaulting to the payer" (`ACCT-M2-plan.md:44`,
  3a).
- **Credits and whose balance.** A credit is "a customer's store credit" with `bill_to_kind/id`
  (`credits.table.ts:1-4, 22-23, 57-59`); "a credit made from a payment belongs to the payment's customer"; "Settle
  Invoices refuses another customer's invoice or credit" (`ACCT-M2-plan.md:54`); after milestone #11 still "Credits
  never cross customers" (`:48`).
- **Recurring billing / payment plans.** Installment schedules: "The owner is the bill-to (`bill_to_kind`,
  `bill_to_id`)"; each installment becomes its own invoice billed to that owner
  (`installment-schedules.table.ts:1-13, 70-72`). Billing runs act on `target_type` `contact | organization` from a
  saved query (`ACC/docs/accounting/billing-api.md:65`); billing notices go to `target` (the record) or `roles`
  (contacts holding `contact_role_ids` on it) (`ACC/modules/billing/db/billing-notices.table.ts:12-14, 54-72`).
- **Multi-currency.** Per business unit: `business_units.is_multi_currency`
  (`ACC/modules/books/db/business-units.table.ts:114`, restored by Andrew 2026-10-20; tenant-level gate deferred,
  `ACC/docs/debt/tenant-level-multicurrency-gate.md:1-6`); every document has `currency_code` + `exchange_rate`
  (`invoices.table.ts:120-121`); the customer summary answers one row per currency (`customers.md:1-6, 25-40`). Currency
  is a property of the document, never of the party.

#### 4. Reporting / reconciliation — Implemented; no hierarchy rollup

- **AR aging by customer:** the receivable ledger `receivable_events` carries `bill_to_kind/id` per fact
  (`receivable-events.table.ts:19, 100-101`); the month-end snapshot `ar_snapshot_items` has `customer_kind` (enum
  `BILL_TO_TYPES`), `customer_id`, `customer_name` captured from CRM "so history keeps the name it had … null where CRM
  no longer holds the party (merged away or erased)"
  (`ACC/modules/transactions/db/ar-snapshot-items.table.ts:17-20, 74, 87`; `ACC/docs/snapshots/ar-aging.md:11-18`);
  totals per business unit, currency and bucket, not per customer (`ar-aging.md:18`). The customer summary and the aging
  share one as-of function restricted to one (kind, id) (`customers.md:55-61`).
- **Rollup by organization hierarchy: Absent.** No `hierarchy`, `parent_organization`, `rollup` in Accounting modules or
  docs (grep empty). CRM organizations do have `parent_organization_id`
  (`PLAT/payment-methods-and-collection/…architecture.md:236`), used only to resolve a `parent_organization` bill-to
  option at checkout (§6), never for reporting. The competitive note is explicit: "Accounting hierarchies let a parent
  pay, but do not replace the invoice's debtor … For Rhythm, the debtor is always one party on the invoice, its bill-to"
  (`…architecture.md:296-302`).

#### 5. Merge participant and delete impact — Decided policy, NOT implemented

- **Accounting has no record-merge participant.** No `record-merge` / `merge_apply` in `ACC/modules`,
  `ACC/db/migrations` or `package.json` (grep empty). The policy exists only as a plan: Accounting participant name
  `accounting`; per-table policy "Invoices (bill-to), payments (owner), credits, statements, installment schedules …:
  **Reassign** to the survivor … issued documents keep their frozen bill-to name and address snapshot"; processor
  customers: keep both mappings; open payment intents: **Block** (`ACC/docs/plan/02-accounting-architecture.md:697-712`,
  decision O-10).
- **CRM's merge engine treats Accounting as unregistered.**
  `REQUIRED_PARTICIPANT_SCHEMAS = { contacts: ['platform'], organizations: [] }`
  (`CRM/modules/merges/services/persistence/merge-participants.ts:91-94`); the design says financial documents "default
  is **block** until then" (`CRM/docs/merging/relationship-coverage.md:96`;
  `CRM/docs/merging/implementation-plan.md:79-80, 314-315`), but since `accounting` is neither registered nor required,
  a contact merge today completes **without touching or blocking on Accounting rows**: the merged-away contact's
  `bill_to_id`s are left pointing at a deleted CRM row (the aging snapshot already anticipates "merged away or erased"
  names, §4). Same for `customer_terms`, `stripe_customers`, `payer_id`, `remitter_id`.
- **Delete impact is CRM-internal.** `listContactDeleteImpacts` counts CRM-owned rows and spouse references only
  (`CRM/modules/data-jobs/services/operations/contact-delete.ts:14-17`; `CONTACT_CASCADE_NAMES` at
  `CRM/modules/entities/services/persistence/contacts.ts:1273-1279`). Nothing asks Accounting (or Commerce) whether the
  party holds invoices. Relevant existing CRM facts: `contacts.spouse_id` (self-referencing FK,
  `CRM/modules/entities/db/contacts.table.ts:101, 125, 142`), `date_of_birth` (`:105`),
  `do_not_mail/do_not_call/email_opt_out` (`:116-118`); on merge, spouse is a scalar choice and the opt-outs are OR-ed
  (`CRM/docs/merging/conflict-resolution.md:71, 74`).
- Build state for context: `ACC/docs/milestones/README.md:5-9, 22-35` at this sha lists ACCT-M1 "Changes Requested" and
  M2 "Proposed"; the memory notes record M1 Product Accepted (2026-10-20 18:31 ET) and M3 work packages landing (commit
  `17cca211` "ACCT-M3 WP-C landed, WP-D landed, WP-E"), so the README lags the build.

---

### COMMERCE

#### 6. Orders, carts, promotions — Implemented (the party vocabulary) / Decided (purchasing subject)

- **Party vocabulary:** `PARTY_KINDS = ['contact', 'organization']`
  (`COM/node_modules/@rhythmsoftware/macroservice-commerce-contracts/dist/fields.js:81`).
- **Buyer ≠ bill-to ≠ recipient — Decided (Andrew 2026-10-17), Implemented.** The checkout and order carry three
  parties: `buyer_kind/buyer_id` = the **purchasing subject** ("the contact buying for themselves or the organization
  bought for, named explicitly … never inferred from the bill-to"), `acting_contact_id` (the person who acted),
  `bill_to_kind/bill_to_id` (NOT NULL), optional `ship_to_kind/ship_to_id` (both-or-neither CHECK), with
  `billing_address`/`shipping_address` frozen JSON (`COM/modules/checkout/db/checkouts.table.ts:21-25, 67-74, 97-101`;
  `COM/modules/orders/db/orders.table.ts:30-34, 138-144`). "The bill-to may differ from the subject and never decides
  whose rules apply" (`checkouts.table.ts:23`;
  `PLAT/commerce-orders/18-pricing-engine-and-extension-points.md:167-172`).
- **"Purchased for" / gift purchases: the per-line recipient.** `checkout_items.recipient_kind/recipient_id` and
  `order_lines.recipient_kind/recipient_id` (nullable pair, CHECK both-or-neither and `IN ('contact','organization')`)
  (`COM/modules/checkout/db/checkout-items.table.ts:81-82, 110-111`;
  `COM/modules/orders/db/order-lines.table.ts:82-83, 121-122, 135`). This is 1.0's "buy for someone else" made explicit
  (`PLAT/commerce-orders/09-ui-architecture.md:20`). There is no separate "gift" flag or gift-giver concept: a gift is
  simply buyer ≠ recipient. The **entitlement holder** is "the line's recipient, else the order's purchasing subject"
  (`COM/modules/entitlements/db/entitlements.table.ts:4, 62-63`).
- **Line "beneficiary"** is the Billing/Fundraising word for the same recipient (`BILL/08-consolidated-billing.md:27`).
- **Whose rules apply (member pricing, eligibility):** the pricing customer is the purchasing subject, or the line's
  recipient when the product says `pricing_subject: recipient` (an event registration priced by the attendee's
  membership) (`18-pricing-engine….md:180-186, 246-249`; `PLAT/member-pricing/04-domain-model.md:79-82`). Member pricing
  "comes from the subject's membership: an organization's membership gives organization member pricing, a contact's
  gives theirs" (`18:175-178`). **No family/household membership path exists**: a spouse or child gets member pricing
  only if they are themselves the subject or recipient with their own eligibility.
- **Promotion eligibility by member status:**
  `promotions.eligibility_subject ∈ purchaser | recipient | purchasing_organization | actor_contact`
  (`COM/modules/promotions/db/promotions.table.ts:52`;
  `PLAT/pricing-promotions/rhythm-2-coupons-pricing-promotions-architecture.md:430, 971-978`); per-customer limits count
  per purchaser (contact or organization) (`:432, 510`); redemptions store `purchaser_kind/id` and the evaluated subject
  (`COM/modules/promotions/db/promotion-redemptions.table.ts:11-12, 61`). Anonymous: "Unknown is not eligible … 'Sign in
  to use this code'" (`:982`).
- **Cart identity, anonymous vs signed-in:** a checkout REQUIRES a buyer (`buyer_kind/buyer_id NOT NULL`,
  `checkouts.table.ts:67-68`); "Portal 'my cart' is the buyer's single open checkout per store" (`:34`). Anonymous
  visitors get a **public quote** only ("Adding to a cart requires signing in; the quote does not", `18:187-191`). Note
  the Events proposal assumes `checkouts.buyer_kind ∈ contact | provisional_contact | anonymous`
  (`PLAT/events-architecture/25-public-registration-and-identity.md:49`), which Commerce's enum does not have — an open
  seam.
- **Who may buy on behalf of an organization:** CRM organization roles named on the store/product
  (`organization_purchasing_role_ids`); empty = any active affiliate (`18:192-197`). There is no equivalent "who may buy
  on behalf of another contact" rule in Commerce; Events adds one for registrations (§7).
- **Bill-to choices at checkout (payment methods & collection, Decided 2026-10-18, partly built as WP-01/04/05):** the
  collection option's `bill_to_options ⊆ {self, organization, parent_organization}` and
  `remitter_rule ∈ {bill_to, employer, primary_organization, parent_organization}`
  (`PLAT/payment-methods-and-collection/…architecture.md:451-452`); §4.5 "Bill-to is the debtor … For `organization`, it
  is the organization from the buyer's primary CRM affiliation … For `parent_organization`, that organization's
  `parent_organization_id`"; expected remitter and payer are separate facts (`:503-515`); remittances §4.6 (`:517-527`).
  The arrangement is frozen on the order (`:473`). These are the only party-resolution rules that exist; a household
  would be a new `bill_to_option` and/or `remitter_rule` value.
- **Group purchases:** no Commerce concept beyond many lines each with a recipient; group discounts are Commerce's by
  Events D-87 (`PLAT/events/12-decisions.md:960`); promotions evaluate per line (`pricing-promotions…:1392`, J3).
  Billing's consolidation handles many members' dues onto one organization invoice (§10).
- **Licensing proposal:** "Billing accounts | keyed by customer | out of this document | they belong to the deferred
  management-companies design" (`PLAT/licensing-entitlements-billing/README.md:112`) — Absent.

---

### EVENTS (proposal state; nothing built)

#### 7. Registration — Planned (package approved 2026-10-20 with decisions carried as recommended; `PLAT/events/12-decisions.md:3`)

- **Three roles defined, two stored.** "registrant completes … (a coordinator, a parent, a group administrator) …
  attendee is the registration's holder … buyer pays" (`PLAT/events/10-domain-model-and-integration.md:239`, glossary
  `:119`). Stored: `registrations.attendee_contact_id` (nullable) + required `attendee_snapshot`
  (`PLAT/events-architecture/06-domain-model.md:76, 90`; `PLAT/events/04-registration.md:369`); the payer is Commerce's
  `buyer` and `bill_to` on the order; the registration stores only `order_id`, `order_line_id`
  (`06-domain-model.md:77`). **No `registered_by` column** (Absent); the registrant appears only as history actor
  (`04-registration.md:424`). **Who is the Accounting bill-to for a registration is never stated** (Absent); the staff
  sheet offers "pay now, invoice or comp" (`04:170`).
- **Group registration:** one checkout, one registration per person, `group_id`; admins add people from the
  organisation's CRM affiliations or by invitation link, "never any tenant contact"; organisation-billed with a Commerce
  recipient per item; `registration_groups.billing_organization_id` (`04-registration.md:162-164, 373, 606`, `:44`).
  Attendees are an existing CRM contact, a new provisional contact, or a name-only snapshot; CRM match only on exact
  unclaimed primary e-mail (`25-public-registration-and-identity.md:131-134`).
- **Registering someone else:** "a parent registering a child is one order, one line, attendee ≠ buyer"
  (`06-domain-model.md:77`, the only parent/child mention); spouse appears only as a guest type (`04:42`); authority
  rule "organisation role, delegate or invitation" (`04:169`); "I am registering someone else" switch
  (`25:116-117, 198`); guests are full registrations with `host_registration_id`, cancelled with the host (`04:167`;
  D-85 `12-decisions.md:948`); a guest "never shares an e-mail with the host" (`04:467`). **household / family /
  dependent / guardian: Absent** (whole-tree grep).
- **Waivers and consent:** `registration_consents(registration_id, kind, granted, disclosure_version, …)`, kinds lead
  sharing, networking directory, marketing (`04:380, 156`); **no column for who gave the consent** (Absent). D-26: lead
  consent is per attendee (`12-decisions.md:378`) vs the public checkout collecting "purchaser-level questions,
  consents" once (`25:120`) — unresolved. **Liability waivers, guardian consent, age, date of birth: Absent** ("waiver"
  = fee waiver only, `12:894`).
- **Eligibility / member rates:** Eligibility Engine binding, decision id frozen on the line (D-66,
  `12-decisions.md:748-760`; `04:573`); "The purchaser's eligibility prices the purchaser's own registration; each
  attendee's member price requires that attendee's eligibility", or `member_of_organization` prices every attendee of a
  member organisation (`25-public-registration-and-identity.md:94`); member verification needs "exactly one unclaimed
  contact" on the e-mail (`25:84, 132`), which a shared household mailbox defeats (`25:133`). **Family membership
  conferring rates: Absent.**
- **Check-in / badges:** attendance rows on `registration_id`, badge on the registration, credentials on
  `entitlement_grants.holder_registration_id` (`06-domain-model.md:121`; `10:246`; `36-domain-model-additions.md:125`).

---

### MARKETING (proposal state; `services-marketing` is a README)

#### 8. Recipients, consent, subscriptions — Decided (ownership) / Planned (shape) / Implemented (legacy CRM columns)

- **A recipient is always one CRM contact.** `send_recipients` keyed on `contact_id`, unique
  `(tenant_id, send_id, contact_id, record_key)` (`MKT/appendix-a-schema.md:33`; per-channel
  `MKT/appendix-c-multichannel-schema.md:65`). An organization "is not" a recipient
  (`MKT/04-targeting-and-recipient-resolution.md:29`); roles are a way of finding contacts (`:47`); an unmatched address
  is never stored (`MKT/24-communication-preferences-and-consent.md:78`; forms create a contact,
  `MKT/09-landing-pages-and-forms.md:21-24`).
- **Consent ledger: CRM owns it — Decided D-M2, Andrew 2026-10-20** ("subscription types, subscriptions, consent
  history, global unsubscribe incl. `email_opt_out` lives in services-crm; Marketing reads and writes it through CRM
  capability functions"), superseding chapters 08/20/24 and appendices on ownership (`MKT/decisions-2026-10-20.md:9`;
  background `MKT/16-risks-assumptions-and-decisions.md:42`, `MKT/31-risks-and-decisions-multichannel.md:34`). **No CRM
  table design exists yet** (Absent); the written designs are still `marketing.*` / `platform.communication_*`
  (`appendix-a:57-61`, `appendix-c:37-47`).
- **Planned shape:** subscriptions per contact × subscription type with a status per channel (`24:23`, `appendix-c:41`);
  e-mail consent per contact with the address as evidence (`24:30`; `MKT/08-preferences-and-consent.md:42`); SMS consent
  per phone number, push per installation (`24:30`); channel-consent rows unique per `(channel, destination, scope)` and
  "the row's contact is the one who last acted" for a shared destination (`24:22`; `appendix-c:39`); global opt-out type
  `all` (`08:23`, `24:37`); address-level unsubscribes by `(channel, address, type)` (`08:37`, `24:26`); shared
  suppression list keyed `(channel, address)` for bounces/complaints, no bypass (`08:41`, `04:63`, `appendix-c:13`).
- **Implemented, legacy, "stored; enforced nowhere":** `crm.contact_communication_preferences` (per contact per
  communication type; `subscription_status` subscribed/digest/unsubscribed;
  `CRM/modules/entities/db/contact-communication-preferences.table.ts:1-6, 19-34`, slated for retirement `MKT/08:7, 9`);
  contact flags `do_not_mail`, `do_not_call`, `email_opt_out` (`CRM/modules/entities/db/contacts.table.ts:116-118`);
  `CRM/docs/compliance/control-coverage.md:148`.
- **Direct mail / householding: Absent.** Print is "not designed" (`MKT/20-multichannel-architecture.md:48`; `31:58`;
  R-17 `MKT/18-customer-evidence-and-requirements.md:180-182`); nothing reads `do_not_mail` (`08:7`). "Household" occurs
  once, as a shared phone example (`24:72`: one STOP stops every contact on the number). The nearest e-mail rule: people
  sharing a mailbox each get their own e-mail, with an optional per-send "Send once per address (to the first person by
  name)", off by default, others collapsed as `shared_address_collapsed` (`04:80, 118`; `shared_address_rule`,
  `appendix-a:15`).
- **Salutation / merge fields:** `contact.*` (every readable `crm.contacts` field), `record`, `organization` (the
  contact's primary organization), `association`, `message`, `links.*`, `function.<key>`
  (`MKT/07-authoring-and-delivery.md:23, 25`). CRM has `prefix`, `salutation`, `first_name`, `nickname`, `suffix`,
  `credentials`, computed `name`/`full_name`/`name_last_first` (`CRM/modules/entities/db/contacts.table.ts:65-80`).
  **Formal/informal salutation, joint salutation, household name: Absent.**
- **Audience resolution pattern (what a household audience would copy):** recipient relationships declared by the owning
  service, four kinds: `self`, `reference`, `organization_role` (current affiliations only), `organization_affiliates`
  (`04:41-48`); an organization with no role holder contributes no recipient (`04:116`). No "primary contact" resolver.
- **Minors / age gating: Absent.**

---

### FUNDRAISING (proposal state; nothing built)

#### 9. Gift model — Decided in the proposal (ADRs "recommended", treated as taken per `FUND/30-risks-and-open-decisions.md:165-172`)

- **Donor of record = one CRM party.** Shared three-column party shape with
  `CHECK (party_kind IN ('contact','organization'))` and exactly-one-id constraint (`FUND/08-data-model.md:83-92`);
  `gifts.donor_kind NOT NULL, donor_contact_id, donor_organization_id` with `gifts_donor_one` (`08:208, 245`); frozen
  once recorded, changed only by reversal + new gift (`08:256`; ADR-11 `FUND/28-decision-records.md:129-139`). No
  fundraising constituent/household table: `donor_profiles` is "one per constituent" = CRM party (`08:536`;
  `FUND/07-domain-architecture-and-ownership.md:11`). "Legal donor" is used only for donor-advised funds
  (`FUND/09-gift-management.md:99-101`). **Household rule: "the donor is the person on the check. Household members
  receive soft credits by rule"** (`09:103`).
- **Payer ≠ donor:** `payer_kind/payer_contact_id/payer_organization_id`,
  `payer_reason ∈ donor_advised_fund | employer | household | agent | other` (`08:209-210`). The payer is the Accounting
  bill-to: the direct path posts `POST /settlements` with customer "the PAYER (defaults to the donor)" (`09:54-70, 96`);
  each allocation is one Accounting invoice line (`08:290`; ADR-02 `28:19-29`); "The donor sees a receipt, never an
  invoice" (`FUND/11-recurring-giving.md:62`). A donor correction needs no Accounting change when the payer is unchanged
  (`09:180`).
- **Soft / recognition credits:** `gift_credits.credit_kind ∈ hard | soft | fundraiser | team`, recipient a contact or
  organization only, reasons include `household` (`08:299-317`); exactly one hard credit per gift to the donor; soft
  credits unbounded ("Three household members may each receive recognition for the full amount") and never in financial
  totals (`08:322-323`; `09:130`); written once at recording (`09:156`); generated from settings
  `household_relationship_type_ids`, `household_credit_mode (none | soft_credit)`, spouse-only today (`08:184`;
  `09:151`).
- **Household giving rollups:** "Σ over the household key, deduplicated by gift id"
  (`FUND/24-analytics-and-semantic-queries.md:47`; basis `household` `:23`; annual snapshot `:82, 106`); giving
  societies `basis: household` (`FUND/17-recognition-stewardship-and-corporate-giving.md:33`; `08:552, 758-760`); **the
  household key is computed from spouse pairs and stored nowhere** (`17:41`). Gaps: `household_recognition_name` /
  `household_opt_out` named at `17:45` but absent from `donor_profiles` (`08:536`); statements `group_by: household`
  (`17:43, 78`; `FUND/23-console-and-portal-experience.md:132`) while `statement_runs.basis` allows only hard /
  hard_and_soft and `giving_statements` holds one party (`08:555-556`).
- **Tribute:** `gift_tributes` honoree (text + optional contact) and
  `notify_contact_id / notify_name / email / address`, distinct from the donor (`08:335`); acknowledgment rows have
  their own `recipient_*`, kind `tribute_notice` (`08:375`); notified contact may get a `tribute` soft credit
  (`09:152`).
- **Tax receipt:** an `acknowledgments` row of kind `receipt`; `gift_credit_id NULL` = the donor (`08:375`); soft-credit
  recipients "never" receive a receipt (`09:208`); template sees donor and payer (`09:209`); tied to the gift
  (`accounting_settlement_id` `08:233`), not to an Accounting document. **No joint/household receipt** — yet the API
  example sets `recognition_name: "Ann and Hal Lee"` on a single-donor gift (`FUND/20-api-contracts.md:43`).
- **Pledges / recurring:** pledges `donor_*` + optional `payer_*`, payer changeable by amendment (`08:401-402, 458`;
  `FUND/10-pledges-and-commitments.md:125`); installments have no payer columns (`08:436-449`); recurring plans
  `donor_*` only, payer effectively the stored method's owner (`08:470, 503-505`). Portal access: "donor, payer or
  owner" (`FUND/22-security-and-permissions.md:86`), nothing for a spouse.
- **Anonymous:** flag on a real donor (`gifts.is_anonymous` `08:211`; pledges `:411`; plans `:477`; `prefers_anonymous`
  `:536`), donor kept for receipt/audit (`09:105`); unknown givers go to a configured "Anonymous Donor" contact
  (`09:105`); per-credit `is_anonymous` (`08:310`) — nothing says a household soft credit inherits the donor's
  anonymity.
- **28-decision-records.md:** ADR-16 "Household recognition is computed from configured CRM relationship types and
  stored as soft credits; no household table" — tradeoff "Only the spouse pair works today" (`28:189-199`); ADR-04
  credits (`28:43-53`); ADR-11 immutability because receipts may be issued (`28:129-139`); ADR-02 money is an Accounting
  line + settlement (`28:19-29`); open D-08 "Ask CRM for contact relationships" (`FUND/30:137-151`). No ADR on donor of
  record, joint gifts, receipts or anonymity.

---

### BILLING (proposal state; 13 decisions RULED 2026-10-20; nothing built)

#### 10. What Billing owns; no billing account — Decided

- **Ownership:** Billing = its own licensed application and `services-billing` (D-02 `BILL/13-decisions.md:8, 39`; C-1
  `:261`), framework on Durable Jobs "NOT the Data Job Engine" (D-03 `:9, 55`); Billing owns billing runs, schedules
  incl. installment plans (moved from Accounting, C-2 `:262`), consolidation, financial communications, statements,
  dunning/collections (`BILL/01-executive-recommendation.md:11-17`; D-01/D-08/D-10 `13:7, 14, 16`); Accounting keeps
  "invoices, payments, credits, write-offs, aging or the customer's balance" (`01:19`) and the stored methods + tender
  (D-12 `13:197`). Ownership table `BILL/09-statements-and-communications.md:63-74`.
- **Billing account / responsible party / guarantor: Absent.** None of the terms appears; the data model lists runs,
  notices, plans, statements, collections only, run tables gaining `payer_*` columns
  (`BILL/06-proposed-architecture.md:169-177`). "The order's bill-to is the **payer**; each line's recipient is the
  **beneficiary**" and the payer comes from the obligation source's `confirm`: "the holding's owner by default; the
  organization when the holding or the package says the organization pays" (`BILL/08-consolidated-billing.md:27`);
  Billing places the order with `bill_to` (`06:137`); the Billing 360 card sits on the CRM contact/organization
  (`BILL/07-console-experience.md:32`).
- **Consolidated billing (D-06, BILL-M4):** hard separators payer (`bill_to`), currency, terms, pro forma, tender, date;
  default "consolidate per payer" (`08:19, 23`); many members' dues onto one organization's invoice ("an employer, a
  chapter, a parent company", `08:75`; `BILL/12-milestones.md:88-91`); optional split "by beneficiary" (`08:21`); no
  parent-over-child organization rollup; the only "Family" model is a market note on Novi
  (`BILL/evidence/30-market-ams-platforms.md:179`).
- **Statements (D-08):** recipients "the customer's billing contact (a contact's own address; an organization's contacts
  by the template's roles), resolved through … `bill_to_contact`, `organization_billing_contact`" (`09:17`); "one
  statement per customer with a balance" (`12:60`) reading Accounting's per-customer summary (`09:13`). Whether a
  contact's statement folds the organization's items is unstated.
- **Dunning / collections (D-10):** "one message per payer (the customer billed, a contact or an organization's billing
  contact)" (`09:27`); order notices to "the payer's communication identity" with optional "tell the beneficiary too"
  (`08:27`); collections case BILL-M6 (`09:33`); no mechanism for a responsible party other than the bill-to (Absent).
- **Stored payment methods (D-12):** "owner (contact or organization)" (`BILL/15-stored-payment-methods.md:13`), routes
  by `?owner=` (`:49`), default per owner and purpose (`:34`); whether one person's card may pay another's invoice is
  unstated.

---

### (a) Every place a person or organization is referenced as a money party

Kind column values are `contact | organization` everywhere unless noted. "FK target" is the logical target; **no table
has a physical foreign key into `crm.*`** (Platform's no-cross-schema-key rule).

| Table.column(s)                                                                                                       | Logical target                                | Kind / role                                                     | State                           | Cite                                                                                                                                                     |
| --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accounting.invoices.bill_to_kind, bill_to_id, bill_to_name, billing_address`                                         | crm.contacts / crm.organizations              | debtor (customer)                                               | Implemented                     | `ACC/modules/transactions/db/invoices.table.ts:122-125, 206`                                                                                             |
| `accounting.invoices.ship_to_address` (jsonb)                                                                         | — (address only)                              | tax situs for shipped lines                                     | Implemented                     | `invoices.table.ts:126`                                                                                                                                  |
| `accounting.invoices.collection_remitter_kind/id` (generated from `custom_metadata.collection.remitter`)              | CRM party                                     | expected remitter                                               | Implemented                     | `invoices.table.ts:35-42`                                                                                                                                |
| `accounting.payments.bill_to_kind/id/name`                                                                            | CRM party                                     | customer the money is held for                                  | Implemented                     | `payments.table.ts:73-75`                                                                                                                                |
| `accounting.payments.payer_kind/id/name` (nullable, both-or-neither; name alone allowed)                              | CRM party or free name                        | who sent the money (offline only)                               | Implemented                     | `payments.table.ts:76-78, 147-148`                                                                                                                       |
| `accounting.payment_intents.bill_to_*`                                                                                | CRM party                                     | processor payment's customer                                    | Implemented                     | `payment-intents.table.ts:62-64, 98`                                                                                                                     |
| `accounting.settlements.bill_to_*`                                                                                    | CRM party                                     | tender's customer                                               | Implemented                     | `settlements.table.ts:56-58, 92`                                                                                                                         |
| `accounting.credits.bill_to_*`                                                                                        | CRM party                                     | store-credit owner                                              | Implemented                     | `credits.table.ts:57-59, 107`                                                                                                                            |
| `accounting.credit_applications.bill_to_*`                                                                            | CRM party                                     | whose credit was applied                                        | Implemented                     | `credit-applications.table.ts:48-49, 81`                                                                                                                 |
| `accounting.credit_memos / debit_memos / write_offs / reversals / payment_reversals .bill_to_*`                       | CRM party                                     | document's customer                                             | Implemented                     | `credit-memos.table.ts:38-39`; `debit-memos.table.ts:37-38`; `write-offs.table.ts:42-43`; `reversals.table.ts:74-75`; `payment-reversals.table.ts:47-48` |
| `accounting.refunds.bill_to_*`                                                                                        | CRM party                                     | refund's customer (no payee column)                             | Implemented                     | `refunds.table.ts:81-83, 136`                                                                                                                            |
| `accounting.remittances.remitter_kind/id/name`                                                                        | CRM party                                     | one receipt paying others' invoices (to be retired)             | Implemented / Decided to retire | `remittances.table.ts:14-16`; `ACC/docs/debt/payment-model-one-receipt-many-customers.md`                                                                |
| `accounting.remittance_lines.bill_to_*`                                                                               | CRM party                                     | the invoice's debtor per line                                   | Implemented                     | `remittance-lines.table.ts:49-50`                                                                                                                        |
| `accounting.receivable_events.bill_to_*`                                                                              | CRM party                                     | aging/summary ledger attribution                                | Implemented                     | `receivable-events.table.ts:100-101`                                                                                                                     |
| `accounting.ar_snapshot_items.customer_kind, customer_id, customer_name`                                              | CRM party (name captured)                     | month-end aging                                                 | Implemented                     | `ar-snapshot-items.table.ts:74, 87`                                                                                                                      |
| `accounting.customer_terms.party_kind, party_id`                                                                      | CRM party                                     | collection permissions, default terms                           | Implemented                     | `customer-terms.table.ts` header; `customers.md:63-80`                                                                                                   |
| `accounting.stripe_customers.contact_id -> customer_id`                                                               | crm.contacts only                             | processor customer / vault                                      | Implemented                     | `stripe-customers.table.ts:17-21`                                                                                                                        |
| `accounting.installment_schedules.bill_to_*, billing_address`                                                         | CRM party                                     | payment-plan owner                                              | Implemented                     | `installment-schedules.table.ts:70-72`                                                                                                                   |
| `accounting.subledger_entries.customer_type, contact_id, organization_id`                                             | CRM party                                     | GL subledger dimension                                          | Implemented                     | `subledger-entries.table.ts:76-78`                                                                                                                       |
| `accounting.revenue_recognition_schedules.customer_type, contact_id, organization_id`                                 | CRM party                                     | deferred-revenue dimension                                      | Implemented                     | `revenue-recognition-schedules.table.ts:66-68`                                                                                                           |
| `accounting.document_templates.send_to, contact_role_ids` (statement)                                                 | CRM contact roles                             | statement recipients                                            | Implemented                     | `document-templates.table.ts:86-87`                                                                                                                      |
| `accounting.billing_notices.recipients, contact_role_ids`                                                             | target party or role holders                  | dunning/notice recipients                                       | Implemented                     | `billing-notices.table.ts:54-72`                                                                                                                         |
| query views `customer_contact_id`, `customer_organization_id`                                                         | crm.contacts / crm.organizations              | entity-aware link                                               | Implemented                     | `customer-names.ts:40-55`                                                                                                                                |
| `commerce.checkouts.buyer_kind/id`                                                                                    | CRM party                                     | purchasing subject (whose rules)                                | Implemented / Decided           | `COM/modules/checkout/db/checkouts.table.ts:67-68`                                                                                                       |
| `commerce.checkouts.acting_contact_id`                                                                                | crm.contacts                                  | who acted                                                       | Implemented                     | `checkouts.table.ts:69` region, `orders.table.ts:140`                                                                                                    |
| `commerce.checkouts.bill_to_kind/id`                                                                                  | CRM party                                     | debtor for the order's invoice                                  | Implemented                     | `checkouts.table.ts:71-72`                                                                                                                               |
| `commerce.checkouts.ship_to_kind/id` (nullable)                                                                       | CRM party                                     | ship-to party                                                   | Implemented                     | `checkouts.table.ts:73-74, 100-101`                                                                                                                      |
| `commerce.checkout_items.recipient_kind/id` (nullable)                                                                | CRM party                                     | purchased-for / beneficiary                                     | Implemented                     | `checkout-items.table.ts:81-82, 110-111`                                                                                                                 |
| `commerce.orders.buyer_*, acting_contact_id, acting_user_id, bill_to_*, ship_to_*, billing_address, shipping_address` | CRM parties / platform.users                  | frozen at placement                                             | Implemented                     | `orders.table.ts:138-144`                                                                                                                                |
| `commerce.order_lines.recipient_kind/id`                                                                              | CRM party                                     | line beneficiary                                                | Implemented                     | `order-lines.table.ts:82-83, 121-122`                                                                                                                    |
| `commerce.entitlements.holder_kind/id`                                                                                | CRM party                                     | right holder (recipient else buyer)                             | Implemented                     | `entitlements.table.ts:4, 62-63`                                                                                                                         |
| `commerce.promotion_redemptions.purchaser_kind/id, organization_id, subject_kind/id`                                  | CRM party                                     | limit counting, eligibility subject                             | Implemented                     | `promotion-redemptions.table.ts:11-12, 61`                                                                                                               |
| `commerce.promotions.eligibility_subject`                                                                             | role selector                                 | purchaser / recipient / purchasing_organization / actor_contact | Implemented                     | `promotions.table.ts:52`                                                                                                                                 |
| `commerce.order_collection_arrangements` bill_to, remitter (planned table)                                            | CRM party                                     | debtor + expected remitter                                      | Decided, partly built           | `PLAT/payment-methods-and-collection/…:473, 536`                                                                                                         |
| `fundraising.gifts.donor_kind, donor_contact_id, donor_organization_id`                                               | CRM party (XOR)                               | donor of record                                                 | Planned (Decided in proposal)   | `FUND/08-data-model.md:208, 245`                                                                                                                         |
| `fundraising.gifts.payer_kind, payer_contact_id, payer_organization_id, payer_reason`                                 | CRM party                                     | Accounting bill-to for the gift                                 | Planned                         | `FUND/08:209-210`; `09:96`                                                                                                                               |
| `fundraising.gift_credits` recipient party, `credit_kind`, `reason` (incl. `household`)                               | CRM party                                     | hard/soft recognition                                           | Planned                         | `FUND/08:299-317`                                                                                                                                        |
| `fundraising.gift_tributes.notify_contact_id / notify_name / email / address`                                         | CRM contact or free text                      | tribute notice recipient                                        | Planned                         | `FUND/08:335`                                                                                                                                            |
| `fundraising.acknowledgments.recipient_*` (kind receipt / thank-you / tribute_notice)                                 | CRM party                                     | receipt recipient                                               | Planned                         | `FUND/08:375`                                                                                                                                            |
| `fundraising.pledges.donor_*, payer_*`; `recurring_plans.donor_*`                                                     | CRM party                                     | commitment owner / payer                                        | Planned                         | `FUND/08:401-402, 470`                                                                                                                                   |
| `marketing.send_recipients.contact_id`                                                                                | crm.contacts only                             | message recipient                                               | Planned                         | `MKT/appendix-a-schema.md:33`                                                                                                                            |
| CRM-owned consent/subscription ledger (shape TBD)                                                                     | crm.contacts (+ address/phone as destination) | consenting party                                                | Decided D-M2, Absent in code    | `MKT/decisions-2026-10-20.md:9`                                                                                                                          |
| `crm.contact_communication_preferences.contact_id`; `crm.contacts.do_not_mail/do_not_call/email_opt_out`              | crm.contacts                                  | legacy preferences, unenforced                                  | Implemented                     | `CRM/…/contact-communication-preferences.table.ts:19-34`; `contacts.table.ts:116-118`                                                                    |
| events `registrations.attendee_contact_id` + `attendee_snapshot`; `registration_groups.billing_organization_id`       | crm.contacts / crm.organizations              | attendee; group payer                                           | Planned                         | `PLAT/events-architecture/06-domain-model.md:76-77`; `PLAT/events/04-registration.md:373`                                                                |
| billing run tables `payer_*`; stored methods `owner`                                                                  | CRM party                                     | billed party; card owner                                        | Planned (D-06, D-12)            | `BILL/06:171`; `BILL/15:13`                                                                                                                              |

### (b) Where a household could appear as a party without ambiguity

The closed `contact | organization` vocabulary is enforced in roughly twenty CHECK constraints, two contracts packages,
the customer-name view joins, the CRM-party reads (tax exempt, preferred address, primary e-mail), the Stripe mapping,
the aging snapshot capture, the merge policy and every proposal's party shape. Adding a third kind is therefore not a
column change but a product-wide vocabulary change. Given that, the assessment:

1. **Never a money party in release 1: not bill-to, not payer, not remitter, not donor of record, not credit owner, not
   refund payee, not stored-method owner, not buyer.** Each of these resolves one party's balance, receipt, tax position
   or portal access, and every existing rule assumes that party is a CRM contact or organization that can receive
   e-mail, hold a Stripe customer, be tax-exempt (organizations) and be merged. The planned "one receipt, many
   customers" milestone already gives a household what it needs operationally: one spouse pays, the payment's `bill_to`
   is that spouse, allocations land on each member's invoices, and each member's statement attributes the allocation
   "paid by X" (`ACCT-M2-plan.md:42-50`). The payer stays a person.
2. **Safe as a resolution rule, not a kind.** If a household product must be billed as one (family dues), the invoice's
   bill-to should be a designated **household billing contact** — one CRM contact — chosen by a rule, exactly as "bill
   my organization" resolves to the primary affiliation's organization and "bill my parent organization" to its parent
   (`PLAT/payment-methods-and-collection/…:451-452, 507`). Concretely: a new `bill_to_options` value (e.g.
   `household_billing_contact`) and a `remitter_rule` value, both resolving through CRM household membership to a
   contact. The household id can travel in the order's `selling_context` / invoice `custom_metadata` (the `collection`
   precedent, `invoices.table.ts:35-42`) and in `order_kind` for reporting, which the ledger never reads. Nothing in
   Accounting changes.
3. **Safe as a beneficiary/recipient grouping.** Commerce's per-line `recipient` already lets one buyer purchase for
   several people; a household is a convenient way to populate recipients (and Events attendees) from CRM, and Billing's
   consolidation "per payer, optional split by beneficiary" (`BILL/08:19-21`) already produces one invoice to the paying
   member with lines per family member. No new party kind needed.
4. **Safe as a recognition and reporting basis.** Fundraising already has the pattern (ADR-16): hard credit to the
   donor, household soft credits by rule, household totals computed as a key and de-duplicated by gift id. A Households
   proposal should replace "spouse pair" with "household membership as of the gift date" as the key source (`FUND/17:41`
   says the key is stored nowhere, so this is a pure substitution) and close the three gaps:
   `household_recognition_name`/`household_opt_out` missing from `donor_profiles`; statement `group_by: household` vs
   single-party `giving_statements`; soft credits not inheriting `is_anonymous`. Tax receipts stay per donor of record
   (`09:208`); a "joint" name is `recognition_name` text, never a second receipt entity.
5. **Safe as an Accounting read-side rollup, never a ledger attribute.** A household 360 card can sum the per-member
   customer summaries (the same as-of function, per member), the way the portal payable description folds an
   organization's invoices into a contact's (`invoices.md:31`). `receivable_events`, `ar_snapshot_items` and the
   subledger keep the member as the party; the household is a grouping column at query time (CRM membership as of the
   period end), not a captured dimension.
6. **Safe as a Marketing recipient-relationship kind and a householding rule, not as a consenting party.** Consent is
   per contact (D-M2); a shared e-mail or phone records the contact who last acted (`MKT/24:22`), so a household must
   never be the subject of a consent row. A household becomes (i) a fifth declared relationship kind ("household
   members", "household head") beside `organization_role`/`organization_affiliates` (`MKT/04:41-48`), and (ii) a
   print/e-mail de-duplication rule ("one per household, to the head"), generalising the existing per-send "Send once
   per address" toggle (`MKT/04:80`). The joint salutation becomes a CRM composed property on the household, exposed as
   a merge field; today no such field exists.
7. **Events:** the household supplies attendees and a default `registered_by`; the payer stays Commerce's buyer. The
   proposal has no guardian/minor concept, so a household proposal that introduces dependents must also add the consent
   signer on `registration_consents` (absent today, `04:380`) and an age source (`crm.contacts.date_of_birth` exists,
   `contacts.table.ts:105`).
8. **The alternative the code would accept without change — modelling a household as a CRM organization (an organization
   type "Household") — fits every party column today but overloads organization semantics**: `tax_exempt` read at issue,
   affiliations typed `employer | school | other`, organization purchasing roles, "bill my organization" resolving to
   the primary affiliation, the organization-member pricing path, and Billing's "organization pays" default. It would
   make a household a first-class bill-to and payer overnight, which is exactly the ambiguity the proposal is meant to
   avoid. Not recommended without a decision from Andrew.

### (c) Questions the code and proposals could not answer

1. **Family membership dues:** who is the bill-to — a designated member contact, or the household itself? Nothing in
   Membership or Billing names the household; Billing's `confirm` rule knows only "holding's owner" or "the
   organization".
2. **Is a household a new CRM object type, or an organization type?** Determines whether `BILL_TO_KINDS` / `PARTY_KINDS`
   stay closed (recommended) or gain a value (touches ~20 CHECKs, two contracts packages, views, snapshot capture, merge
   policy).
3. **Joint gifts and receipts:** the Fundraising API example names "Ann and Hal Lee" on a single-donor gift; whether a
   jurisdiction's joint receipt (married filing jointly) is a product requirement is unstated.
4. **Statements per household:** Billing says one statement per customer; Fundraising plans `group_by: household` giving
   statements. Should A/R statements ever group by household, and who is the addressee?
5. **Consent attribution on shared destinations:** for a shared mailbox/phone the ledger records the last actor; whether
   a household head may consent for dependents (and under what age) is undecided everywhere.
6. **Minors:** no age rule, guardian consent or waiver signer exists in Events or Marketing; `date_of_birth` is stored
   in CRM but read by none of these services.
7. **Events vs Commerce buyer kinds:** Events assumes `provisional_contact | anonymous` buyers; Commerce requires a
   contact/organization buyer. Which one yields decides whether a household can register before its members are CRM
   contacts.
8. **Merge and dissolution:** Accounting's merge participant is not built, and CRM's engine neither blocks on nor
   reassigns Accounting rows; household merges, splits (divorce) and member moves will inherit this gap. Who owns the
   Accounting participant and when (policy O-10 says F5/F6 of the Accounting plan).
9. **Stored payment methods across a household:** D-12 gives a method one owner; whether a spouse's card may pay another
   member's invoice (and show on whose 360) is unstated — it interacts with the processor-payment rule that the payer is
   the cardholder (`payments.md:138`).
10. **Milestone state drift:** the Accounting milestones README at `17cca211` still reads M1 "Changes Requested" / M2
    "Proposed" while M3 work packages are landing; the live acceptance state should be taken from the session records,
    not this file.

## 24. R4 — Platform primitives for Households, person-to-person relationships and family memberships

## R4 — Platform primitives for Households, person-to-person relationships and family memberships

Read-only research of the Rhythm 2.0 Platform macroservice, written 2026-10-21 for the CRM Households proposal.

- Source: `/Users/andrew/rhythm2/services-platform`, branch `master` at **`0614c54cf`**. Also read, read-only, for the
  owner's side of each contract: `/Users/andrew/rhythm2/services-crm` at `eac3df30`, the shared libraries installed
  under `services-platform/node_modules/@rhythmsoftware/*` (metadata **0.25.0**, the version Platform's deployed
  registration validates with), and the CRM architecture record on the Platform branch `crm-architecture` (not on
  master; cited where a decision only exists there).
- Nothing was modified; `react-console` was not opened.
- Labels: **Implemented** (code or migration on master, or a shipped library), **Decided** (Andrew ruled it; may be
  unbuilt), **Planned** (a proposal or approved design, not built), **Absent** (nothing exists).
- Where a chapter was researched by a parallel read-only inspection, I re-verified the load-bearing citations myself;
  every `path:line` below is absolute.

### 0. The ten findings that shape the proposal

1. **Platform already has a generic, tenant-configured record-to-record relationship: `platform.association_types` /
   `platform.associations`** — any two registered objects (the same object twice is allowed: "Contact mentors Contact"),
   labelled both ways, `one_to_one | one_to_many | many_to_many`, dated, one primary per `from` record, per-side delete
   rules, merge-aware, history-aware, importable, and already surfaced into every query dataset as generated reference
   fields and `exists` collections. **Implemented**
   (`/Users/andrew/rhythm2/services-platform/docs/associations.md:3-18, 22-69`). It is the strongest candidate for
   person-to-person relationships and a serious candidate for household membership (§2.4, §11, §b).
2. **A Household itself should be a native CRM object (its own table, or an organization type)** — not a custom object.
   The custom-objects design draws that line itself: anything carrying money, credentials or transactional rules is a
   native application, never a custom object
   (`/Users/andrew/rhythm2/services-platform/docs/proposals/custom-objects/03-recommended-architecture.md:774-786`); a
   custom record cannot be merged
   (`/Users/andrew/rhythm2/services-platform/lib/record-merge/platform-references.ts:378`) nor reached by eligibility.
3. **The CRM architecture record (branch `crm-architecture`, not master) already recommends** "households are an
   organization of a system type 'Household' with member affiliations; spouse becomes a relationship type" (CRM-Q2,
   option 1, Recommended, not yet ruled) — `git show crm-architecture:docs/proposals/crm/13-decisions.md` lines 66-84.
   **Planned**.
4. **Registering a new entity is four deploy-time manifests from the owner's repo** (metadata → queries → recipes →
   extensions/eligibility), plus one Platform-side code edit for every new query permission key
   (`modules/queries/datasets.ts`) and, if Quick Search is wanted, one Platform-side edit of the closed record-type
   list. Full checklist in §a.
5. **Library schema bumps go Platform first.** Platform validates every manifest with the
   `@rhythmsoftware/macroservice-metadata` version it was built with (0.25.0 today; CRM pins 0.22.0, Membership 0.23.0).
   A new manifest key the deployed Platform does not know fails the CRM deploy (memory
   `library-schema-bump-platform-first`, the 2026-10-20 `archivedField` incident).
6. **Aggregates never double count by construction:** the Explorer refuses any `one_to_many` join; the query layer
   allows such a join for a report, documents that it repeats the base row, and offers `count_distinct`; `exists`
   collections are correlated subqueries, never joins. There is no "relationship-aware criteria" beyond `exists` (E-17
   "later" is a CRM-acceptance ruling, not a Platform doc).
7. **Delegated / "acting for" access does not exist anywhere in Platform.** Impersonation is deferred; management
   companies is DEFERRED; the only scoped-authority primitive is a _derived assignment_ (a role written for a contact at
   tenant or resource scope by a registered derivation source: `committees`, `eligibility`). Record-level security is
   implemented as a mechanism (`resourceType` keys + `requireAccess`) but CRM declares none (CRM-Q8 open, blocks
   CRM-M23).
8. **Notifications are e-mail only today**; a recipient is `{ emailAddress, contactId?, userId? }` — not necessarily a
   contact; consent is CRM's (`crm.contact_communication_preferences`); Slack/Teams are Planned (communications phase 5,
   events D-64).
9. **Imports: the Data Job Engine is real** (`defineDataOperation`, crosswalk `data_id_mappings`, groups, blocked rows,
   validate-then-apply) and `platform.associations/upsert` already imports two-parent rows by source key. Bulk merge is
   Absent. "E-11" (atomic dependency-ordered groups) is a CRM-acceptance ruling the CRM record marks **Not built** in
   the engine.
10. **Eligibility can express "age at a date" today (`crm.contact.age`) but cannot traverse to a household**: the
    subject resolver knows contact, organization, affiliation only; and the whole engine is PARKED (resume only on
    Andrew's go).

---

### 1. Entity registration

#### 1.1 The registry and what an owner declares — Implemented

Platform's metadata module keeps the registry of objects other macroservices own
(`/Users/andrew/rhythm2/services-platform/docs/metadata.md:3-16`). An object id is `<application>.<plural noun>` and
never changes (`docs/metadata.md:20-27`). Tables: `registered_objects`, `registered_fields`, `registered_collections`
(written only by the owner's registration), `custom_fields`, `standard_field_label_overrides`, `custom_field_rules`
(`docs/metadata.md:82-99`).

The owner declares an object beside its Drizzle table with `defineObject` (`docs/metadata.md:214-237`); fields are
opt-out (every column is a field unless `exclude` names it with a reason, `docs/metadata.md:239-251`); `write` is
`editable | create_only | read_only | computed` (`:254-258`); `required` is the field's own API rule (`:259-263`);
`collections` are the arrays the API takes beside the columns, optionally typed by `itemObject` (`:264-270`).

**The registration schema (quoted).** From the installed library
`/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-metadata/dist/object-registration.d.ts`:

```ts
// :664-968 (abridged to the shape)
export declare const registrationManifestSchema: z.ZodObject<{
    formatVersion: z.ZodLiteral<1>;
    application: z.ZodString;
    release: z.ZodString;
    objects: z.ZodArray<z.ZodObject<{
        id, label, pluralLabel, description?,
        table: { schema, name },
        recordIdField,
        fields: [{ key, columnName, label, description?, userReference?: true,
                   write?: 'computed'|'editable'|'create_only'|'read_only', required?: boolean,
                   dataType: 'boolean'|'text'|'large_text'|'html'|'email'|'phone'|'url'|'integer'|'decimal'|… ,
                   configuration, … }],
        collections?: [...],
        capabilities?: { customFields?: true; configurableForms?: true; configurablePageLayouts?: true },
    }>>;
}, z.core.$strict>;
```

```ts
// :977-1016  FieldDeclaration (what the owner says about one column)
export interface FieldDeclaration {
	readonly label?: string;
	readonly description?: string;
	readonly key?: string;
	readonly dataType?: DataType;
	readonly displayType?: DisplayType;
	readonly configuration?: Readonly<Record<string, unknown>>;
	readonly options?: Readonly<Record<string, string>>;
	readonly target?: string | { readonly typeField: string; readonly targets: Readonly<Record<string, string>> };
	readonly userReference?: true;
	readonly write?: FieldWriteAccess;
	readonly required?: boolean;
}
// :1021-1032  CollectionDeclaration { label, itemLabel, description?, maxItems?, itemObject?, write? }
// :1045-1086  ObjectDeclaration { id, label, pluralLabel, description?, recordIdField, fields?, collections?, exclude?, userReferences?, capabilities? }
// :1100       defineObject(table, declaration): RegisteredObject
// :1102       registrationManifest({ application, release, objects }): RegistrationManifest
```

`capabilities` is `z.core.$strict` with only `true` literals; an empty `{}` is refused; absent means none
(`object-registration.d.ts:8-22`). `customFields` requires the other two (`docs/metadata.md:185-187`), and Platform
refuses `customFields` on any object not in `APPROVED_CUSTOM_FIELD_OBJECTS`
(`/Users/andrew/rhythm2/services-platform/modules/metadata/capabilities/approved-custom-field-objects.ts`;
`docs/metadata.md:191-200`). R2-ARCH-001's approved list for CRM is **Contacts and Organizations only**
(`/Users/andrew/rhythm2/services-platform/docs/standards/custom-field-capability.md:32-34`); "Only Andrew decides that
an entity gets custom fields" (`:23-24`).

#### 1.2 How the manifest reaches Platform — Implemented

1. Build: `registrationManifest(...)` in the owner (`CRM: /Users/andrew/rhythm2/services-crm/aws/crm.metadata.ts:1-55`,
   `npm run metadata:manifest`).
2. Deploy, automatically: after the stacks deploy, the owner invokes `platform_metadata_registration` over IAM; answer
   `{ ok, objects, retired_objects, retired_fields, retired_collections }` (`docs/metadata.md:281-286`). CRM's deploy
   step runs, in order, `metadata:register`, `queries:register`, `recipes:register`, `extensions:register`,
   `eligibility:register` (`/Users/andrew/rhythm2/services-crm/.github/workflows/deploy.yml:179-185`; scripts
   `/Users/andrew/rhythm2/services-crm/package.json:30-38`).
3. Platform validates with the same contract and replaces the application's registration in one transaction; a missing
   object/field/collection is retired, kept, and comes back if re-declared; a reference to an unregistered object, a
   table already registered to another object, or an object of another application is refused and nothing is written
   (`docs/metadata.md:287-290`).

#### 1.3 The query registration (datasets) — Implemented

A separate manifest, `queryRegistrationManifest({ application, applicationLabel, release, objects })` over
`defineQueryObject(object, declaration)`
(`/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-metadata/dist/query-registration.d.ts:1004-1018`),
invoked as `platform_queries_registration` after metadata (`docs/queries/semantic-query-layer.md:451-468`). The
declaration interface, quoted from `query-registration.d.ts:954-1001`:

```ts
export interface QueryObjectDeclaration {
	readonly permission: string; // the dataset's own key, e.g. 'crm.contacts.query'
	readonly scope?: 'tenant' | 'system';
	readonly category?: string;
	readonly recordNumberField?: string;
	readonly recordLabelField?: string;
	readonly linkFields?: readonly string[];
	readonly recordLinks?: Readonly<Record<string, string>>;
	readonly defaults?: { fields?; sort?; criteria? };
	readonly hidden?: readonly string[];
	readonly discovery?: DiscoveryRuleDeclaration; // { field, discoverable, archivedField? }  (archivedField = 0.25.0)
	readonly customFieldValues?: PgTable;
	readonly source?: PgView | PgTable;
	readonly discriminator?: QueryDiscriminator;
	readonly computed?: Readonly<Record<string, ComputedFieldDeclaration>>;
	readonly configuredRelationships?: Readonly<Record<string, ConfiguredRelationshipDeclaration>>;
	readonly picks?: Readonly<Record<string, ObjectPickDeclaration | TablePickDeclaration>>;
	readonly collections?: Readonly<Record<string, CollectionDeclaration>>;
	readonly quickSearch?: QuickSearchDeclaration;
}
```

#### 1.4 Other deploy-time registrations an entity may need

| Registration                             | Function / route                                                                      | Doc                                                                                              |
| ---------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Query recipes (Explorer anchors)         | `platform_query_recipes_registration`, after `queries:register`                       | `/Users/andrew/rhythm2/services-platform/docs/queries/saved-queries-and-recipes.md:147-218`      |
| Permissions (API registry manifest)      | `npm run manifest -- check / publish / activate` → `platform_security_registry_check` | `/Users/andrew/rhythm2/services-crm/.github/workflows/deploy.yml:153-172`; §4.1                  |
| Responsibilities                         | `platform_responsibilities_registration`                                              | `/Users/andrew/rhythm2/services-platform/docs/responsibilities.md:85-105`                        |
| Attention definitions                    | `platform_attention_registration`                                                     | `/Users/andrew/rhythm2/services-platform/docs/attention.md:111-143`                              |
| Workflow actions / trigger points        | `PUT /workflow_actions_service/registry/<app>`                                        | `/Users/andrew/rhythm2/services-platform/docs/workflow-actions/README.md:67-92`                  |
| Eligibility condition types              | `platform_eligibility_registration`                                                   | `/Users/andrew/rhythm2/services-platform/docs/eligibility.md:152-176`                            |
| Business event subscribers               | from the deploy manifest into `platform.event_subscribers`                            | `/Users/andrew/rhythm2/services-platform/docs/business-events.md:110-137`                        |
| Tenant lifecycle (purge/reset/seed sets) | the owner's participant manifest; Platform holds a remote copy of keys                | `/Users/andrew/rhythm2/services-platform/docs/tenants/tenant-lifecycle-plan.md:616-617, 644-664` |
| Merge participation                      | two SQL functions per object type, called by CRM in its merge transaction             | `/Users/andrew/rhythm2/services-platform/docs/record-merge-participant.md:14-24`                 |

#### 1.5 What a new CRM `household` must declare, and the "Platform first" steps

Minimum:
`defineObject(households, { id: 'crm.households', label, pluralLabel, recordIdField: 'id', fields: {...}, collections?: {...} })`
added to `crmRegistrationManifest`;
`defineQueryObject(householdsObject, { permission: 'crm.households.query', recordLabelField: 'name', discovery?, collections?, quickSearch? })`
added to the queries manifest; a reader-grant migration (§5.1); recipes; a lifecycle participant resource; a merge
policy for every column holding a contact or organization id (§8.3).

**No `capabilities` unless Andrew rules it** (R2-ARCH-001 §1 rules 3 and 6). Without them a household gets purpose-built
screens (`custom-field-capability.md:146-154`).

**"Bump the metadata library version first" applies when, and only when, the declaration uses a manifest key newer than
the deployed Platform's library.** The 2026-10-20 incident: CRM pinned 0.25.0 and declared `discovery.archivedField`;
the deployed Platform (0.24.0, strict `discoveryRuleSchema`) refused `Unrecognized key`
(`/Users/andrew/.claude/projects/-Users-andrew-rhythm2-services-platform/memory/library-schema-bump-platform-first.md`).
The order is: library release → Platform bump landed and deployed → verify the Platform deploy → land the service that
declares the key. Today Platform is at 0.25.0 and CRM at 0.22.0 (`/Users/andrew/rhythm2/services-platform/package.json`,
`/Users/andrew/rhythm2/services-crm/package.json`), so a household registration using only 0.22.0 keys needs no Platform
bump; one using `archivedField` (0.25.0) needs CRM's bump but no Platform change; one needing a **new** key (say, a
symmetric-relationship declaration) needs library → Platform → CRM. The same rule governs the record-merge, data-jobs
and communications packages, each of which Platform pins.

---

### 2. Query semantic layer

#### 2.1 Datasets, fields, relationships — Implemented

- Dataset id = metadata object id; field ids = metadata keys; flattened picks `<pick>.<key>`; one-hop traversal
  `<referenceKey>.<targetKey>` (decision 13,
  `/Users/andrew/rhythm2/services-platform/docs/queries/semantic-query-layer.md:83-91`); configured-relationship fields
  `<configuration id>_id` (`:936-965`).
- Every metadata `reference` field is a `many_to_one` relationship walked forward; each one is also listed on the target
  dataset as a `one_to_many` relationship, with no fields
  (`/Users/andrew/rhythm2/services-platform/modules/queries/services/describe/resolve-query-model.ts:1777-1795, 1838-1840`).
- The request model (decision 8): `model.instances` + `model.joins[{ from, to, relationship, join_type }]`, `criteria`,
  `columns`, `grouping.group_by` + `aggregates` (`count_rows, count, count_distinct, sum, avg, min, max`), `sort`,
  `page` (`semantic-query-layer.md:59-66, 1631-1649`).

#### 2.2 Many-to-many through a junction, and double counting — Implemented, by construction

There is **no `many_to_many` relationship kind in the compiler**: `JoinableRelationship.cardinality` is
`'many_to_one' | 'one_to_many'`
(`/Users/andrew/rhythm2/services-platform/modules/queries/services/execute/query-model-plan.ts:33-35`). A junction is
reached in one of three declared ways:

1. **A `collections` declaration + `exists` criterion** (never a join):
   `EXISTS (SELECT 1 FROM <child> WHERE tenant AND via = record AND <criteria>)`, one nested level, so "a record is
   answered once however many of its rows match" (`semantic-query-layer.md:1060-1064, 1096-1101, 1152-1160`). Collection
   fields are declared, not read from metadata, "because a collection's rows need not be a registered object"
   (`:1089-1092`). The contact ↔ household junction is exactly this shape:
   `collections: { householdMembers: { table: household_members, via: 'contact_id', fields: { householdId: { column, dataType: 'reference', target: householdsObject }, role, startDate, endDate } } }`.
2. **A `configuredRelationships` family read from another relation** (`match.table` = the junction, `match.value` = the
   other end's id, `LEFT JOIN LATERAL … ORDER BY … LIMIT 1`) — gives a scalar "the contact's household" field with
   `LIMIT 1` keeping the grain (`:995-1044`).
3. **Platform's generated association families** (§2.4) — automatic for every association type, both sides.

**Fan-out in a row list:** "No to-many join ever reaches the outer `FROM`, which is what guarantees one row per grain
key" (`semantic-query-layer.md:1866-1870`); picks must be proven zero-or-one by a unique index checked at registration
against `pg_indexes` (`:844-850`).

**Fan-out in a report:** the request model _does_ allow a `one_to_many` join; the layer "lets a caller choose a grain of
their own" and documents that it repeats the base row per child (`query-model-plan.ts:99-110`;
`/Users/andrew/rhythm2/services-platform/docs/proposals/entity-aware-queries.md:533-537`). The Explorer refuses it by
name (`grainPreservingJoins`, `query-model-plan.ts:113-130`;
`/Users/andrew/rhythm2/services-platform/docs/explorer.md:106-120`), and its counts are
`count(DISTINCT <base record id>)` (`docs/explorer.md:274-278`). The aggregate `count_distinct` compiles to
`count(DISTINCT …)`
(`/Users/andrew/rhythm2/services-platform/modules/queries/services/execute/compile-query.ts:1401-1402`); a `sum` over a
one-to-many join is **not** symmetric — the declared `measure(...)` vocabulary with "distinct-on-key aggregation" is
Proposed and unbuilt (`semantic-query-layer.md:891-897, 1829-1835`). So: a report "sum of dues per household joined to
members" must group at the household grain or use a `rollup`/computed field on the household source view, never a join
to members then `sum`.

**`rollup(name, { aggregate })`** (`LEFT JOIN LATERAL (SELECT agg …)`) is in the proposed vocabulary table (`:824`) but
the shipped `QueryObjectDeclaration` has no `rollups`; the shipped equivalent is a `source` view with `computed` columns
(Organizations' `contact_count`, `:924-934`). A household "member count" is a computed column on
`crm.households_for_queries`.

#### 2.3 Relationship-aware criteria, exists/any/all — Implemented (`exists` only)

`exists` and `negate` (NOT EXISTS) are the whole grammar; "all"/"none"/counts are not criteria
(`semantic-query-layer.md:1096-1101`). "Relationship-aware criteria in the visual builder (E-17)" is a CRM acceptance
ruling marked "later" and assigned to a Platform query-designer milestone — it is not defined in any Platform master
document (`git show crm-architecture:docs/proposals/crm/14-gap-matrix.md` line 192, SR-08: "Missing (later by ruling) …
Platform query designer (PLAT-Mn)"). **Absent on master.**

#### 2.4 Association families: how Platform already exposes a junction to every dataset — Implemented

`TENANT_WIDE_QUERY_FAMILIES` (`/Users/andrew/rhythm2/services-platform/modules/queries/tenant-query-families.ts`)
generates, from `platform.association_types` + `platform.associations`, for every dataset of the tenant: a to-one
reference `<type id>_id` on the `to` side of a `one_to_many` type (both sides for `one_to_one`), `primary_<type id>_id`
on the `from` side when `allow_primary`, and `exists` collections `<type id>` / `<type id>_reverse` on both sides
carrying `record_id`, `start_date`, `end_date`, `is_primary`, `notes` (`semantic-query-layer.md:2975-2999`). Exposure of
these fields is `portal` (`:3013-3020`). No registration, no deploy; a type change moves the tenant's description stamp
(`docs/associations.md:39-42`).

#### 2.5 How Explorer renders a dataset — Implemented

A view is anchored to a **recipe** (`platform.query_recipes`, registered at deploy from
`modules/<module>/queries/recipes/*.recipe.json`,
`/Users/andrew/rhythm2/services-platform/docs/queries/saved-queries-and-recipes.md:138-218`); the recipe fixes the
datasets and joins, the view the columns/criteria/sort (`docs/explorer.md:19-21, 76-93`); a recipe may name only fields
every tenant has (never a configured-relationship field, `saved-queries-and-recipes.md:173-186`). A custom object's
activation writes `objects.<plural>.default` and `with-<type id>` recipes automatically
(`semantic-query-layer.md:3001-3011`); a native entity's recipes are hand-written JSON in the owner's repo. The
dataset's own permission key must be in Platform's build-time list (§2.7).

#### 2.6 `query_description_stamps` — Implemented, mandatory

"Any macroservice whose table a query description reads attaches
`platform.attach_query_description_stamp(<table>, <tenant column>)` in its own migration … and there is one stamps
table, `platform.query_description_stamps`" (`semantic-query-layer.md:1568-1580`); a guard spec fails any table a cold
build reads without the triggers
(`modules/queries/tests/integration/services/describe/description-stamp-coverage.integration.spec.js`, `:1573-1580`).
CRM's precedent: `/Users/andrew/rhythm2/services-crm/db/migrations/0073_query_description_stamps_to_platform.sql:82-88`.
For households: every **configuration** table a description reads (a `household_roles` or `relationship_types` table
used as a configured-relationship family) needs the call; a plain record table does not.

#### 2.7 Platform-side edits a new dataset needs — Implemented (and a trap)

`modules/queries/datasets.ts` is a build-time list of every dataset's query permission key, named in the routes'
`evaluates` so the authorizer signs the key into the slice: "an owner that registers a new dataset adds its key here in
the same release" (`/Users/andrew/rhythm2/services-platform/modules/queries/datasets.ts:7-23, 70-87`). A unit spec
compares it with CRM's real manifest fixture `tests/integration/crm.query-registration.json`
(`/Users/andrew/rhythm2/services-platform/modules/queries/tests/unit/dataset-permission-keys.spec.ts:1-32`). Without the
entry, only staff can query the dataset (the DEF-CRM-0450 class of defect, `datasets.ts:81-83`). The same Platform list
governs Explorer and recipe routes (`docs/explorer.md:180-192`).

---

### 3. Forms and page layouts

- **Platform stores definitions, reads nothing inside them** — Implemented. Four tables (`console_page_layouts`,
  `portal_page_layouts`, `console_forms`, `portal_forms`); `layout` is "any JSON object … The console owns the
  document's shape" (`/Users/andrew/rhythm2/services-platform/docs/layouts.md:14-30, 32-47`). Five slots (two console
  forms since 2026-10-11) (`:239-263`).
- **Assignment lives with the record that uses it** (`platform.layout_assignments`, written by the owner through
  capability role `rhythm_layout_assigner`, composite FKs enforce kind/purpose/tenant)
  (`docs/layouts.md:196-237, 265-277`). CRM's precedent: five layout-slot columns on `crm.organization_types`
  (`/Users/andrew/rhythm2/services-crm/modules/entities/db/organization-types.table.ts:12-14`).
- **A purpose is gated by the registry**: a console/portal form needs `supports_configurable_forms`, a page layout
  `supports_configurable_page_layouts`; `LAYOUT_PURPOSES` maps a purpose to the owner's `<app>.<resource>-layouts.*`
  keys and must gain an entry (`docs/layouts.md:146-160`). A household without R2-ARCH-001 capabilities has **no**
  configurable form or layout and gets purpose-built screens (`custom-field-capability.md:146-154`).
- **360 "cards" / sections: not in Platform.** Nothing in Platform registers composable cards; the standard calls them
  "supported cards" of the console's shared layout infrastructure (`custom-field-capability.md:138`), and `layout` is
  opaque to Platform. **Absent in Platform; lives in the console** (not inspected, per the brief).
- **Portal layouts**: the `portal_page_layouts` / `portal_forms` tables exist; portal participation "is a product scope
  question answered in its milestone plans" (`custom-field-capability.md:95-97`) and portal is always last (standing
  rule).

---

### 4. Security

#### 4.1 Model and declaration — Implemented

- Keys are `<app>.<resource>.<action>`, three segments
  (`/Users/andrew/rhythm2/macroservice-libraries/packages/authorization/src/permission-catalog.ts:43-47`); examples
  `platform.custom-fields.read` (`docs/metadata.md:402`), `crm.contacts.query`
  (`/Users/andrew/rhythm2/services-crm/aws/crm.service.ts:115-119`). Tiers `read | user | admin` default by action;
  `sensitive` keys state theirs; a `resourceType` makes a key record-scoped
  (`permission-catalog.ts:3-4, 56-62, 158-163`).
- Declaration lives in the owner's code:
  `definePermissions(crmService.app, 'contacts', { ...RECORD_TIERS, custom: {...} })` (`crm.service.ts:116`), catalogued
  in `crm.routes.ts:70-82`, published by `generateRegistryManifest` at deploy via `platform_security_registry_check` →
  publish → activate (`/Users/andrew/rhythm2/services-crm/.github/workflows/deploy.yml:153-172`;
  `/Users/andrew/rhythm2/services-platform/docs/proposals/security-authorization.md:430-443`). Activation reconciles
  `policy:<app>:read-only|user|administrator` by tier
  (`/Users/andrew/rhythm2/services-platform/docs/security/built-in-policies.md:85-102`); declaring a policy grants it to
  nobody (`:8-10`).
- Subjects: console users (one per customer wall), portal users (self sign-up; Portal Member role at tenant scope),
  **CRM contacts as assignment subjects** (exactly one of `user_id`/`contact_id`), service principals
  (`/Users/andrew/rhythm2/services-platform/modules/security/db/security-assignments.table.ts:7-31`;
  `/Users/andrew/rhythm2/services-platform/docs/security/service-calls.md:13-18`). Scopes
  `platform | customer | tenant | resource`; no business-unit scope
  (`/Users/andrew/rhythm2/services-platform/docs/security/application-access.md:14-15`).
- A permission "manage household" would be:
  `definePermissions(crmService.app, 'households', { ...RECORD_TIERS, custom: { Manage: { action: 'manage', tier: 'user', description } } })`
  → catalog → routes `authorization: { access: RouteAccess.Console|Portal, permission: HouseholdsPermissions.Manage }` →
  deploy. Portal reach additionally needs Andrew's decision to add the key and `crm.application.access` to Portal Member
  (`built-in-policies.md:194, 200-215`;
  `/Users/andrew/rhythm2/services-platform/modules/security/services/catalog/reconcile-catalog.ts:441-458`). If
  households are organizations (CRM-Q2 option 1), `crm.organizations.*` already gates them unless a `households`
  resource is declared.

#### 4.2 Record-level security — mechanism Implemented, CRM use Absent, decision open

`resourceType` keys, `resource: { type, from: 'path'|'record' }` on routes, access sets, and
`requireAccess / accessFilter / hasAccess` exist
(`/Users/andrew/rhythm2/macroservice-libraries/packages/authorization/src/contracts.ts:62-80`;
`packages/execution/src/require-access.ts:47-91`; `security-authorization.md:890-907`); Platform uses them for custom
objects and responsibilities. CRM: "no key names a `resourceType`" (`crm.service.ts:9-10`). CRM-Q8 (staff scoping by
chapter/region/org type) is open and blocks CRM-M23 — branch `crm-architecture`, `docs/proposals/crm/13-decisions.md`
lines 142-153. Postgres RLS is tenant isolation only
(`/Users/andrew/rhythm2/services-platform/docs/row-level-security.md:1-8, 22-31`).

#### 4.3 Portal identity — Implemented

A portal login maps to a contact by the likely-match flow (auto-link on the member's own address, up to three masked
candidates confirmed by code, or CRM creates a contact)
(`/Users/andrew/rhythm2/services-platform/modules/security/services/portal/contact-match.ts:1-28`); the link is
`platform.users.contact_id`, a soft reference
(`/Users/andrew/rhythm2/services-platform/docs/security/portal-contact-claim.md:8-11`); the signed context carries `cid`
= contact id (D-26) and the libraries expose `ctx.caller.contactId` / `requireContactId`
(`modules/security/authorizer/context/signed-context.ts:39, 78`;
`macroservice-libraries/packages/authorization/src/security-context.ts:54-60, 254, 297-299`). **One contact ↔ one portal
user per tenant** (write-time check,
`/Users/andrew/rhythm2/services-platform/modules/security/services/persistence/users.ts:838-855`) — a child cannot share
a parent's login. The claim names a person and grants nothing (`portal-contact-claim.md:35-37`).

#### 4.4 Delegated / "acting for" — Absent (and the one primitive to reuse)

| Primitive                                    | Status                                                     | Note                                                                                                                                                                                                                                                                                                                                                      |
| -------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Impersonation                                | Planned (flag only)                                        | "Impersonation itself is deferred" (`/Users/andrew/rhythm2/macroservice-libraries/packages/http/src/route-access.ts:35-38`; `security-authorization.md:263, 1104`); audit columns `acting_for_user_id` reserved (`modules/security/db/security-audit-events.table.ts:125-131`)                                                                            |
| Management companies                         | **DEFERRED by Andrew 2026-10-18**, console-to-console only | `/Users/andrew/rhythm2/services-platform/docs/proposals/management-companies/README.md:3, 267-328, 459` — the pattern: "a relationship grants nothing; a grant grants a role; … the wall admits the grant only while the relationship is in force"; grants are dated and produce ordinary scoped `security_assignments` rows                              |
| `representedOrganization`                    | Decided in the eligibility design, not carried             | `/Users/andrew/rhythm2/services-platform/docs/eligibility.md:743-745, 948-949`                                                                                                                                                                                                                                                                            |
| **Contact-keyed derived assignments**        | **Implemented**                                            | another service's rule writes an ordinary role for a contact at tenant or resource scope via `platform.sync_derived_assignments`; sources today only `committees` and `eligibility` (`security-assignments.table.ts:15-21, 67`; `docs/security/security-administration.md:256-294`; `db/migrations/20261018025625_eligibility_derivation_source.sql:4-6`) |
| Organization administrators managing rosters | Planned (CRM-ADR-11, CRM-M22, portal last)                 | branch `crm-architecture`, `13-decisions.md` line 37                                                                                                                                                                                                                                                                                                      |
| Service principals                           | Implemented                                                | a CRM→Membership call is a direct Lambda invoke under IAM as `svc_crm`, every key answered ALL in the path tenant, `originatingUserId` for provenance only (`docs/security/service-calls.md:20-36`; `docs/proposals/service-principals.md:11-19, 104-111, 224-230`)                                                                                       |

The reusable shape for "a household head acts for members": add `crm` as a derivation source (a Platform migration +
grantor role, as eligibility was added), derive a "Household Manager" role for the head's contact at `resource` scope
`household:<id>`, declare the household keys with `resourceType: 'household'`, and have portal routes call
`requireAccess`. Editing a _member's own contact record_ would still need a CRM ownership predicate because the member
is a different contact from the caller's `cid`. The management-companies lesson applies: dated explicit grants, never
unioned identities.

---

### 5. Tenant isolation and table conventions

#### 5.1 Conventions for a new tenant record table — Implemented / Decided

- Key `(tenant_id, id)` named `<table>_pk`; composite FKs `(tenant_id, parent_id)` inside the tenant; **plain text ids
  into other schemas, verified by the write path, never a FK**
  (`/Users/andrew/rhythm2/services-platform/db/migrations/20261020011719_associations.sql:23, 58-59`;
  `docs/associations.md:50`). CRM tables carry no FK to `platform.tenants` (none in services-crm migrations); Platform
  tables do.
- RLS in the creating migration, policy named `<table>_tenant_isolation_policy`,
  `USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), ''))` with the same `WITH CHECK`, ENABLE + FORCE; a
  coverage test fails a table without it
  (`/Users/andrew/rhythm2/services-platform/docs/row-level-security.md:25-31, 160-166, 189-191`). Repositories still
  filter on `tenantOf(ctx)` (`:41`).
- System columns
  `sys_version, sys_created_at, sys_created_by_id, sys_last_modified_at, sys_last_modified_by_id, sys_external_id, sys_locked` +
  `<table>_sys_locked_check` (`associations.sql:15-27`); `<table>_normalize_text_trigger` on every text column
  (`/Users/andrew/rhythm2/services-platform/AGENTS.md:200-209`).
- Ids: records UUIDv7, configuration tables immutable string ids (Andrew 2026-10-19;
  `/Users/andrew/rhythm2/services-platform/modules/integrations/db/integration-ids.ts:2-4, 36-51`); creates accept a
  client UUIDv7 (`/Users/andrew/rhythm2/services-platform/docs/standards/record-write-contracts.md:211-214`).
- Grants: the app role reaches its own tables by default privileges; `rhythm_table_reader` reads every schema
  (`AGENTS.md:307-312`); **`rhythm_query_reader` column grants are generated from the registered columns into the
  owner's migration** (CRM precedent `/Users/andrew/rhythm2/services-crm/db/migrations/0019_regions.sql:46`); a
  collection/family column the reader cannot select is refused at registration
  (`semantic-query-layer.md:1046-1051, 1170-1173`).
- Every owned list states a maximum (`AGENTS.md:264-272`); relationship dates are `date`
  (`/Users/andrew/rhythm2/services-platform/docs/time.md:13-24`).

#### 5.2 Write contracts — Decided (enforcement rolling out)

W1 versions on every field-changing write (400 missing, 409 stale), W2 no-op writes write nothing, **W3 "an omitted
nested collection is unchanged … Ending or removing a relationship is an explicit operation with its own permission"**;
`[]` in a relationship collection changes nothing (`record-write-contracts.md:48-59, 116-118, 136-153`). Household
members and relationships are relationship collections: add/end/remove are separate routes. R2-ARCH-002: every new
business operation is published to Submissions or recorded as deliberately not
(`/Users/andrew/rhythm2/services-platform/docs/standards/submission-compatible-operations.md:16-18`).

#### 5.3 Sandbox provisioning, tenant copy, export

- **Tenant copy / sandbox from a parent: Absent.** Sandboxes are tenants of type `sandbox` with a `parent_id`
  (`/Users/andrew/rhythm2/services-platform/modules/tenants/db/tenants.table.ts:216`); 1.0's copy is "Defer (new
  capability)" (`/Users/andrew/rhythm2/services-platform/docs/tenants/tenant-lifecycle-plan.md:209`); the participant
  contract has no clone operation
  (`node_modules/@rhythmsoftware/macroservice-tenant-lifecycle/dist/participant.d.ts:98-131`). No document says how
  cross-service ids stay consistent on copy. (Because keys are `(tenant_id, id)`, a copy that rewrote only `tenant_id`
  would keep text ids valid — inference, not documented.)
- **"Snapshots"** are monthly AR/membership reporting snapshots, not copies
  (`/Users/andrew/rhythm2/services-platform/docs/proposals/snapshot-architecture.md:17-20`); a family membership in
  Membership falls under its source requirements (`docs/snapshots/source-requirements.md:23-28`), CRM households do not.
- **Tenant data export**: foundation built (coverage registry, tables), generation Planned (WP-3/4); a many-to-many is
  exported as a **link dataset** (`/Users/andrew/rhythm2/services-platform/docs/data-exports.md:8-19, 151-162`;
  `docs/proposals/tenant-data-export/05-export-package-format.md:87-128`); each lifecycle resource needs an `export`
  classification (`modules/data-exports/export-coverage/contract.ts:28-43, 51-88`).
- **Lifecycle**: every tenant-keyed table must be declared in exactly one participant's `resources` with a purge
  disposition (completeness test, `tenant-lifecycle-plan.md:686-689`); a new participant key needs a Platform
  declaration or every purge is `PurgePlanChanged` (`:616-617`) and a reset classification
  (`docs/data-jobs/tenant-reset.md:63-64`). Placing household tables under CRM's existing `crm.entities` needs no
  Platform change
  (`/Users/andrew/rhythm2/services-platform/modules/tenants/services/lifecycle/remote-participant-manifests.ts:42-48`;
  reset `reset-participants.ts:118`).

---

### 6. Notifications and workflow primitives

- **Communications — Implemented, e-mail only.** A purpose names the records it carries by registered object id
  (`records`, `recipientRecords`) and declares `variables` only for data of no record; merge fields come from the
  metadata description, custom fields lifted to top level
  (`/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-communications/dist/purposes.d.ts:6-33`;
  `/Users/andrew/rhythm2/services-platform/docs/communications/architecture.md:1768-1775, 2343-2355`). Purposes are code
  in the shared package, reviewed in a Platform change; deploy-time purpose registration is Planned
  (`architecture.md:844-846`). Templates are owned by a record (`templateOwner`, no bindings; Andrew 2026-10-09,
  `:1777-1805`). **A recipient is `{ key, contactId?, userId?, emailAddress, displayName?, data? }` — the address is
  required, the contact optional**, ≤500 per request (`communication-request.d.ts:14-15, 35-42`); the caller submits in
  its own transaction with `submitCommunicationRequest(tx, …)` (`architecture.md:2005-2026`). Channel is `'email'` only
  (`purposes.d.ts:69`); Slack/Teams are Planned (phase 5, `:236, 262-264, 1601-1604`; events D-64
  `docs/proposals/events/12-decisions.md:740-744`). Consent lives in CRM (`crm.contact_communication_preferences`,
  `architecture.md:1373-1376`); Platform applies only deliverability suppression (`:434-444`;
  `accept-communication-request.ts:347-353`).
- **Business events — Implemented.** `defineEvent` + `events.publish(tx, Event, { subject, scope, payload })` + `nudge`;
  keys `<application>.<subject>.<past-tense verb>` and must start with the application (so `crm.household_member.added`,
  never `household.member.added`) (`node_modules/@rhythmsoftware/macroservice-events/README.md:43-99, 63-64`); outbox
  ledger + queue per schema, at-least-once, no ordering, 90-day ledger (`docs/business-events.md:43-75, 104-137`; README
  `:143-160`). Subscribers via `defineEventSubscriber`, registered from the deploy manifest
  (`docs/business-events.md:110-137`). The Integration Platform catalogue (`integration-contracts` 0.7.1) has no
  household keys — an uncatalogued key loads with a warning.
- **Workflow actions / automation rules — Implemented library, executors not published.** Hosts call
  `raise(tx, occurrence)`; trigger points declared with `defineTriggerPoint`; Platform declares `communications.send`
  and an inactive `notifications.notify`
  (`/Users/andrew/rhythm2/services-platform/modules/workflow-actions/contracts/platform-declarations.ts:20-21, 46-79`);
  the builtins package is "not published" (`docs/workflow-actions/README.md:98-100`); recipient rule kinds
  `applicant | participants | assigned_reviewers | security_role | user | address_field | host_resolved` — no
  `responsibility` kind (events D-64 asks for one)
  (`node_modules/@rhythmsoftware/macroservice-workflow-actions/dist/recipients.d.ts:18-40`). A tenant's automation rule
  fires only for an event its owner registered as a trigger point of host `platform.automation_rule`, and the
  **owner's** consumer calls `raiseForEvent` (`docs/workflow-actions/automation-rules.md:3-20`).
- **Responsibilities — Implemented.** Deploy-registered keys with `scope_kinds`, `role_defaults`, `expect_holder`,
  `fallback_key`; holders via `platform.responsibility_holders(tenant, key, scope_refs)` →
  `user_id, display_name, email_address`; **a responsibility grants no permission**
  (`docs/responsibilities.md:9-17, 69-105`). Notices to holders: the caller reads the holders and passes them as
  recipients; Platform's own staff notices still address `role:administrator` with a note that the body will be replaced
  by the responsibility read (`/Users/andrew/rhythm2/services-platform/lib/notices/recipients.ts:1-10`).
- **Attention — Implemented** (doc conflict: the proposal still says "nothing implemented", the module doc and code say
  built, `docs/attention.md:3-5`). Items written at source through `platform.publish_attention_item` under capability
  role `rhythm_attention_writer`; audiences
  `responsibility | user | team | role | contact | organization_role | eligibility`; attention never sends messages;
  member-portal read is on hold (`docs/attention.md:7-17, 59-74, 90-92, 176-186`;
  `docs/proposals/responsibilities-and-attention/16-decisions.md:123-136`).

---

### 7. Data Engine (Data Job Engine)

- **Registration — Implemented.**
  `defineDataOperation<Input>({ objectType, operation: 'update'|'upsert'|'insert'|'delete', contractVersions, permissions: { submit, perRecord }, patchableFields, matchingKeys, dependsOn, groupBy?, importBehaviour, prevalidate, prepare?, apply, create?, update?, bulkApply?, rollback?, classifyError })`
  (`/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-data-jobs/dist/define-data-operation.d.ts:195-262`).
  Each service keeps its own registry
  (`/Users/andrew/rhythm2/services-platform/modules/data-jobs/services/operations/data-operations.ts:32-44`; CRM
  `modules/data-jobs/services/operations/data-operations.ts`); a Data Run derives the route from the object type
  (`docs/data-jobs/data-runs.md:167-171`) and the service must be in `DATA_RUN_SERVICES`
  (`modules/data-jobs/services/runs/data-run-services.ts:39-45`; CRM is).
- **Upsert keys / crosswalk — Implemented.** `insert`/`upsert` match on `sourceKey`, `update`/`delete` on `id`
  (`define-data-operation.js:69-74`); `data_id_mappings` per service, unique
  `(tenant, source_system, object_type, source_key)`, written in the record's transaction
  (`/Users/andrew/rhythm2/services-platform/db/migrations/0094_data_job_engine.sql:16-29`); another service's keys are
  resolved by reading its table (`modules/data-jobs/services/persistence/data-id-mappings.ts:47-57`). Natural keys
  (e-mail) are typed but not enforced — Planned (`rhythm-data-job-engine-plan.md:237-241`).
- **Dependency order — Implemented at the Data Run manifest only.** Waves come from the manifest's `depends_on` (Kahn),
  not from an operation's `dependsOn` (`docs/data-jobs/data-runs.md:104-115`; verified gap
  `docs/proposals/data-engine-architecture/12-dependencies-and-referential-integrity.md:33`). Groups succeed or fail
  together within one job (`macroservice-data-jobs/README.md:99-112`). A parent with no crosswalk row is
  `blocked / dependency_missing`, retried by `retryDataJob` (README `:212-217`). **"E-11"** (atomic groups resolving
  references in dependency order) is a CRM-acceptance ruling the CRM record lists as "Ruled E-11 (shared engine) — Not
  built" (`git show crm-architecture:docs/proposals/crm/10-data-operations.md` line 27); the Platform proposal's nearest
  items are DE-10 (references + `data_job_dependencies`, Planned) and DE-11 (failure policies, Planned) — "none is
  approved yet" (`docs/proposals/data-engine-architecture/22-architecture-decision-records.md:7, 311, 338-361`).
- **Validate-then-apply, rollback, idempotency — Implemented.** `mode: "validate"` then `/apply` on frozen items;
  deletes need an impact digest and typed confirmation (`rhythm-data-job-engine-plan.md:281-288`; README `:114-132`);
  rollback is optional per operation (only CRM contact update declares one; Platform's operations deliberately have
  none; a Data Run's undo is a tenant reset) (`docs/data-jobs/platform-data-operations.md:145-152`;
  `docs/data-jobs/data-runs.md:150-151`); `Idempotency-Key`, receipts, `sourceRevision` stale/duplicate
  (`plan.md:269-279`).
- **A two-parent junction row — Implemented: `platform.associations / upsert`.** Each line names both ends by
  `<objectId>:<source key>` or `id:<record id>`, resolved through the owner's crosswalk; prevalidate checks type
  activity and cardinality within the file; apply creates or updates own fields only
  (`/Users/andrew/rhythm2/services-platform/modules/data-jobs/services/operations/association-import.ts:6-12, 73-77, 195-223, 245-285, 313-340`).
  Two gaps to copy correctly: its `dependsOn` is a fixed list (`custom_object_records, crm.contacts, crm.organizations`)
  and a missing endpoint is **rejected** (`ObjectInvalidError` → `invalid_value`, `:337-338`), whereas CRM's contact
  import **blocks**
  (`/Users/andrew/rhythm2/services-crm/modules/data-jobs/services/operations/contact-import.ts:94-111, 143-156`). Today
  CRM's affiliations (the contact↔organization junction) are imported embedded in the contact item, and spouse is a
  two-pass reference-only update (`contact-import.ts:16-20`;
  `/Users/andrew/rhythm2/services-crm/docs/data-jobs.md:181-231`).
- **Exports — Implemented** for query datasets (CSV/XLSX/PDF, schedules, e-mail delivery,
  `docs/queries/exports-and-schedules.md:9-31, 150-170`); the full-tenant export generates nothing yet (§5.3).
- **Bulk update — Implemented in CRM** (`crm.contacts/update`, ≤500 ids, validate, rollback;
  `rhythm-data-job-engine-plan.md:1517-1532`); from-a-query selections are Planned (DE-20). **Bulk merge — Absent**;
  merges are one-at-a-time `crm.record_merge_jobs` and "imports never merge" (`plan.md:247-250`). A merge repoints
  crosswalk keys to the survivor (`services-crm/docs/data-jobs.md:266-269`).

---

### 8. Audit and history

- **Record history — Implemented (CRM triggers, Platform copy).** Database triggers fold every row change into one event
  per record per transaction and settle it into `<schema>.record_revisions` at commit; one revisions table per
  macroservice with `record_type` as a column, so a new entity adds no table; child rows join the parent their row
  names; a commit with no `app.source`/`app.user_id` fails
  (`/Users/andrew/rhythm2/services-platform/docs/proposals/record-change-history.md:28-34, 199-204, 257-259, 431-437`;
  `docs/record-history.md:99-105`). Capture folds a child into a parent only within one schema
  (`record-history.md:65-67`); an association from a contact is written to `platform.record_revisions` under
  `crm.contacts`, and CRM's drawer does not show those rows yet (`:147-151`, Absent). Platform's stream reads
  `platform.associations` twice (`associations` and `reverse_associations`) so both sides get history (`:52-56`) — a
  household-member table can do the same.
- **Audit events — Implemented.** `platform.audit_events` has a closed `AUDIT_EVENT_TYPES` list with a generated CHECK
  (`/Users/andrew/rhythm2/services-platform/modules/audit/db/audit-events.table.ts:109-118, 309`); adding one is four
  steps (`docs/audit-events.md:445-449`). Configuration changes go to `platform.security_audit_events`, whose
  **entity-type CHECK is regenerated as a full list on every migration that adds a type**
  (`modules/security/db/security-audit-events.table.ts:39-97, 111, 158`; latest
  `db/migrations/20261020051317_file_uploads.sql:29-31`) — two branches adding a type conflict; regenerate from the TS
  list after merging master (memory `audit-entity-type-check-full-list`). Association-type changes are audited as
  `association_type` (`docs/associations.md:96-97`); a household-type/relationship-type configuration table needs its
  own entity type.
- **Deletion rules — Decided (D-02, D-07, D-25, D-41/42/44), mechanisms Planned, nothing built.** Three operations:
  deactivate (status), archive (`archived_at, archived_by_id, archive_reason`, reversible, never a status), hard delete
  (impact-previewed, approved, durable job, deletion record)
  (`/Users/andrew/rhythm2/services-platform/docs/proposals/record-lifecycle/README.md:3-8, 45-56, 103`;
  `01-lifecycle-semantics.md:8-18`). The deletion-impact contract adds to the lifecycle manifest
  `operations?: 'planRecordDeletion'|'holdRecordDeletion'|'applyRecordDeletion'|'releaseRecordDeletion'` and
  `recordReferences?: { objectType, contractVersion: 1 }[]`; an answer gives per reference kind a disposition
  `block | retain | detach | redact | delete` with counts, ≤200 items by id+version, a summary and a digest; **a
  manifest that omits `recordReferences` blocks every deletion**; `detach` writes a tombstone, never another id
  (`02-deletion-impact-contract.md:47-53, 61-64, 82-98, 139-173`). Orphaned activities are never reassigned "to … a
  household" (D-44, `04-orphaned-activities.md:30-43`). Linked addresses: a contact's address row `linked` to an
  organization address through an affiliation becomes `copied` when the source is removed; Planned fourth mode
  `disconnected` (D-25, `05-linked-addresses.md:11-50`, open Q-7 `:78-83`). Today `deleteContact` hard-deletes and
  clears other contacts' `spouse_id` (`01-lifecycle-semantics.md:52-56`). For a Platform association the delete side is
  already handled by `platform.apply_endpoint_deletion(tenant, 'crm.contacts', id)` in the owner's transaction
  (`detach | cascade | restrict`, `docs/associations.md:171-237`).
- **Merge participant contract — Implemented** (`@rhythmsoftware/macroservice-record-merge` 0.3.0). Quoted from
  `/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-record-merge/dist/participant.d.ts:15-92`:

```ts
export interface ParticipantAction { readonly table: string; readonly column: string; readonly policy: MergePolicyKind;
    readonly rows: number; readonly summary: string; readonly destructive: boolean; readonly rowIds?: readonly string[]; }
export interface ParticipantDecision { readonly conflictId: string; readonly class: ParticipantDecisionClass; // 'relationship' | 'identity'
    readonly summary: string; readonly choices: readonly ParticipantChoice[]; readonly default?: ParticipantChoiceRef; readonly required: boolean; }
export interface ParticipantBlocker { readonly code: string; readonly summary: string; }
export interface ParticipantWarning { readonly code: string; readonly summary: string; }
export interface ParticipantPlan { readonly contractVersion: typeof MERGE_PARTICIPANT_CONTRACT_VERSION;
    readonly actions: readonly ParticipantAction[]; readonly decisions: readonly ParticipantDecision[];
    readonly blockers: readonly ParticipantBlocker[]; readonly warnings: readonly ParticipantWarning[]; }
export interface ParticipantApplyResult { readonly contractVersion: …; readonly actions: readonly ParticipantApplyAction[]; }
```

Two SQL functions per object type, `<schema>.merge_plan_<object>(survivor_id, merged_ids) → jsonb` and
`merge_apply_<object>(merge_id, survivor_id, merged_ids, decisions)`, `SECURITY DEFINER`, executable by
`rhythm_crm_app`, called in CRM's transaction (`docs/record-merge-participant.md:14-24, 97-110`). **Every column holding
a contact or organization id must declare a policy; a coverage test fails CI otherwise** (`:32-35`;
`lib/record-merge/platform-references.ts:1-16`). Policies include
`reassign-dedupe { businessKey, onCollision: keep-survivor|keep-earliest|keep-latest|decide|{combine} }` and `clear`
(`dist/policies.d.ts:20-62`). Merge targets are only `crm.contacts | crm.organizations` (`policies.d.ts:2, 10`): a
household that is its own entity has **no merge of households** (Absent) unless it is an organization. Platform's
association participant repoints endpoints and _ends_ a row that would collide (`docs/associations.md:239-259`).
Aliases: `crm.merged_record_aliases`, `resolveMergedId(s)` → `{ id, mergedInto: { survivorId, mergeId, mergedAt } }`
(`dist/merged-ids.d.ts:4-41`); history stores keep the old id (`preserve-history`, `docs/audit-events.md:179-183`).

---

### 9. Eligibility engine

- **Rule shapes — Implemented** (`@rhythmsoftware/macroservice-rules` 0.2.1,
  `/Users/andrew/rhythm2/services-platform/node_modules/@rhythmsoftware/macroservice-rules/dist/grammar.d.ts:234-293`):
  `group (and/or, not)`, `condition { type, subject, operator, value, asOf? }`,
  `related { path, quantifier: any|all|none|atLeast|atMost|exactly, where }`, `reference { definition, version }`,
  `fact`; limits 200 nodes, depth 8, reference chain 3, path ≤3 steps (`:31-41`; `subjects.d.ts:47-62`). Condition kinds
  `sql` (query-layer predicates over a dataset keyed on the subject's own id), `function`, `fact`
  (`docs/eligibility.md:382-390, 403-404`).
- **"Age at a date" — Implemented**: CRM's `crm.contact.age` is an as-of shift
  `dateOfBirth on_or_before {$asOf:{shift:{amount:-18,unit:'years'}}}`, evaluable at the run's `as_of` or a per-node
  `asOf: { fact }` (`docs/eligibility.md:414-416, 1112, 1158`; `docs/eligibility-reads.md:41-53`).
- **"Member of a household with an active membership" — Absent end to end.** The grammar can say it; the relationship
  map ships only `primaryOrganization, employer, designatedAffiliation, affiliations, committeeSeats` (+
  `affiliation.organization`, `parentOrganization`) (`macroservice-rules/dist/subjects.js:66-81`); the resolver reads
  three paths and knows contact/organization/affiliation subjects only
  (`modules/eligibility/services/evaluate/subjects.ts:6-19, 39-43`); nobody registers relationships yet
  (`docs/eligibility.md:276-277`); a membership `sql` condition keyed by `contactId` does not compile (`:403-404`);
  `platform.eligibility_check` refuses related blocks (`:617-618`). A household design needs a `withRelationships`
  registration (`household`, `householdMembers[]`) plus resolver code, plus a Membership condition type.
- **PARKED** — Andrew's stop 2026-10-19; engine + repairs on master `d379ba051` (ancestor of `0614c54cf`, verified) and
  deployed to dev; re-runs and the config-id job on branches; "do not land without a new go"
  (`/Users/andrew/rhythm2/briefs/eligibility/PARKED-2026-10-19.md:1-36`).

---

### 10. Well-known objects and system-seeded configuration

- **Well-known objects are dev/test fixtures, never production**
  (`/Users/andrew/rhythm2/services-platform/docs/well-known-objects.md:3-9, 33-37`), converged on every dev deploy
  (`:39-45`). Not the mechanism for a seeded "Household" type.
- **Baseline configuration (D-48, Decided; Implemented in CRM, not yet in Platform) is the mechanism.** Defaults are a
  participant's `SeedSet { key, version, seeds: Seed[] }`
  (`node_modules/@rhythmsoftware/macroservice-tenant-lifecycle/dist/participant.d.ts:60-77`), each seed `apply`
  inserting `ON CONFLICT DO NOTHING` on natural keys, recorded in `<schema>.tenant_seed_applications`, reconciled into
  every operational workspace on every deployment, never overwriting a rename/deactivation/deletion; a new default = new
  seed key + raised set version; changing an applied default = a reviewed data migration
  (`/Users/andrew/rhythm2/services-platform/docs/standards/baseline-configuration.md:22-47, 62-65`). CRM's worked
  example: `modules/tenant-lifecycle/services/lifecycle/baseline-reconciliation.ts` (`:64`),
  `duplicates-participant.ts:82-88`.
- **Identification and protection — CRM precedent (Implemented).** A seeded configuration row is a configuration-id row
  with an immutable **`system_code`** column (unique per tenant when set;
  `activity_types_system_code_immutable_trigger`), so code finds "the tenant's Call type" whatever it was renamed to; a
  system type cannot be deleted (409, "deactivate it instead")
  (`/Users/andrew/rhythm2/services-crm/modules/activities/db/activity-types.table.ts:11-14`;
  `modules/activities/services/persistence/activity-types.ts:13-18`). Generic protection is
  `sys_locked IN ('deletes','updates & deletes')` (`db/migrations/20261020051317_file_uploads.sql:26`). A "Household"
  organization type or a "Spouse" relationship type would carry a `system_code` and be seeded this way; an association
  type (`platform.association_types`) has **no** system-code column today — a seeded Spouse type there would be an
  ordinary tenant row with a reserved configuration id (Absent: a "system" flag on association types).
- **Translation — Absent.** No i18n/localisation mechanism exists in Platform; the only relabelling is the tenant's
  standard-field label override (`docs/metadata.md:174-179`) and configuration-row renames.

---

### 11. Custom objects

- **Implemented** (migrations `20261020011719`–`…23`; PLAT-M6 still "Proposed"): tenant-defined objects (≤50) with typed
  properties, numbering, publication, archive, history, automatic datasets, portal access rules stored
  (`/Users/andrew/rhythm2/services-platform/docs/custom-objects.md:48-50, 93-107, 278-284, 457, 492`), related through
  associations.
- **Why a Household should not be one** — the design's own boundary: "a thing with its own identity … relationships to
  people" may be a custom object, but anything needing money, credentials, schedules "or any transactional rule the
  platform must guarantee" is "a native application … never a custom object"
  (`docs/proposals/custom-objects/03-recommended-architecture.md:774-786`); a custom object has no behaviour of its own
  (`:30-35`), may never own native records (`:781-786`), cannot be merged
  (`lib/record-merge/platform-references.ts:378`), is not an eligibility subject, and the portal is paused
  (`10-milestone-plan.md:36`). Family memberships are money and terms.
- **Why affiliations are not the general model and should not be replaced**: they are written only through the contact
  aggregate, their primary _is_ the contact's organization, they carry a job title, overlap exclusion and roles
  (`03-recommended-architecture.md:343-355`; `docs/associations.md:15-18`). "Associations are the model for every
  relationship a tenant defines, including contact-to-contact" (`:349-352`). Association attributes beyond the fixed set
  (a role on a link, a share) are **not in the first release**; the planned path is custom fields on an association
  type's registered object (`:389-391`, Planned). Associations are directional (`forward_label`/`reverse_label`); a
  symmetric type such as Spouse is not discussed (Absent).

---

### 12. Polymorphic references

- **Ruled pattern — Implemented (library 0.22.0, Platform 2026-10-18):** a reference field's configuration is either
  `{ targetObjectId }` or
  `{ typeField: 'customer_type', targets: { contact: 'crm.contacts', organization: 'crm.organizations' } }` (2–20
  targets), the row's **own type column** choosing the target; stored in `reference_type_field`,
  `reference_type_values`, `reference_target_object_ids` on `registered_fields` and `custom_fields`
  (`/Users/andrew/rhythm2/services-platform/docs/proposals/entity-aware-queries-polymorphic-references.md:29-48, 124-134`).
  The dataset declares `recordLinks: { customer_name: 'customer_id' }` so a display column opens the right record
  (`:53-67`). Compiled as `CASE` over the type column, no join; a target the caller may not query answers null
  (`:81-87, 139-145`).
- **What it refuses:** no display value, no relation fields (`customer_id.name` is `QueryFieldUnknown`), no hop; every
  owner needing display text stores it beside the id (`:44-48, 135-138`); a polymorphic reference is refused as a
  custom-field target (library changelog 0.22.0).
- **The older split pattern stays allowed**: Accounting's `customer_contact_id` / `customer_organization_id` (one null
  per row) "stay: they allow hops" (`:104-109`); CRM's `activity_links` and tasks'
  `subject_object_id`/`subject_record_id` are in the same family (`modules/queries/datasets.ts:78-85`). EAQ's earlier v1
  ruling ("two ordinary references … plus a computed display label", `docs/proposals/entity-aware-queries.md:564-574`)
  is superseded for new datasets by the polymorphic form.
- **For a membership holder or invoice customer that may be a household:** add `household` as a third target value on
  the polymorphic `customer_id`/`holder_id` (adding a type value is allowed; removing or retargeting is refused,
  changelog 0.22.0) **only if `crm.households` is a registered object**; if households are organizations (CRM-Q2
  option 1) nothing changes for any consumer — the strongest practical argument for that option.

---

### (a) Checklist — registering a new CRM entity end to end

Order matters; each step names the file or function. Steps marked **[P]** touch services-platform; everything else is
services-crm.

1. **Decide the capabilities row** (R2-ARCH-001 §6 table) — custom fields/forms/layouts only on Andrew's decision;
   otherwise purpose-built screens. `docs/standards/custom-field-capability.md:156-170`.
2. **Table(s)**: `modules/<module>/db/<table>.table.ts` with `(tenant_id, id)` PK, composite FKs inside CRM, text ids to
   other schemas, `snakeSystemColumns()`, `lockedCheck`, `uuidV7Check`/`configurationIdCheck`, list limits; migration
   with RLS ENABLE+FORCE+policy, `_normalize_text_trigger`, child-side indexes leading with `tenant_id`, and
   `SELECT platform.attach_query_description_stamp('crm.<config table>','tenant_id')` for any configuration table a
   description reads. (§5.1, §2.6.)
3. **History**: nothing per entity (triggers + `crm.record_revisions` with `record_type`); if the junction should appear
   on both parents' histories, register it twice in the capture stream as Platform does for associations
   (`docs/record-history.md:52-56`).
4. **Metadata object**: `modules/<module>/metadata/<entity>.object.ts` → `defineObject`; add to `aws/crm.metadata.ts`;
   `npm run metadata:manifest` regenerates. Reference targets must already be registered (`externalReferenceTargets`).
5. **Permissions**: `definePermissions(crmService.app, '<resource>', …)` in `aws/crm.service.ts`; catalog in
   `aws/api/crm.routes.ts`; routes name them; deploy's `manifest check/publish/activate`. Portal keys into Portal Member
   need Andrew. (§4.1.)
6. **Query object**: `modules/<module>/queries/<entity>.query.ts` → `defineQueryObject` with `permission`, `discovery`,
   `collections` (for `exists`), `configuredRelationships`, `quickSearch`, `source`/`computed` for counts; add to
   `aws/crm.queries.ts`; reader-grant migration generated from the registered columns (`aws/crm.query-grants.ts`, cf.
   `0019_regions.sql:46`).
7. **[P] `modules/queries/datasets.ts`**: add `'crm.<entity>': { permission: 'crm.<resource>.query', scope: 'tenant' }`
   and refresh Platform's copy of CRM's manifest fixture (`tests/integration/crm.query-registration.json`) so
   `dataset-permission-keys.spec.ts` passes. Without this only staff can query the dataset.
8. **Recipes**: `modules/<module>/queries/recipes/<slug>.recipe.json` (`<app>.<dataset>.<slug>`, versioned), checked by
   the repo's unit spec, registered by `recipes:register` after `queries:register`.
9. **[P] Quick Search** (if wanted): add the record type to the closed list
   `modules/quick-search/registry/record-types.ts` (datasetId, requiredPermission, datasetQueryPermission) and have
   CRM's change-event consumer invoke `platform_quick_search_indexer` for the new type
   (`docs/quick-search.md:41-45, 70`). Owner's `quickSearch` block in step 6 supplies the projection.
10. **Layouts** (only with capabilities): `[P]` `LAYOUT_PURPOSES` entry in `modules/layouts/purposes.ts` +
    `crm.<resource>-layouts.*` keys; CRM writes `platform.layout_assignments` via `rhythm_layout_assigner`
    (`docs/layouts.md:156-160, 265-277`).
11. **Merge**: declare a policy for every new column holding a contact/organization id in `lib/record-merge` coverage
    (`reassign-dedupe { businessKey: ['household_id','contact_id'] }`, `clear` for self-references); if the entity is an
    organization type, the organization participant already covers it. (§8.)
12. **Delete**: in the entity's delete transaction call `platform.apply_endpoint_deletion` if it can be an association
    endpoint; plan for the Planned deletion-impact contract (`recordReferences` on the lifecycle manifest).
13. **Tenant lifecycle**: declare every new table under an existing CRM participant's `resources` (purge disposition,
    export classification) or add a key **and** `[P]` its Platform remote declaration + reset classification; seed
    defaults (a "Household" organization type, "Spouse" relationship type) as a `SeedSet` with `system_code`, never a
    migration insert. (§5.3, §10.)
14. **Business events**: `defineEvent('crm.household.created' | 'crm.household_member.added' …)` in `aws/crm.events.ts`,
    publish in the write transaction; ask the Integration Platform to catalogue the keys; register trigger points for
    automation rules if tenants should automate on them.
15. **Communications**: add a purpose to `@rhythmsoftware/macroservice-communications` (shared package release →
    Platform pin), pick a `templateOwner`, send with `submitCommunicationRequest` and CRM-resolved recipients. (§6.)
16. **Data jobs**: `defineDataOperation` for `crm.households` (`insert`/`upsert`) and for the junction
    (`dependsOn: ['crm.households','crm.contacts']`, block on missing parents), register in CRM's operations registry;
    Data Run manifests must state `depends_on` explicitly. (§7.)
17. **Attention / responsibilities** (optional): register a responsibility and an attention definition about
    `crm.households`.
18. **Eligibility** (parked): no work until Andrew's go; when resumed, `withRelationships` + resolver paths.
19. **Audit**: `[P]` if a Platform configuration table is added, extend `AUDIT_ENTITY_TYPES` and regenerate the full
    CHECK list after merging master.
20. **Deploy order**: libraries → Platform (if any manifest key is new) → CRM migrations → `manifest activate` →
    `metadata:register` → `queries:register` → `recipes:register` → `extensions:register` → `eligibility:register`
    (`deploy.yml:170-185`).

### (b) Assessment — which Platform primitive a household should use

| Need                                                                                | Use                                                                                                                                                                                                                                                                                                                                             | Why / caveat                                                                                                                                                                                |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Naming / salutation templates** ("The Smith Family", "Mr and Mrs Smith")          | **No Platform primitive.** A computed `write: 'computed'` field on the household (as `fullName` is on contacts, `docs/metadata.md:230`), composed by CRM's write path; LiquidJS templates exist only inside Communications (`architecture.md:811-816`) and are not a naming service.                                                            | Absent; CRM owns the rule. A tenant-configurable salutation template would be a new CRM configuration row, not Platform.                                                                    |
| **Shared addresses**                                                                | **Reuse CRM's linked-address mode** (a contact's address row `linked` to an organization address through an affiliation, converting to `copied`/`disconnected` when the source goes — D-25) if households are organizations with member affiliations.                                                                                           | Only works for the organization option; a standalone household entity would need a new link source (Absent). Open Q-7 on affiliation end. `record-lifecycle/05-linked-addresses.md`.        |
| **Consent**                                                                         | **CRM's `crm.contact_communication_preferences`** per contact; Platform applies suppression only. Household-level consent does not exist and should not be invented (memory: CRM owns the consent ledger).                                                                                                                                      | A "household mailing" is N recipients each checked against their own consent.                                                                                                               |
| **Delegated portal access** (head manages members)                                  | **Derived assignments** (`platform.sync_derived_assignments`) with a new `crm` derivation source, a role at `resource` scope `household:<id>`, keys with `resourceType: 'household'`, `requireAccess` on portal routes.                                                                                                                         | Platform change (migration + grantor role) needed; no impersonation; one portal user per contact; member-record edits need a CRM ownership predicate; portal last.                          |
| **Query aggregates** ("members per household", "households with ≥2 active members") | `collections` + `exists` for membership criteria; a `source` view with `computed` counts (`member_count`) for sums/counts; `count_distinct` when a report joins one-to-many; **never** a `one_to_many` join in an Explorer recipe. If membership uses Platform associations, the generated families give `<type>_reverse` collections for free. | `rollup`/`measure` vocabulary is unbuilt; symmetric aggregation Absent.                                                                                                                     |
| **Import**                                                                          | `defineDataOperation` for households; for members either `platform.associations/upsert` (works today, after adding `crm.households` to its `dependsOn` and changing missing-endpoint to `blocked`) or a CRM `crm.household_members` operation with `dependsOn` and blocking. Data Run manifest `depends_on` explicit.                           | Atomic cross-type groups (E-11) not built.                                                                                                                                                  |
| **Person-to-person relationships**                                                  | **Platform association types** (`crm.contacts` ↔ `crm.contacts`, dated, labels both ways, cardinality) — merge, delete, history, import and query exposure already handled. Seed "Spouse", "Parent of" types as a CRM seed set of `platform.association_types` rows.                                                                            | Directional only (no symmetric flag); no attributes beyond the fixed set; no `system_code` on association types; `spouse_id` on contacts becomes a view over the association or is retired. |
| **Household membership**                                                            | Two viable shapes: (1) households = organizations of system type `household` + member **affiliations** (CRM-Q2 option 1: free merge, polymorphic customers unchanged, linked addresses, organization portal admin roadmap); (2) households = own entity + Platform **associations** `household_has_member`.                                     | (1) reuses the most; (2) is cleaner but loses household merge (targets are contacts/organizations only) and the polymorphic-customer story until a third target is added everywhere.        |

### (c) Questions the code could not answer

1. **CRM-Q2 is not ruled.** Which of its three options Andrew chooses decides whether Platform work is needed at all
   (option 1 needs none of §12's target changes). Branch `crm-architecture` only.
2. **Symmetric relationship types.** Associations are directional; is "Spouse" one type with two labels
   ("Spouse"/"Spouse") and a `one_to_one` cardinality, or does Platform need a symmetric flag so the query families do
   not generate two fields? Nothing documented.
3. **Attributes on a relationship** (a household role: head, adult, child, dependent). Associations carry none beyond
   dates/primary/position/notes; the planned path (custom fields on the association type's object) is unbuilt. A CRM
   junction table is the alternative.
4. **Age-based membership rules** ("a child is a dependent until 26") need the eligibility engine, which is PARKED; who
   owns the interim rule and where it runs is open.
5. **Household merge** when households are their own entity: no merge target exists; would Andrew accept "no household
   merge" for launch?
6. **Portal access by household head** needs a `crm` derivation source in Platform (`security_assignments` CHECK +
   grantor role migration) — a Platform milestone that is not planned anywhere.
7. **Deletion of a member contact** under the Planned record-lifecycle: whether a household row with `restrict` should
   block, or `detach` and raise an attention item — the proposal's participant inventory does not cover households.
8. **Whether E-11 (atomic dependency-ordered import groups) and E-17 (relationship-aware criteria) will be built in the
   Platform engines or waived** — both are CRM-acceptance rulings recorded only on the `crm-architecture` branch; no
   Platform owner is named on master.
9. **Membership's "family membership" holder model** (polymorphic `member_kind`/`member_id` in
   `snapshot-architecture.md:678-679`) — whether Membership is prepared to add a `household` target or expects an
   organization.
10. **Quick Search for households** needs a Platform edit of a closed list; whether the proposal wants households
    searchable at all.

## 25. R5 — Console and portal architecture for Households (research report)

## R5 — Console and portal architecture for Households (research report)

Written 2026-10-21 04:30 ET. Read-only; nothing was modified. Every claim carries a label: **Implemented** (read in
code), **Decided** (a ruling Andrew recorded), **Planned** (in an approved or proposed record, not in code), **Absent**
(looked for, not found).

### Sources and revisions

| Source                                                                         | Revision read                                                                                                                                                                     | Note                                                                                  |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `~/rhythm2/react-console` (the Reactor console, Next.js 15 app router + React) | `13e4bcee9` (master, 2026-10-21 02:58 ET); `origin/master` is `df32ee08a`, 8 commits ahead, all Committees/Accounting acceptance and wire-name commits, none touching CRM screens | the console                                                                           |
| `~/rhythm2/rhythm-web`                                                         | `5ecc208` (master)                                                                                                                                                                | the Web Experience Platform monorepo: member portals, public websites, CMS (see §6)   |
| `~/rhythm2/services-websites`                                                  | checkout `f038650` (branch `ws-install-content-workflows`); chapters 17–24 read from `origin/master` `9c62624`                                                                    | Websites architecture                                                                 |
| `~/rhythm2/services-submissions`                                               | `78b2c11` (master = origin/master); `docs/architecture/` read from `origin/master` (the working tree does not have the folder)                                                    | Submissions architecture, 25 chapters                                                 |
| `~/rhythm2/services-platform`                                                  | `0614c54cf` (master)                                                                                                                                                              | PAF banner, identity docs, Membership architecture                                    |
| `~/rhythm2/worktrees/services-platform/crm-households`                         | `d63f10b5d`                                                                                                                                                                       | CRM architecture chapter 09 and evidence e04 (read first; cited rather than repeated) |
| `~/rhythm2/worktrees/services-platform/mobile-apps-architecture`               | `2dab6a5b4`                                                                                                                                                                       | `docs/proposals/mobile-apps/` (28 chapters)                                           |
| `~/rhythm2/services-crm`                                                       | `eac3df30`                                                                                                                                                                        | only grepped for guardian/minor                                                       |

Where e04 (`crm-households/docs/proposals/crm/evidence/e04-crm-console-assessment.md`, written against console
`1f345862b` on 2026-10-19) already answers a question, it is cited; where the code at `13e4bcee9` has moved on, the
difference is stated.

---

## CONSOLE

### 1. App structure

#### 1.1 Route layout for CRM — Implemented

Every tenant screen lives under `app/(console)/console/[tenantId]/`; the tenant layout resolves the id once
(`/Users/andrew/rhythm2/react-console/app/(console)/console/[tenantId]/layout.tsx:1-16`, `TenantIdGate`). The CRM routes
at `13e4bcee9`:

```
crm/contacts/page.tsx                        Contacts explorer
crm/contacts/new/page.tsx                    New contact (FormEngine)
crm/contacts/[id]/layout.tsx                 merged-record redirect (D-20)
crm/contacts/[id]/page.tsx                   Contact 360 (record-view engine)
crm/contacts/[id]/edit/page.tsx              Edit contact (FormEngine)
crm/contacts/[id]/orders/{new,[orderId]}     order prototype, mock only (D-53)
crm/organizations/page.tsx                   Organizations explorer
crm/organizations/new/page.tsx               New organization
crm/organizations/[id]/{layout,page}.tsx     Organization 360 (bespoke)
crm/activities/{page,[id]/page}.tsx          Activities explorer and record
crm/dataQuality/**                            duplicates, dismissed, merges history
crm/merges/new, crm/merges/plans/[planId]    merge workspace
```

The Contact 360 page only resolves the record and hands off to the engine
(`app/(console)/console/[tenantId]/crm/contacts/[id]/page.tsx:34-47`): with `CRM_LIVE` it renders `ContactLiveScreen`
(browser-side read, because only the browser holds a token); otherwise sample data with a `SampleRecordMissing` notice
(D-06).

#### 1.2 How an application's navigation is registered — Implemented

- The **domain registry** `lib/console/domains.ts` is the single source: one `DomainDefinition` per application with
  `key` (also the permission domain and the "enabled-domain" key), `permission`, `settingsHref` and `items` built with
  `built(...)`/`planned(...)` (`/Users/andrew/rhythm2/react-console/lib/console/domains.ts:90-124`). CRM is
  `domains.ts:126-142`: Contacts, Organizations, Activities (built), Relationships (planned), Data Quality (built).
- `components/console/nav-config.ts:110-135` turns the registry into sidebar sections; a domain shows once its first
  screen is built (`hasBuiltItem`, Andrew 2026-10-20).
- The **noun registry** `lib/console/record-types.ts:1-78` gives each record type its explorer URL, record URL, label,
  icon, permission, `fieldsRecordType` and backend `objectId`; `contact` and `organization` are `record-types.ts:77-78`.
  Pickers, recents, Quick Search, related-record cards and Jazz all route through `recordHref`. A record type **must
  name a navigation item** of its domain (`define()` throws otherwise, `record-types.ts:52-58`).

#### 1.3 How "apps" are licensed / switched on — Implemented

- The security context's `applications` list (what the tenant has licensed, answered by Platform) switches domains on:
  `enabledDomainsOf` maps application keys to domain keys through `DOMAINS_OF_APPLICATION`
  (`/Users/andrew/rhythm2/react-console/lib/security-context/entitlements.ts:60-107`). A staff wildcard never opens an
  application the tenant does not have (`:96-99`).
- `domainEnabled` (`lib/authz/evaluate.ts:81-83`) and `visibleSections` (`lib/console/use-visible-nav.ts:15-19`) hide a
  section whose domain is off or whose keys the person lacks; hidden, not disabled.
- A new noun under CRM therefore needs no licensing work: it inherits the `crm` application. A new _application_ would
  need a Platform application key, an entry in `DOMAINS_OF_APPLICATION`, a domain in `domains.ts`, and permission keys
  in `lib/authz/permissions` with backend aliases.

#### 1.4 The one live/mock switch — Implemented (fixed since e04)

- `resolveMockMode` (`lib/config/mock-mode.ts:38-62`): `NEXT_PUBLIC_MOCK_MODE`/`NEXT_PUBLIC_MOCK_PREVIEW`; refused
  outright on a production deployment (D-06). `pnpm dev` defaults to mock; `pnpm dev:live` forces both flags to
  `"false"` so no env file can re-enable mock (`scripts/dev.mjs:31-57`).
- **`CRM_LIVE = !MOCK_MODE`** (`lib/api/crm.ts:128-134`): "the console has one switch, every application live or every
  application mock (Andrew, 2026-10-19); CRM's own `NEXT_PUBLIC_CRM_API` is retired and ignored". e04 §"Live/mock
  mechanism" and its gap #5 (two switches) are **superseded**: the comment records that before 2026-10-20 every Vercel
  build drew the 360s from sample data.

#### 1.5 API client layer — Implemented

- `lib/api/crm.ts:1-28` is the design note: a generated snake_case client (`lib/api/generated/crm`, from
  `openapi-ts.crm.config.ts`), whose transport converts every answer to camelCase; console shapes go out through
  `toWire` (`lib/api/wire-case`), so the console is camelCase inside and snake_case at the transport. CRM paths carry
  the tenant second (`/contacts/{tenantId}/{id}`); a contact is answered whole (emails, phones, addresses, preferences,
  affiliations embedded); errors are the macroservice error body, a stale write is 409; creates are retry-safe by client
  UUIDv7.
- `createCrmApi` (`lib/api/crm.ts:379-440`) is the method surface: `getContact`, `listContacts`, `createContact`,
  `replaceContact`, `patchContact`, `deleteContact`, `getOrganization`, `listOrganizations`,
  `listOrganizationAffiliations`, the type lists and `listRegions`; `createBrowserCrmApi(tenantId)` (`:446-448`). Lists
  are cursor-paged only, 100 a page, walked whole up to `CRM_LIST_CAP = 2_000` (`:71-74`); there is no server search on
  CRM's own lists — searching goes through Platform's query layer (see pickers, §2.8).
- Hooks and mappers per feature: e.g. `lib/affiliations/use-organization-affiliations.ts`, `lib/contacts/crm-live.ts`,
  `lib/contacts/record-operations.ts`; `docs/crm-api-client.md` documents the pattern.

### 2. Contact 360 and Organization 360

#### 2.1 The composable card architecture (Contact 360) — Implemented

`docs/record-views.md:1-40` is the authoritative description. Mechanism:

- A **Record View** is JSON: `settings` + `layout` (`header` with `arrangement`, `body` of `section` and `tabs` blocks).
  The default Contact 360 is `/Users/andrew/rhythm2/react-console/lib/record-views/views/contact-360.json`
  (`recordType: "crm-contacts"`). Header cards: `contact.identity`, `contact.engagement-score` (`:20-35`). Body: a
  `duplicates` section (card `contact.duplicates`, permission-gated), then one `tabs` block with groups `summary`,
  `relationship`, `learning`, `financial`, `engagement`, `records` (`:60-82`) and tabs overview, membership, chapters,
  committees, volunteering, engagement, events, certifications, education, credits, invoices, donations, accounting,
  communications, activity, notes, documents, relationships. The Overview tab's cards: `contact.full-name`,
  `contact.emails`, `contact.addresses`, `contact.organization-affiliations`, `contact.relationship-timeline`,
  `contact.phones`, `contact.web`, three `record.field-group`s (one is "Status & Relationships" with `spouse_name`,
  `:355-375`), `contact.custom-values`, `record.audit`, `custom-records.owned`.
- A **card type** is
  `defineCard({ type, name, description, icon, recordTypes, configSchema (zod), component, display: { chromes, defaultChrome, regions } })`
  (`lib/record-views/registry.ts:23-55`); every card is listed in the directory
  `components/record-views/cards/index.ts:60-80`; implementations in
  `cards/{shared,contact,committee,order,accounting, custom-record}/`. Shared cards reusable by any record type:
  `record.kpi-strip`, `record.table`, `record.field-group`, `record.audit` (`cards/shared/`).
- **Visibility rules** on tabs, cards and actions: `attribute` (op eq/neq/in/truthy/…), `capability`, `permission`,
  `all`/`any`/`not` (`lib/record-views/schema.ts:51-86`).
- **Actions** are per tab (`primary`, `secondary`, `more`) and either navigate (`href`, e.g. `edit`,
  `/crm/merges/new?objectType=contacts&recordIds={id}&returnTo={path}` at `contact-360.json:213`) or open a registered
  **panel** (`panel: "record.archive"` etc.). Panel ids are a closed registry (`lib/record-views/panels.ts:35-55`;
  constants `components/record-views/panels.tsx:32-49`: `record.version-history`, `record.change-type`,
  `crm.log-activity`, `record.lock`, `record.unlock`, `record.lock-history`, `record.archive`, `record.unarchive`,
  `record.copy-link`, `crm.create-task`, `custom-record.publish/unpublish`, plus `contact.merge` and the `order.*`
  panels). Archive/Unarchive use an `attribute archivedAt` rule (`contact-360.json:224-245`) — e04's "Archive hidden, no
  handler" is superseded.
- The **engine** `components/record-views/record-view.tsx` knows nothing about contacts: every card comes from the
  registry, placement from the view, data from the **record context** (`lib/record-views/context.ts`), built by
  `lib/record-context/contact.crm.ts` for live. Actions without `href`/`panel` are toast-only and are removed on a live
  console (`components/console/record/action-toolbar.tsx`, e04 addendum A).
- **Do Platform record layouts drive it?** — **Absent**. `lib/record-views/resolve.ts:20-61` returns the bundled JSON
  per record type (`crm-contacts`, `commerce-orders`, `committees-committees`) with a `registerViewSource` hook "for
  tenant- or record-specific overrides later". The layout designer saves to `localStorage` (e04 B, CAP-CFG-012). The CRM
  architecture rules that 360 cards, actions and forms go through the shared metadata systems (09 §1, "Andrew's ruling
  §2.5") — **Decided**, not yet wired to Platform `modules/layouts`.
- **Tabs not available**: `CRM_CONTACT_UNAVAILABLE_TABS` (`lib/record-context/contact.crm.ts:114-128`) still lists
  membership, chapters, volunteering, engagement, events, certifications, … (14 of 15 from e04; `committees` is now a
  live card `contact.committee-service`, visibility `committees.appointments.view`). A tab shows "not available yet" via
  `tabStatus` (`:396-397`).
- **History/audit panel**: `record.version-history` panel + `record.audit` card; adapters exist for contacts, activities
  and custom records only (`lib/record-history/adapters.ts:21-27`); **organizations Absent** (the comment says one
  adapter + one JSON entry is the whole of adding one).

#### 2.2 Organization 360 — Implemented, bespoke

`components/console/organization/` (`organization-screen.tsx`, `organization-header.tsx`, `organization-details.tsx`,
`organization-contacts.tsx`, `organization-hierarchy.tsx`, `organization-tab-panel.tsx`, `organization-sections.tsx`,
`organization-affiliations*.tsx`, `affiliation-dialogs.tsx`) is hand-built, not a record view
(`app/(console)/console/[tenantId]/crm/organizations/[id]/page.tsx:1-40`). Tab actions are a typed table `orgActions`
(`organization-sections.tsx:84-120`): on live only actions with a panel/href are offered — Change Type, Log Activity,
Lock/Unlock/Lock History, Archive/Unarchive; **Edit Organization has no form** (primary without href, `:86`), Add
Contact/Assign Role/Email All/Export/Add Subsidiary/Set Parent are demos (comment `:78-83`). CRM 09 §5 rules it must
move onto the record-view system — **Decided**, not built.

#### 2.3 "Standard layout" conventions — Implemented

Two page widths only (`docs/page-layouts.md:1-35`): `full` (record pages, editors) and `standard` (centered 1100 px,
configuration lists via `ConfigListScreen`), chosen by rendering through `PageLayout`
(`components/console/page-layout`). 360 views and designers collapse the sidebar to its icon rail
(`docs/page-layouts.md` "Pages that collapse the sidebar"). Memory "Accounting UI standards 2026-10-20" ("Standard
layout", Name before Code, Save returns to list, BU context carries) is the Accounting application of these.

#### 2.4 List pages and the one-row toolbar — Implemented

- **Explorer lists** (Contacts, Organizations, Activities, Tasks) are `defineRecipeExplorer` definitions over a Platform
  **query recipe**: `components/console/contacts/contacts-definition.tsx:78-146` —
  `recipeId: "crm.contacts.with-organizations"`, `noun`, `routes.base`, `readPermission`, `supportingFields`,
  `columnOverrides`, `extraColumns` (unavailable ones say why, `:22-30`), `presence` chips (incl. `ARCHIVED_PRESENCE`),
  `views` (Recently Viewed pinned; All Contacts unlisted base, Andrew 2026-10-13), `pageSizes`, `assistant` (Ask rules),
  `actions { primary, bulk, row }`, `export`. The shared core is `lib/record-explorer/` + `components/record-explorer/`
  (view bar, columns menu, bulk bar, save-view dialog, server views); AGENTS.md:252 forbids per-noun branches in it.
  Page sizes 10/25/50/100, selection/export cap 10,000 (e04 A).
- **Configuration lists** use `ListToolbar` (`components/configuration/list-toolbar.tsx:15-60`): `leadingFilters`
  (Business Unit first, then context selectors) → search box → Refresh icon → Show Inactive (button or switch) → result
  label → `trailingActions`; drawn by `ConfigListScreen` (`config-list-screen.tsx:31`) with
  `PageLayout width="standard"`. This is the "BU → filter → Search → Refresh → Show Inactive" one-row standard.

#### 2.5 Tables, pagination, saved views — Implemented

Explorer table `components/record-explorer/explorer-table.tsx` with column picker, pinned order, server views
(`use-server-views.ts`, `view-manager.tsx`, `save-view-dialog.tsx`), cursor paging and restored scroll; config lists use
`components/configuration/config-table`; the primitive is `components/ui/table.tsx`. Saved/pinned views are Platform
Explorer's (CRM 09 §3).

#### 2.6 Forms — Implemented

- Console record forms are **FormEngine JSON** (Andrew authorized 2026-10-10, AGENTS.md:220). The contact form is
  `lib/forms/definitions/contact-admin.json` (`objectId: crm.contacts`, `context: admin`), generated by
  `build-contact-admin.ts`: `RhythmIdentityBand` (photo, prefix…credentials, status, dateOfDeath, jobTitle, name
  preview, duplicate hint) then `RhythmTabs` Overview (email/phone/address lists), **Affiliations
  (`RhythmOrganizationAffiliations` bound to collection `affiliations`)**, Communication, … The component registry is
  `components/forms/registry/index.ts:178-182`; contact-specific components in `components/forms/contacts/`.
  `organization-admin.json` exists but no organization edit route uses it (e04 A; §2.2).
- Validation on the server (409 on stale `sysVersion`, D-05/D-35), draft restore (D-17), the editor bar Andrew approved
  (`docs/editors.md`, AGENTS.md:188). Create/Edit live writes `POST`/`PATCH /contacts/{t}` with client UUIDv7
  (`lib/contacts/record-operations.ts`, e04 A).

#### 2.7 Dialogs / pop-ups — Implemented

`components/ui/dialog.tsx`, `sheet.tsx`, `alert-dialog.tsx`. The affiliation editor is a **Sheet**
(`components/affiliations/affiliation-editor.tsx:24-31`); the organization-side Add Person is a **Dialog**
(`components/console/organization/affiliation-dialogs.tsx:105-122`); record panels (archive, lock, change type, version
history) are registered panels (§2.1); Log Activity is a sheet (`components/activities/log-activity-sheet.tsx`).

#### 2.8 Record pickers — Implemented

- Shared control `components/record-pickers/record-picker.tsx:17-60`:
  `RecordPicker { source, value, onChange, filters?, valueLabel?, compact?, clearable }` — a typeahead over one noun's
  **PickerSource** with recents; also `record-multi-picker.tsx`, `dataset-picker.tsx`.
- A source is `createPickerSource({ recordType, adapter, toOption, supportsInactive, createPermission })`
  (`lib/record-pickers/source.ts:26-64`): the `recordType` must be in the noun registry, the adapter is the record
  type's explorer adapter, so "a picker and the list screen always agree" (`types.ts:37-55`). CRM sources:
  `crmOrganizationPickerSource(tenantId)` (whole list held, cap 2,000, matched in the browser; `sources.ts:63-80`,
  `crm-adapter.ts:1-16`) and `crmContactPickerSource` (**queries Platform dataset `crm.contacts`**,
  `crm-adapter.ts:186-190`) — so the spouse picker is no longer sample-backed (e04's DEF-CRM-0026 superseded).
- **Inactive handling** is one rule (`lib/record-pickers/inactive.ts:1-50`, Andrew 2026-10-11): never offered as a new
  choice; a held value stays visible labelled `Name (inactive)` or `Name (archived)` and saves unchanged; there is no
  "include inactive" checkbox in a picker (that is Quick Search's). Contact-form wrappers: `OrganizationPicker`,
  `ContactPicker { excludeId }`, `useContactNames` (`components/contact-editor/fields/entity-pickers.tsx:157-230`).

#### 2.9 Quick Search — Implemented, closed type list

`QuickSearchRecordType = "contacts" | "organizations"` (`lib/quick-search/types.ts:16-32`); the backend declares the
index types (`:6-8`), Platform issues an Algolia key, the browser queries Algolia (e04 A). Destinations resolve through
`recordHref` and `recordViewAvailable` (`lib/quick-search/destinations.ts:50-68`). Adding a type is a Platform index
declaration plus this union, `CONSOLE_RECORD_TYPE_OF` and the noun registry.

#### 2.10 Explorer embedding, related lists, timeline — Implemented

Related lists on 360s are **not** embedded Explorers: Organization 360's Contacts tab reads the paged affiliations API
with URL-held filters (`organization-affiliations-section.tsx:29-52`); Contact 360 related data comes from the record
context's `datasets` and the shared `record.table`/`record.kpi-strip` cards; the timeline is
`contact.relationship-timeline` (`cards/contact/activity-cards.tsx`). `custom-records.owned` embeds owned custom
records.

#### 2.11 Duplicate / merge screens — Implemented (partly)

Routes `crm/dataQuality/*`, `crm/merges/new`, `crm/merges/plans/[planId]`; components `components/duplicates/*`; Contact
360 "Merge with…" links to the workspace with the record pre-filled (`contact-360.json:213`), and a `contact.merge`
panel is registered. e04 C: no second-record picker (D-03), no field-level outcome (D-30), no "Create anyway" (D-32) —
still open at this revision as far as the listed files show.

### 3. Affiliation editing on Contact 360 — the "add a household member" analogue — Implemented

Precisely, four layers:

1. **Contact 360 card (read-only).** `components/record-views/cards/contact/affiliations-card.tsx:36-93`
   (`type: "contact.organization-affiliations"`, config `showManage`). It renders
   `OrganizationAffiliationsSection readOnly` with rows ordered primary → current → upcoming → past and a **Manage**
   link to the contact editor when the viewer holds `crm.contacts.edit` (`affiliationsManageHref`, `:32-34`). Note: it
   still imports `getOrganization` from `lib/contact-editor/mock/reference` to name organizations (`:15,78`) — a sample
   reference read on a live card (same smell e04 A recorded for the editor).
2. **Shared section** `components/affiliations/organization-affiliations-section.tsx:14-28`: props `rows`, `readOnly`,
   `onAdd`, `onEdit`, `onMakePrimary`, `onRemove`, `issues` (per row), `noPrimaryNotice`, `manageHref`. It shows counts
   ("2 Current · 1 Upcoming · 3 Past"), an All/Current/Upcoming/Past toggle (D-10, `:30-59`), an **Add Affiliation**
   button when editable (`:61-72`), group dividers (`:165-181`), and offers Make Primary only for a current,
   non-primary, open-ended row (`:88-93`).
3. **Row** `components/affiliations/affiliation-row.tsx:34-62`:
   `AffiliationRowView { organizationName, organizationHref, organizationArchived (D-07 badge), typeName ("(inactive)" suffix), jobTitle, roles[{name, dates?}], startDate, endDate, status current|past|future, isPrimary, links[] }`;
   Edit / Make Primary / Remove in a dropdown; `density` compact for an organization's people list; `title` override for
   that list.
4. **Editor sheet** `components/affiliations/affiliation-editor.tsx:33-110`:
   `AffiliationDraft { organizationId, organizationName, affiliationTypeId, isPrimary, jobTitle, startDate, endDate, notes, roleIds }`;
   props `mode add|edit`, `renderOrganizationPicker` (the console picker injected), `affiliationTypes` (active + the
   held inactive one labelled), `contactRoles`, `supplies` (what the affiliation supplies today), `defaultLinks`
   (`DefaultLinkPreview` with state new/replaces/skipped/missing, "Link Business Address to …"), `messages` per control
   (error blocks, warning does not), `primaryDisabledReason`, `primaryNotice` ("Ending X leaves Y with no primary…"),
   `stopsResolving` (linked rows that would stop resolving).
   - **Form binding** `components/forms/contacts/affiliation-components.tsx:1-16`: `RhythmOrganizationAffiliations`
     binds the form's `affiliations` collection; linked contact information is **by reference**
     (`lib/affiliations/contact-links.ts`); making another primary moves the flag inside the list, so it is part of the
     contact's one save; the write becomes explicit add/edit/end/remove directives (`lib/affiliations/contact-write.ts`
     `toAffiliationWrite`, D-09; `contact-editor.tsx:43,539`).
5. **Organization side (the other direction).** `components/console/organization/organization-affiliations.tsx:88-126`
   (`OrganizationAffiliationsCard`, "Add Person", `onEdit`, `onEnd`) over the paged API;
   `affiliation-dialogs.tsx:102-178` `AffiliationDialog` ("Add Person to {organization}" / "Edit Affiliation") with
   `renderContactPicker`, roles each with their own dates and inactive-labelled role names, and `EndAffiliationDialog`.
   e04's DEF-CRM-0187 ("Add Person can't set a role") appears addressed here — re-verify live.

Summary for Households: "add a member" already has a complete pattern — a list section with counts and status filters, a
row with primary/roles/dates/status, an add/edit sheet with an injected picker, a dialog for the reverse direction, and
a write model of explicit directives with a version.

### 4. The spouse field — Implemented (interim), Planned (migration)

- Form: `bound("RhythmRecordPickerField", "spouseId", { record: "contact" })`
  (`lib/forms/definitions/build-contact-admin.ts:194`), rendered by `ContactPicker` with `excludeId` (self never
  offered) over the Platform `crm.contacts` dataset; Review Changes names the spouse through `useContactNames`
  (`entity-pickers.tsx:208-230`).
- 360: field `spouse_name` in the "Status & Relationships" field group (`contact-360.json:355-375`), catalog entry links
  to the spouse's 360 and appends "(Archived)" (`lib/record-views/fields/contact-fields.ts:51-54`; test
  `lib/record-views/__tests__/spouse-archived.test.ts`).
- Rulings: D-19 spouse is reciprocal, target one canonical relationship (e04 §2); CRM 05 §5 migrates `spouse_id` to a
  symmetric relationship type and retires it (`crm/05-functional-architecture.md:298-316`) — **Planned, CRM-M21**.

### 5. What a new entity (Household) needs in the console

Two shapes are on the table. **(A)** CRM 05 §5's recommendation: a household is an **organization of a system type
"Household"** with affiliations of type "Household member" ("no new party kind",
`05-functional-architecture.md:313-315`; CRM-Q2 option 1 Recommended, `13-decisions.md:66-80`; CRM-M21 walkthrough
"create a household and add members", `15-completion-roadmap.md:195-198`) — **Planned**. **(B)** a first-class household
entity. The console cost differs:

| Need              | Shape A (organization of type Household)                                                                                                              | Shape B (new entity)                                                                                                                                                        |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List / search     | Organizations explorer + a type filter (recipe `crm.organizations.with-types`); a saved/pinned view "Households"                                      | new `defineRecipeExplorer` definition + CRM dataset and recipe registered with Platform queries (09 §1)                                                                     |
| 360               | Organization 360 — bespoke, no edit form, no history adapter, 500-org hierarchy scan (e04 #4); would show "Household member" rows in its Contacts tab | new view JSON in `lib/record-views/views/` + `resolve.ts` entry, a record-context builder, household cards via `defineCard`, page + merged-redirect layout, history adapter |
| Forms             | the missing organization editor (D-36 work)                                                                                                           | FormEngine JSON definition + build script + any new form components                                                                                                         |
| Picker            | `crmOrganizationPickerSource` with a filter, or a dedicated source                                                                                    | `createPickerSource` + adapter; noun registry entry                                                                                                                         |
| Nav entry         | none needed (or a planned item "Households" under CRM, `domains.ts:135-141`)                                                                          | same                                                                                                                                                                        |
| Quick-search type | organizations already indexed; a household is found as an organization                                                                                | backend index declaration + `types.ts` union                                                                                                                                |
| Explorer dataset  | exists                                                                                                                                                | CRM registers a dataset                                                                                                                                                     |
| Contact 360       | affiliations card already lists it (type name "Household member")                                                                                     | a new card `contact.household`                                                                                                                                              |
| Permissions       | `crm.organizations.*`                                                                                                                                 | new `crm.households.*` keys + backend aliases                                                                                                                               |

Approval constraints — **Decided**:

- **Console changes need Andrew's explicit direction**: `AGENTS.md:46-53` ("Do not modify the React console … unless
  Andrew specifically directs changes there"; a backend/API change or feature brief does not authorize it).
- **UI change approval (2026-10-11)**: `AGENTS.md:114-130` — existing v0-designed interfaces are the approved baseline;
  wire by adapting data to existing components; no generic tables/forms/dialogs in place of approved designs; a control
  whose backend is missing stays inert with "isn't available yet"; significant changes need approval first.
- **R2-DEV-001** (`AGENTS.md:67-86`): no substantive milestone implementation without an approved plan; acceptance by
  Andrew in a live console build after the developer runs the walkthrough in Chrome.
- CRM 09 §1: 360 cards, record actions, forms, field editors, audit views and Explorer recipes go through the shared
  metadata systems, never CRM-only components.
- Custom fields opt-in per entity (R2-ARCH-001, `AGENTS.md:94-100`): a household gets custom fields only if Andrew adds
  it to the approved list.

---

## PORTAL

### 6. What the portal is today

#### 6.1 rhythm-web — Implemented, held

`~/rhythm2/rhythm-web` is the **Web Experience Platform** monorepo: one Next.js application per customer under
`sites/<slug>` holding all of that customer's sites (member portals, public websites, event sites), generated from
`@rhythm/web-runtime`, with the CMS (Payload) in `apps/cms` (`/Users/andrew/rhythm2/rhythm-web/README.md:1-40`). The
architecture is services-platform `docs/proposals/web-experience-platform/` (accepted 2026-10-11) and services-websites
`docs/architecture/` chapters 01–24. Member-facing packages, each in "four layers" (server actions → hooks → unstyled
parts → default UI):

- `@rhythm/web-account` — sign-up, sign-in, sign-out, password reset/change, e-mail verification, "Is this you?"
  (`packages/web-account/README.md:1-20`).
- `@rhythm/web-contacts` — **my profile, my contact information, my organization, the people at my organization, the
  member and organization directories** (`packages/web-contacts/README.md:1-12`). Every route it calls is an assumption
  on CRM's portal API (`API-ASSUMPTIONS.md`); the real routes are CRM-M22's (§6.4).
- `@rhythm/template-member-portal` — the standard portal: ~65 screens, 20 written, the rest stubs
  (`packages/template-member-portal/README.md:1-40`); written screens include `routes/organization-roster` "Its people,
  if I may manage them".

Status per Websites ch. 22 §7 (origin/master `9c62624`): portal site per customer live in dev (Rubber, Northfield);
sign-up/sign-in; "Is this you?"; sealed session; account package; 19-screen template — all **Implemented, held** ("No
portal work until Andrew directs it", 14 §1, 18 Oct). Websites ch. 06 §7: the portal shows no memberships, invoices,
registrations yet; no SSO across a customer's sites (gap G12).

#### 6.2 The console's `(portal)` route group — Implemented, Accounting only

`app/(portal)/portal/[tenantId]/{invoices,payments,paymentMethods,statement}` — Accounting self-service only; no CRM
pages (e04 §1 "Portal" row, addendum D). `lib/portal/member.tsx:1-12` is a **development stand-in**: in mock mode the
member is a seeded contact; against dev the console session "acts as" a contact chosen with `?member=<contactId>`
("never a product behaviour"). It documents that a real portal session carries the contact as `cid`.

#### 6.3 Member identity and linking to a CRM contact — Implemented

- One Cognito **portal pool per customer account**, separate from the console pool; members self-register; a
  post-confirmation trigger creates the Rhythm user with the **Portal Member** role (Websites `06-portals.md:43-69`;
  Platform `modules/security/aws/api/portal-identity.routes.ts`).
- **"Is this you?"**: exact e-mail match links silently; otherwise CRM returns up to three likely contacts
  (`crm.portal_contact_candidates`), masked hints, a 6-digit code (5 attempts), or "None of these are me" creates a
  contact flagged for staff duplicate review (`06-portals.md:71-96`; routes `GET /security/{t}/portal/me/contact_match`,
  `…/challenge`, `…/verify`, `…/decline`). Tested end to end 18 Oct.
- The link is `platform.users.contact_id` — **a soft column, no FK, no uniqueness; a contact merge relinks it** — signed
  into the portal security context as the optional claim **`cid`**; a console context never carries `cid`
  (`/Users/andrew/rhythm2/services-platform/docs/security/portal-contact-claim.md:1-30`, decision D-26). Services use
  `ctx.caller.contactId` / `requireContactId` (`:22-30`). Index `users_contact_idx (tenant_id, contact_id)` only
  (`modules/security/db/users.table.ts:104`).

#### 6.4 Account / profile self-service — Planned (CRM-M22, last)

- CRM gap matrix rows PO-01..PO-05 are all **Missing**: member's own profile with name/credential locks; member's
  organization profile; organization roster and child organizations; **organization administrator through Security
  grants derived from affiliation roles (CRM-ADR-11)**; profile-confirmation step for application processes
  (`crm/14-gap-matrix.md:243-247`). L1-23 "portal delegates and contact login profiles" were non-functional in 1.0 and
  would be designed new only by decision under PO-04 (`:284`).
- CRM-M22 "Portal CRM self-service" is **last of all scheduled CRM milestones**, depends on M9..M19, M21, M23, "built on
  Andrew's portal components"; walkthrough: member updates profile; **organization administrator pages a 10,000-person
  roster, adds, edits and detaches a contact, manages a child organization** (`crm/15-completion-roadmap.md:200-207`).
- CRM 05 §7: portal authorization is Platform Security's — a scoped organization-administrator capability derived from
  affiliation roles through a published CRM read; CRM routes enforce what Security resolves; staff "log in as member" is
  out of CRM scope, a Platform capability (`05-functional-architecture.md` §7).
- Membership architecture: P6 "Derived authority" (committee appointments grant portal keys) and **P9 "Console and
  portal wiring (Andrew-directed)"** (`membership-architecture/11-implementation-phases.md:40-43,161-163`); D-36
  "Committees derive portal authority only" (`README.md:153`). No family/household membership anywhere in the 16
  chapters (**Absent**).

#### 6.5 Organization-administrator roster management (the "manage my household" analogue) — Planned, with a built client-side assumption

rhythm-web `packages/web-contacts/API-ASSUMPTIONS.md` specifies what `@rhythm/web-contacts` already calls:

- Permission keys on the Portal Member access document: `crm.contact.self.read/update`,
  `crm.contact.self.contact-information.manage`, `crm.organization.read/update`, **`crm.organization.contacts.manage`**,
  `crm.directory.read`; the UI reads `mayEdit`/`mayManageContacts` as a courtesy only, CRM refuses the write regardless
  (`:37-55`).
- Routes: `GET/PUT /contacts/{t}/portal/me/organization`; `GET /…/organization/contacts` (paged);
  `POST /…/organization/contacts` (creates a contact **at the caller's own organization**; the organization is never in
  the body); `PUT /…/contacts/{contactId}` (a colleague's row, 404 if not at that organization);
  `DELETE /…/contacts/{contactId}` **ends the listing, never deletes the person** (`:106-120`).
- Two rules: removal detaches and the record survives; **an administrator cannot remove themselves** (409 with advice,
  not 403) (`:122-128`).
- Deliberately absent: any route taking the caller's own contact id (every self route is `me`); any route writing
  **another member's personal record** (roster routes write employment details only, never privacy choices, password or
  e-mail of record); any console-tier route callable on a member's behalf (`:159-167`).

For Households the direct transfer is: a "household administrator" key derived from a household affiliation role; roster
routes under `/portal/me/household/...`; detach-not-delete; cannot-remove-self; and the open question of whether a
household administrator may edit a member's _personal_ record (the organization design says no).

#### 6.6 Guardian / minor / consent handling — Absent

Grepped rhythm-web (`packages`, `docs`), services-websites chapters 01–24, services-submissions chapters 01–25, the CRM
proposal (18 chapters), the Membership architecture (16 chapters), the mobile proposal (28 chapters) and services-crm
`docs/`: **no guardian, minor, dependant or parental-consent rule anywhere**. The only "consent" is communication
consent (CRM 05 D6 most-restrictive-wins on merge; Submissions `communications.record_consent` "designed in Platform
communications, not built", `12-published-operations.md:195`). "Parent" occurs only for organization hierarchy and as
the example relationship type "Parent of / Child of" (`crm/05-functional-architecture.md:307`). The contact record
carries `date_of_birth` (`contact-360.json` "Personal Details") with no age rule attached. Submissions has
staff-on-behalf starts (`on_behalf_of`, a `representative` participant, `05-processes.md:216`) and co-applicants
(`22-portals-as-sites.md` §4.1) — the nearest "someone acts for someone" mechanism, but adult-to-adult.

### 7. Authentication and account management

- **Identity provider**: Cognito; one portal pool per customer account, created when Websites is enabled
  (`06-portals.md:43-56`); the console has its own pool. Google sign-in off.
- **Account model**: "one user per membership in one wall, no shared person, no cross-wall relation"
  (`services-platform/docs/proposals/security-identity-and-access.md:135,484`). A person in two associations has two
  unrelated accounts.
- **Login ↔ contact**: one login links to at most one contact (`users.contact_id`); nothing prevents two logins linking
  to the same contact (no uniqueness, `portal-contact-claim.md:9-11`); a merge relinks (CRM D1..D10 "relink one login,
  block two", `crm/13-decisions.md:10`). There is **no notion of one login choosing among several contacts**
  (**Absent**).
- **Act on behalf**: "Acting for an organization" is a **separate selection the portal session records
  (`representedOrganization`)**, not derived from `cid` (`portal-contact-claim.md:27-29`) — **Planned/partly built**
  (the eligibility engine's slot). Staff impersonation (`act` claim, "impersonation sessions") is **deferred/Later**
  (`security-identity-and-access.md:374,553,717`); 1.0's token-in-URL design must not be repeated (CRM 05 §7).
- **Session**: one sealed `__Host-rw-member` cookie per site host (`06-portals.md:98-104`); SSO across a customer's
  sites designed, not built (G12).

### 8. Mobile

`docs/proposals/mobile-apps/` (worktree `2dab6a5b4`), all **Planned**:

- Identity reuses the portal pools, the 14-step authorizer, **`cid`** and the likely-match routes unchanged
  (`13-identity-security-and-permissions.md:3-19`); a second public app client `mobile-member` per pool (`:47`); device
  tracking table `platform.user_devices` and `/portal/me/devices` routes; re-auth route for sensitive operations
  (`:111-113`).
- **Account switching deferred**: "one signed-in account per app installation; changing accounts requires sign-out
  followed by sign-in" (`:84-90`). No family or household switching.
- **Membership card, member number, wallet pass: Missing** — "no card or member number concept" in Membership; needs a
  card concept, member number, pass generation (Documents for the template)
  (`11-backend-services-and-domain-ownership.md:51`; dependency B-3 `26-dependencies-and-missing-capabilities.md:39`).
  Home screen has a "membership card" section (`07-customer-experience-and-ux-design.md:208`).
- Profile view/edit on mobile = CRM's `/contacts/{t}/portal/me` family that `web-contacts` already calls (`11-…:53`).
  **No household/family mention in 28 chapters** (**Absent**).

---

## Finish

### (a) Console components a Household 360 would compose from

| Existing component                                                                                                                                                                                               | Use for Household                                                                                                                                                                             |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Record-view engine `components/record-views/record-view.tsx` + a `lib/record-views/views/household-360.json` + `resolve.ts` entry                                                                                | the 360 page itself (tabs, groups, sections, actions, visibility rules)                                                                                                                       |
| `defineCard` (`lib/record-views/registry.ts:40-55`) + directory `cards/index.ts`                                                                                                                                 | household cards; `contact.identity` as the model for a header card                                                                                                                            |
| Shared cards `record.field-group`, `record.kpi-strip`, `record.table`, `record.audit` (`cards/shared/`)                                                                                                          | details group, counts strip, related lists (memberships, invoices), created/modified                                                                                                          |
| `OrganizationAffiliationsSection` + `AffiliationRow` + `AffiliationEditor` (`components/affiliations/*`)                                                                                                         | the **Members** section: counts, Current/Upcoming/Past filter, primary flag ("head of household"), roles with dates, Add/Edit/Make Primary/Remove, the add/edit sheet with an injected picker |
| `AffiliationDialog` / `EndAffiliationDialog` (`components/console/organization/affiliation-dialogs.tsx`) and `OrganizationAffiliationsCard` + `useOrganizationAffiliations` (paged API, URL-held filters)        | the household-side "Add Person" / "End membership" dialogs and the paged members list                                                                                                         |
| `RecordPicker` + `createPickerSource` + `crmContactPickerSource` (`lib/record-pickers/*`, dataset-backed, inactive rule)                                                                                         | contact picker in Add Member; a household picker on Contact forms (`ContactPicker { excludeId }` pattern)                                                                                     |
| `RhythmRecordPickerField` in FormEngine (`build-contact-admin.ts:194`)                                                                                                                                           | a "Household" field on the contact form if the design wants one, like Spouse                                                                                                                  |
| Field catalog entry pattern `contact-fields.ts:51-54` (`spouse_name`)                                                                                                                                            | a `household_name` field on Contact 360 linking to the Household 360                                                                                                                          |
| `defineRecipeExplorer` (`contacts-definition.tsx:78-146`) + `lib/record-explorer/*`                                                                                                                              | Households list with views, filters, Ask, export, bulk bar                                                                                                                                    |
| `ListToolbar` (`components/configuration/list-toolbar.tsx`) + `ConfigListScreen`                                                                                                                                 | a configuration list (e.g. household member roles) in the one-row toolbar standard                                                                                                            |
| Record panels `record.archive/unarchive`, `record.lock*`, `record.change-type`, `record.version-history`, `crm.log-activity`, `crm.create-task`, `record.copy-link` (`components/record-views/panels.tsx:32-49`) | header actions, reused by id in the view JSON                                                                                                                                                 |
| `lib/record-history/adapters.ts` FACTORIES                                                                                                                                                                       | one adapter entry gives Version History                                                                                                                                                       |
| Merged-record redirect layout (`crm/contacts/[id]/layout.tsx`)                                                                                                                                                   | the same D-20 behaviour for merged households                                                                                                                                                 |
| Quick Search union `lib/quick-search/types.ts` + `destinations.ts`                                                                                                                                               | ⌘K results once Platform indexes the type                                                                                                                                                     |
| `lib/console/record-types.ts` `define()` + `domains.ts` item                                                                                                                                                     | URLs, labels, icon, permission, `recordHref` for links and pickers                                                                                                                            |

### (b) What does not exist

1. Any household, family, dependant, guardian or minor concept in console code, portal packages, Websites, Submissions,
   Membership, CRM 2.0 code or the mobile proposal (only CRM 05 §5's one-paragraph recommendation and CRM-M21's line).
2. Typed relationships (nav item "Relationships" is `planned`, `domains.ts:139`; Contact 360 "Relationships" tab is a
   placeholder table).
3. Platform-driven record layouts for the 360 (the designer saves to the browser; `resolve.ts` serves bundled JSON).
4. An Organization 360 on the record-view engine; an organization edit form; an organization history adapter.
5. A second-record picker in the merge workspace; field-level merge outcome; "Create anyway".
6. Any CRM page in the console's `(portal)` group; any CRM portal route in services-crm for profile, organization or
   roster (all PO rows Missing; `@rhythm/web-contacts` runs against its own mock).
7. Household/portal self-service of any kind; a "household administrator" permission; a portal route that writes another
   member's personal record (explicitly refused for organizations).
8. One login managing several contacts, or switching the acting contact; staff impersonation of a member; account
   switching on mobile.
9. A membership card / member number / wallet pass (Membership).
10. A consent ledger or parental-consent flow (Platform/CRM consent is "designed, not built"; CRM owns the consent
    ledger per memory 2026-10-20).
11. Quick Search for anything but contacts and organizations; a global `/search` page over real data.

### (c) Questions the code could not answer

1. **Shape A or B** — is a household an organization of system type "Household" (CRM 05 §5 Recommended, CRM-Q2 open) or
   a first-class entity? The console cost (table in §5) and the Contact 360 presentation differ sharply; only Andrew's
   CRM-Q2 ruling settles it.
2. If shape A, is the bespoke Organization 360 acceptable for households before CRM moves it onto the record-view engine
   and gives it an edit form (D-36)?
3. Who may see a household's other members in the portal — every member, the "head", or only a derived
   household-administrator grant — and may that administrator edit another member's _personal_ record (the organization
   design says no)? The code has no precedent for minors or guardians.
4. Should the spouse field survive as a field, become a relationship (CRM 05 §5), or become "household member with role
   Spouse"? D-19 only fixes reciprocity.
5. Whether a household needs its own Quick Search type and Explorer dataset (shape B) is a Platform index/dataset
   decision CRM must register; nothing in the console can decide it.
6. Whether Membership's family memberships (not designed anywhere) bill the household party (CRM 05 §5's stated reason
   for shape A) — the Membership architecture has no family program.
7. The portal is "always last" and Andrew builds portal components; the research cannot say which milestone a household
   portal feature would attach to (CRM-M22 is the only portal CRM milestone and lists PO-01..05 only).

## 26. R6 — Rhythm 1.0 precedent for households, families and person-to-person links

## R6 — Rhythm 1.0 precedent for households, families and person-to-person links

Research for the Rhythm 2.0 household architecture. Read-only pass over `~/rhythm` (Rhythm 1.0 production code), the 2.0
CRM architecture's 1.0 chapter and evidence, the 2.0 spec/migration trees, and live Zendesk/Jira searches run 2026-10-21
(ET). Nothing was modified, created or commented on anywhere.

Labels used per finding: **Implemented in 1.0** (code shipped and reachable from a screen), **Schema only** (a field or
table exists but no product flow creates or uses it), **Documented requirement** (a ticket or spec asks for it),
**Absent** (no trace).

Prior evidence this report cites rather than repeats:

- `/Users/andrew/rhythm2/worktrees/services-platform/crm-households/docs/proposals/crm/02-rhythm-1-and-support-history.md`
  (chapter 02; §2.3 relationships, §2.6 portal, §4 failure patterns)
- `/Users/andrew/rhythm2/worktrees/services-platform/crm-households/docs/proposals/crm/evidence/e01-rolodex-records-and-relationships.md`
  (e01; §5.3 generic relationships, §10.3 portal delegates, line 58 personal fields)
- `/Users/andrew/rhythm2/worktrees/services-platform/crm-households/docs/proposals/crm/evidence/e06-jira-zendesk-history.md`
  (e06; P1–P13). e06 contains **no** household/family pattern: its searches were merge, duplicates, affiliation,
  address, query, import and portal. The household-specific ticket evidence below is new.

### 0. The one-paragraph answer

Rhythm 1.0 never had a household, family or dependent concept. The only person-to-person structures were (a) a one-way
`spouse_id` pointer on the contact that no console form exposed, (b) a generic typed "relationships" link (2022) that
only the 360 page's Relationships tab read, that support told customers not to use, and that fundraising alone consumed
(automatic soft credits by relationship type), and (c) a portal account-delegation design (2023) whose backend was never
wired up. Everything that let one person's purchase cover other people was built on the **organization**: an
organizational membership "flows down" to the organization's contacts by contact role or is "assigned" to named contacts
up to a seat count, and event group registration is per organization. Guests at events were a per-registration
sub-record, with contact creation by e-mail match that produced duplicates for years. No age/minor/guardian handling
existed beyond an eligibility rule comparing `date_of_birth` to a minimum/maximum age. Customers asked for
multi-organization affiliations, queryable relationships, and a working "act on behalf of someone outside my
organization" flow; no ticket in Zendesk or Jira RB asks for family memberships or households by name.

### 1. Rolodex (CRM): what linked one person to another

#### 1.1 `spouse_id` on the contact — **Schema only**

- Shape: a single string pointer to another contact, validated only as "not self"
  (`/Users/andrew/rhythm/rolodex-contacts/src/schema/object.schema.js:51`; unit test
  `/Users/andrew/rhythm/rolodex-contacts/tests/unit/object/validation.spec.js:120-122`). Registered as a reference with
  `deleteAction: "none"` so a contact referenced as a spouse cannot be hard-deleted, in one direction only, and the
  reference is mislabelled "Role" (`/Users/andrew/rhythm/rolodex-contacts/config.js:123-128`). e01 line 58 and line 133
  say the same ("Spouse is a one-way pointer", SPEC FND-ROL-071).
- Exposure: no Angular console or portal form binds `spouse_id`. The only "spouse" strings in UI code are a
  customer-defined custom field `rolodex-contacts__spouse_name__c` in a captured sample form
  (`/Users/andrew/rhythm/platform-ngx-controls/src/app/sample-web-form/sample-web-form.component.ts:24654-24661`) —
  evidence a tenant worked around the gap with a free-text custom field — and a Cypress relationship test that uses a
  relationship type named SPOUSE (§1.2).
- Queries: the query field catalogue exposed `spouse_id` and a `spouse_name` join
  (`/Users/andrew/rhythm/system-queries/tests/json/all-fields.json:464-467,541-544`), which is why a customer saw
  "spouse fields in the CRM queries" with no data in them (ZD 10599, 2024-01-31, AIC, below).
- Non-reciprocal: nothing wrote the reverse pointer. Classified as non-sensitive personal data
  (`/Users/andrew/rhythm/libraries-utilities/tests/unit/personal-data.spec.js:32`).
- Lineage: the 2018 generated SDK model still carries `family_id` and `family_member_type: 'husband' | 'wife' | 'child'`
  (`/Users/andrew/rhythm/sdk-rolodex-angular/model/contact.ts:38-44,179-183`; repo last commit 2018-05-26). No service
  schema in `~/rhythm` has `family_id`; the MemberSuite-era family idea was dropped before 1.0 shipped. **Absent** in
  shipped code.
- 2.0 today: the migration carries `spouse_id` into `crm.contacts` in a second pass and refuses self-references
  (`/Users/andrew/rhythm2/rhythm2migration/transform/mappings/contacts.ts:255-290`;
  `/Users/andrew/rhythm2/rhythm2migration/docs/crm-mapping.md:256,885-886`). The 2.0 CRM therefore already has the same
  one-way pointer, which a household design must either absorb or retire.

#### 1.2 Generic relationships (`rolodex-relationships`, July 2022) — **Implemented in 1.0, barely used**

- Origin: RB-1019 (2022-05-02, "Support for Rolodex Relationships") says it was added because the Avectra import had "no
  way to represent these linkages, particularly when they are 1-many"; scope Org→Contact, Org→Org, Contact→Contact,
  Contact→Org. RB-1250 (2022-07-23) is the matching "relationships outside of the primary organization_id" ticket.
- Shape (e01 §5.3; `/Users/andrew/rhythm/rolodex-relationships/src/schema/object.schema.js:17-28`): `type_id`,
  `left_side_type/id/name`, `right_side_type/id/name`, `start_date`, `end_date`. Direction fixed by the type; no
  reciprocal label, no primary flag, duplicates allowed, names copied at save and never refreshed
  (`/Users/andrew/rhythm/rolodex-relationships/src/object/cleaning.js:50-53` builds
  `"(Spouse) Larry Stevens -> Jesse Stevens"`). Relationship types lived in `rolodex-relationship-types` (not cloned
  locally; e01 §3.4). Test fixtures seed exactly two types, OWNER (org→contact) and SPOUSE (contact→contact)
  (`/Users/andrew/rhythm/testing-well-known-objects/src/objects/rolodex/relationship-types.js:12-44`).
- Console: a Relationships tab on the contact/organization 360 with Current/Past/Future filters and a typeahead editor
  (`/Users/andrew/rhythm/rolodex-ngx-console-controls/cypress/e2e/relationships.spec.js:61,128-147`). Portal: nothing.
  Queries: nothing (RB-2317 removed even the custom relationship fields from CRM queries).
- Consumers: only Fundraising (§3). Membership, Events, directories and the portal never read it (e01 §5.3, AFF-INV §4).
- What customers found (new evidence, live Zendesk):
  - ZD 9942 (2023-12-12, Kellen): "Is this feature working? ... are relationship types just another means for tenants to
    subjectively relate records to each other; spouse, family, contractor, etc?" Support's answer (2023-12-14): "we
    don't typically recommend using these relationships as there is no way to set or view the relationships in the
    portal ... they cannot be pulled into a query, so all in all, it doesn't sound like it would be very beneficial."
  - ZD 10599 (2024-01-31, AIC): entered many relationships, "can't find how to pull this information"; support: "You
    cannot query relationships at this time." Customer wanted "a list of people who are related to each other with name,
    email, relationship type, who they were related to".
  - ZD 11124 (2024-03-12, Smithbucklin, feature request): "set multiple organization relationships and set one as the
    primary relationship ... an active contact on more than one organization." (The 2.0 CRM's affiliations answer this;
    chapter 02 P5.)
- Migration to 2.0: contact→organization relationships become typed affiliations; **contact→contact and org→org
  relationships "have no 2.0 home and are excluded"**
  (`/Users/andrew/rhythm2/rhythm2migration/docs/crm-mapping.md:176-183`). The pilot tenant (rubber.org) has 0
  relationships and 2 relationship types, consistent with the feature being unused.

#### 1.3 Portal account delegation (acting for another person) — **Schema only / never released**

- Design: RB-2274 (2023-04-02, status "Ready for Release", never shipped): "if a person belongs to the same organization
  as another person, they have the ability to act on their behalf. But sometimes, we want one person to act on behalf of
  another, even if they are in separate organizations ... register someone else for an event who isn't in your
  organization ... send a request for delegation; an email goes out ... If they approve it, the person has permission.
  They can even put a time limit on it." Three deliverables: delegation processes with e-mails, portal controls to
  approve/review/revoke, delegates on the 360 security tab.
- What exists: `portal_delegates[{contact_id, expiration_date, status}]` on the delegating contact
  (`/Users/andrew/rhythm/rolodex-contacts/src/schema/object.schema.js:160-166`); a manageability rule — same
  organization, or the contact does not exist yet, or the manager is an unexpired delegate
  (`/Users/andrew/rhythm/rolodex-contacts/src/logic/check-manageability.js:32-44,70-89`); a delegate lookup index
  (`/Users/andrew/rhythm/rolodex-contacts/src/lambda/list-by-portal-delegate-id.js:38-53`, with the comment that it
  assumes "a relatively small # of contacts using account delegation per tenant"); request/grant modules that cannot run
  (`request-delegate-access.js:5` requires a misspelled package; `grant-delegate-access.js` has no caller) — e01 §10.3
  and FND-ROL-118. The console's membership "group process" screen offered "allow group administrators to submit
  applications for people not linked to their organizations" with an empty delegation-process dropdown (RB-2623,
  2023-06-02: "there isn't anywhere within membership to configure this ... only available in the FAA tenant").
- Implication: the only 1.0 precedent for "a person manages another person's record" is organization membership; the
  cross-organization, consent-based variant was designed but never delivered. Note the 2.0 authorizer already names the
  idea: "`actingForContact`: the caller acts for the record's contact (a delegate or guardian)"
  (`/Users/andrew/rhythm/knox-authorizer/src/authorization/vendor/service-platform/runtime/record-rules.ts:8`, a 2.0
  TypeScript artifact vendored into the 1.0 tree, not 1.0 behaviour).

#### 1.4 Household, family, dependent, guardian, minor — **Absent**

- `household`: zero product hits. Only a Font Awesome icon category, a scholarship demo form's "Family Size" help text
  ("people living in your household"), and surnames in payment-migration CSVs.
- `guardian`: only Auth0 Guardian MFA scopes
  (`/Users/andrew/rhythm/libraries-auth0/src/applications-service.js:256-260`) and the 2.0 comment above.
- `dependent`/`minor`/`joint`/`beneficiary`/`covered member`/`secondary member`: no product hits beyond English prose.
- `family`: after excluding `font-family`, the only product hits are "Family Office" picklist values in a captured
  customer web form and scholarship applicant fields (`family_size__c`, `family_annual_income__c`) — custom fields on an
  application, not a CRM structure. A well-known-object fixture with `owner_type: 'family'` is deliberately invalid test
  data (`status: 'activex'`, `origin: 'switft'`;
  `/Users/andrew/rhythm/testing-well-known-objects/src/objects/membership/memberships.js:85`).
- Live Zendesk (2026-10-21): `subject:household` 0 tickets; `subject:family` 2 (both `family_name` SSO fields);
  `"joint membership" OR "household" OR "family member" OR "under 18" OR "guardian" OR "dependents"` 0 tickets;
  `subject:spouse` 2 (both event guest/spouse registration, §4). Jira RB text search for spouse/household/"family
  membership"/"family member"/"joint membership"/guardian/"under 18": 5 issues, none about families (RB-57 pricing
  expression, RB-1024, RB-1195, RB-3646 guest amounts, RB-7201 login). **No customer asked 1.0 for a household or family
  membership by name in six years of tickets.**

#### 1.5 Age — **Implemented in 1.0** (eligibility only)

- `date_of_birth` is an ISO date on the contact (`.../rolodex-contacts/src/schema/object.schema.js:110`), on the console
  form, imports and queries, classified as sensitive personal data
  (`/Users/andrew/rhythm/libraries-utilities/src/personal-data.js:206`). The 2024 directory leak (ZD 20897, RB-7556;
  chapter 02 §2.6) exposed it.
- The only rule that reads it: the shared eligibility library's age restriction, `minimum_age`/`maximum_age` compared to
  fractional years from `date_of_birth`, refusing with "no date of birth is on file" when missing
  (`/Users/andrew/rhythm/libraries-eligibility/src/eligibility.schema.js:100-103`;
  `/Users/andrew/rhythm/libraries-eligibility/src/age-logic.js:20-27`). Eligibility criteria are attachable to
  membership application processes, add-ons, event functions, exhibitor processes, certifications, submissions, portal
  security policies and products (well-known fixtures under
  `/Users/andrew/rhythm/testing-well-known-objects/src/objects/*/`). No minor/guardian consent, no "a parent registers a
  child", no age-derived membership category.

### 2. Membership: how one purchase covered several people — **Implemented in 1.0**, organization-only

- Owner types are exactly `contact` and `organization`
  (`/Users/andrew/rhythm/membership-memberships/src/schema/object.schema.js:78-86`;
  `/Users/andrew/rhythm/membership-types/src/schema/object.schema.js:16`;
  `/Users/andrew/rhythm/membership-applications/src/schema/object.schema.js:20`;
  `/Users/andrew/rhythm/certifications-certifications/src/schema/object.schema.js:18`). No family or household owner.
- Three origins of a membership record: `principal`, `flowdown`, `assigned`
  (`.../membership-memberships/src/schema/object.schema.js:121`).
  - **Flowdown**: a membership type can `enable_flowdown` with a `flowdown_membership_type_id` for the contacts,
    `organization_type_ids` (child organizations that inherit, cascading, `flowdown_depth` ≤ MAX) and `contact_role_ids`
    (which contacts of the organization inherit; empty = every contact)
    (`.../membership-types/src/schema/object.schema.js:26-38`;
    `/Users/andrew/rhythm/membership-memberships/src/logic/flow-down/contact-flowdown.js:27-37`). The child record
    copies a fixed field list from the parent (status, type, package, dates, add-ons, order, chapters, sections) and
    tracks `parent_membership_id`, `master_membership_id`, `overridden_fields`
    (`/Users/andrew/rhythm/membership-memberships/src/logic/flow-down/write-inherited-membership.js:15-33`; schema
    `:94-128`).
  - **Assigned**: the organizational package sets `maximum_number_of_contacts` and optionally
    `allow_for_purchase_of_additional_contacts` with an add-on product and a cap
    (`/Users/andrew/rhythm/membership-packages/src/schema/object.schema.js:81-89`); the membership stores
    `assigned_memberships[{contact_id, chapter_ids, section_ids, add_on_ids}]` and, since RB-5875, a
    `total_seats_purchased` seat model (`.../membership-memberships/src/schema/object.schema.js:130-139`). The portal
    "Manage member contacts" screen lets an organization administrator pick existing contacts or type new ones (which it
    creates as contacts first) and saves the array
    (`/Users/andrew/rhythm/membership-ngx-portal-controls/projects/membership-ngx-portal-controls/src/lib/components/manage-member-contacts/manage-member-contacts.component.ts:90-91,135-160`);
    the application form's "choose corporate member contacts" step does the same at join time
    (`/Users/andrew/rhythm/membership-ngx-controls/projects/membership-ngx-controls/src/lib/webForms/applications/choose-corporate-member-contacts/`).
- Who may manage whom: the same-organization rule of `check-manageability.js` (§1.3). A family cannot be modelled unless
  it is an organization.
- Known problems from e06 that apply directly: P1 (flowdown memberships left pointing at merged/deleted organizations,
  RB-22; membership not moved on merge, RB-916/1879/ZD 8209), P5 (roles drive flowdown but role cardinality is not
  enforced; 828 organizations with several Owners, RB-7662), P10 (roster screens unpaged, child organizations not
  editable in the portal, RB-5646/6617/6986). RB-5622 added "assigned members are required to join a chapter".
- Implication for 2.0: 1.0's only "membership covering several people" machinery is organization-shaped (seats, flowdown
  by role, administrator roster). A household design can reuse the seat/assignment vocabulary but must not require the
  household to be an organization; and whatever holds the "assigned" contact id must participate in merge (P1).

### 3. Fundraising: soft credits through relationships — **Implemented in 1.0**

- Settings: `apply_soft_credits_to_relationships` plus
  `soft_credit_relationships[{relationship_type_id, direction: 'left to right' | 'right to left' | 'both', percentage}]`
  (`/Users/andrew/rhythm/fundraising-settings/src/schema/object.schema.js:16-27`). Console copy: "a relationship of type
  Spouse might flow both ways; if either spouse makes a gift, the other spouse gets credit"
  (`/Users/andrew/rhythm/fundraising-ngx-console/src/app/setup/pages/modify-settings/modify-settings.component.html:165-172`).
- Logic: on gift/pledge fulfilment, list the donor's relationships on both sides, keep CRM sides only, verify the
  related record still exists ("we don't want to soft credit ghosts"), and write
  `soft_credits[{donor_type, contact_id|organization_id, fund_id, percentage, amount, reason: 'relationship'}]`, one per
  fund split (`/Users/andrew/rhythm/libraries-fundraising/src/soft-credits.js:36-128`;
  `/Users/andrew/rhythm/libraries-fundraising/src/gift.schema.js:19-30`). Reasons also allow `matching`, `tribute`,
  `other` for manual credits. Credits are shredded to a `fundraising_soft_credits` table for reporting
  (`/Users/andrew/rhythm/fundraising-gifts/src/logic/soft-credit-shredder.js:15-28`;
  `/Users/andrew/rhythm/fundraising-gifts/config.js:9`), and a query definition explains the spouse case
  (`/Users/andrew/rhythm/fundraising-query-definitions/src/definitions/giftsWithSplitsSoftCredits.js:9-10`).
- Tribute gifts: `in memory of` / `in honor of` with an optional linked `contact_id` and honoree notification
  (`/Users/andrew/rhythm/libraries-fundraising/src/payload.schema.js:25-39`).
- Absent: no household giving total, no "primary donor of a household", no household receipting. Soft credit is the
  nearest thing to a household rollup and it is per-gift and forward-only ("existing gifts are not changed").
- Implication: this is the one 1.0 consumer that proves demand for a reciprocal, typed person-to-person link with a
  direction. A 2.0 household must give Fundraising an equivalent (household members as soft-credit recipients) or the
  1.0 capability regresses.

### 4. Events: guests and registering other people — **Implemented in 1.0**

- Guests: a registration carries `guests[{id, guest_registration_package_id, guest_contact_info}]`, and each guest
  becomes its own registration with `is_guest_registration`, `parent_registration_id`, `parent_registration_guest_id`,
  and `is_transient_guest` when no contact exists
  (`/Users/andrew/rhythm/events-registrations/src/schema/object.schema.js:44-45,118-124`). Registration types have a
  `guest` kind and `restrict_to_guests`
  (`/Users/andrew/rhythm/events-registration-types/src/schema/object.schema.js:17-20`); the process sets
  `maximum_number_of_guests` and `do_not_save_guests_as_contacts`
  (`/Users/andrew/rhythm/events-registration-processes/src/schema/object.schema.js:39-41`). Demo portal data names the
  package "Guest/Spouse"
  (`/Users/andrew/rhythm/ngx-portal/src/app/events/services/events-configuration.service.ts:711`).
- Guest-to-contact matching: by e-mail; if none matches, a contact is created with
  `origin_service = 'events-registrations'` unless the process forbids it; a guest with no contact is linked to the
  **registrant's own contact id**
  (`/Users/andrew/rhythm/events-registrations/src/logic/create-guest-registrations.js:41-66,98-100`).
- Known problems: RB-2568 (2023-05, an existing e-mail silently renames the guest to the matched contact), RB-2697
  (2023-06, the registrant's own e-mail for a guest creates one linked and one unlinked guest registration; workaround
  "disabling email as a guest registration option"), RB-1668/2838/3480/4251 (duplicate guest registrations, 2022–2024),
  RB-4256 (guests not counted in capacity), RB-5215 (cannot remove a guest), RB-214 (2021, guest-created contacts should
  be visible as such on the 360). Zendesk `subject:guest` returns 86 tickets 2023–2026 (e.g. ZD 13811 "One Free Guest
  Registration, Unlimited Paid Guest Registrations", ACG 2024-11; ZD 18889 "block a function registration fee ... unless
  a Spouse/Guest is selected", 2026-02; ZD 367 "adding spouse registration", CLAS 2020-11, urgent). Guests are where 1.0
  customers actually met spouses and family members.
- Group registration: a registration group belongs to an **organization** with an administrator contact
  (`/Users/andrew/rhythm/events-registration-groups/src/schema/object.schema.js:16-19`); group registrations accept
  contact ids, contact info or existing registrations
  (`/Users/andrew/rhythm/events-group-registrations/src/schema/object.schema.js:17-32`). Registering someone outside
  your organization was the stated motivation for the undelivered delegation feature (RB-2274). RB-794 (2022-03):
  linking during group registration created duplicates (e06 P4).
- Implication: 1.0 proves two things a household design must answer: (1) a "guest" is often a spouse or child who may or
  may not deserve a contact record, and the e-mail-only match-or-create was the main duplicate factory; (2) "register
  people I am responsible for" was wanted beyond the employer and never delivered.

### 5. Collect/Commerce and portal identity

- Invoices and orders are owned by a contact or an organization; `bill_organization` on memberships and packages
  (`.../membership-memberships/src/schema/object.schema.js:163`;
  `.../membership-packages/src/schema/object.schema.js:91`) is the only "someone else pays" switch. RB-3523/3524
  (2024-01) let parent-organization contacts invoice child-organization contacts. No "bill the spouse" or household
  billing — **Absent**.
- Portal users link to exactly one contact (`/Users/andrew/rhythm/knox-users/src/schema/contact-user.schema.js:8-11`);
  reach = own contact, same organization, ancestors/descendants, unexpired delegates (e01 line 541). No way for one
  login to switch between related people — **Absent**.

### 6. Rhythm 2.0 specs and migration — what already mentions these ideas

- `/Users/andrew/rhythm2/rhythm2migration/docs/crm-mapping.md:176-183` (relationships rule) and `:256,885-886` (spouse
  second pass); `/Users/andrew/rhythm2/rhythm2migration/docs/reconciliation.md:144` checks `spouse_id`.
- `/Users/andrew/rhythm2/docs-2/specs/apps/rolodex/planning/tenant-lifecycle/journeys/JNY-RLX-007-tenant-provisioned-and-erased.md:184`
  seeds "a contact with a spouse" for the erase test. No spec under `docs-2/specs` mentions household, family
  membership, dependent, guardian or minor (grep 2026-10-21).
- The 2.0 CRM architecture (chapter 02 §2.3, chapter 13 decisions) treats spouse as a kept one-way field and
  relationships as a gap; neither proposes households.

### 7. What 1.0 never had that customers asked for

Evidence-backed asks, with the ticket that carries them:

1. **Queryable, portal-visible relationships** — ZD 9942 (Kellen, 2023-12), ZD 10599 (AIC, 2024-01): staff entered
   relationships and could not report on them; support advised against the feature.
2. **A person in more than one organization with one primary** — ZD 11124 (Smithbucklin, 2024-03); answered by 2.0
   affiliations, relevant because a household is a second "membership of a group" for the same person.
3. **Acting on behalf of someone outside my organization, with their consent** — RB-2274 (2023-04, never released),
   RB-2623 (2023-06): the group-registration and group-application flows stopped at the employer boundary.
4. **Guests who are spouses or family, priced and limited as such** — ZD 367 (2020), ZD 13811 (2024), ZD 18889 (2026):
   "one free guest then paid", "spouse/guest ticket gating"; and the long duplicate-guest trail
   (RB-2568/2697/2838/3480/4251).
5. **Removing the "spouse" field customers did not want on forms** — ZD 10243 (SHOP, 2024-01: "I can't find anything
   that is showing 'gender' and 'spouse'"), ZD 11199 (CVMA, 2024-03: directory opt-out shows "Spouse ID"). Both show the
   dead field leaking into member-facing screens.

Not found: any ticket asking for a family/household membership type, dependents on a membership, parent–child member
records, or minors. If Rhythm 2.0 is building households, the demand is from product strategy or prospects, not from the
1.0 support record.

### 8. What to avoid, from the 1.0 record

- A one-way pointer with no reverse write (spouse_id) and a link table nothing reads: both rotted and surfaced as
  confusing fields (ZD 10243, 11199, 10599).
- Links keyed by copied ids and names with no registry of holders (relationships copied `left_side_name`; flowdown and
  assigned memberships copied contact ids) — the P1 merge losses.
- Match-or-create by e-mail inside a sub-flow (guests) — the P4 duplicate factory; a household member without an e-mail
  must still be a first-class, de-duplicable record or an explicit non-contact.
- Per-channel authorization (console vs portal) for "who may manage whom" — P10; the manageability rule must be one
  server-side rule that a household membership extends.
- Shipping the data shape before the flow (portal_delegates had a schema and an index for two years with no way to
  create one).

### 9. Questions the code could not answer

1. How many 1.0 tenants populated `spouse_id` or created contact→contact relationships? The pilot tenant has 0
   relationships; production counts need a DynamoDB scan (not run; read-only session without a tenant mandate).
2. Whether any 1.0 tenant ran a "family" membership by modelling the family as an organization (a type named Family with
   flowdown). No organization type named Family appears in fixtures; a production query of `rolodex_organization_types`
   would settle it.
3. Which relationship type names tenants actually created (only OWNER and SPOUSE are in fixtures; the
   `rolodex-relationship-types` repository is not cloned locally).
4. Why RB-2274 (delegation) stopped at "Ready for Release" — the ticket has no closing comment; the misspelled import
   suggests it was never exercised end to end.
5. Whether MemberSuite/Avectra conversions carried family data into custom fields (chapter 02 notes employment history
   custom objects for CLARB; nothing seen for families).
6. Whether Fundraising's soft-credit-by-relationship was switched on by any tenant (settings are per tenant; not
   readable here).

## 27. R7 — Rhythm 1.0 relationships, membership flowdown and eligibility, precisely

## R7 — Rhythm 1.0 relationships, membership flowdown and eligibility, precisely

Extends `/Users/andrew/rhythm2/briefs/households/research/r6-rhythm1.md` (R6). R6 established the inventory (what
existed, what customers asked for); this report documents the exact data shapes, algorithms, triggers and defect trail
for (a) contact relationships and relationship types, (b) membership flowdown and assigned/seat memberships, and (c)
eligibility and benefits. Read-only pass over `~/rhythm` plus the `rolodex-relationship-types` repository fetched from
GitHub on 2026-10-21 (ET); nothing modified, no commits, no ticket writes.

Citation forms: local files `path:line`; GitHub-fetched files
`rhythmsoftware/rolodex-relationship-types@master:path:line`. Labels: **Implemented in 1.0**, **Documented
requirement**, **Absent**.

### 0. Ten findings R6 did not have

1. A relationship **type** has exactly five fields: `id`, `name`, `left_side_type`, `right_side_type`, `is_active`. No
   direction flag, no reciprocal label or reciprocal type, no "show in portal", "flow down benefits" or "soft credit"
   flags (`rhythmsoftware/rolodex-relationship-types@master:src/schema/object.schema.js:15-21`). Its validation,
   cleaning, change- and deletion-validation modules are the untouched template stubs
   (`…:src/object/validation.js:15-23`, `…:src/object/cleaning.js:9-22`); the readme is boilerplate (`…:readme.md:6-8`).
2. A relationship could be stored in **either direction only by accident of where you started**: the console editor
   always puts the record whose page you are on onto the left side when its type matches the type's left side, and onto
   the right side otherwise
   (`/Users/andrew/rhythm/rolodex-ngx-console-controls/projects/rolodex-ngx-console-controls/src/lib/relationships/create-relationship/create-relationship.component.ts:87-106`).
   For a contact→contact type such as Spouse, Larry's page writes Larry→Jesse and Jesse's page writes Jesse→Larry;
   nothing stops both existing, and nothing marks one as the reciprocal of the other.
3. The relationships tab shows the type name and the _other_ party only, hiding which side the current record is on
   (`…/relationships-one-click/relationships-one-click.component.html:82-109`). An asymmetric type (e.g. "Parent of")
   therefore reads identically on both records.
4. A membership record's id is `contact:<contact_id>` or `organization:<organization_id>`
   (`/Users/andrew/rhythm/membership-memberships/src/object/generate-identifier.js:38-47`). **Each CRM record has at
   most one membership record.** Flowdown and assignment do not add a second membership; they _overwrite the person's
   only membership record_ (subject to the conflict rules in §2.4).
5. Flowdown's "who receives" test is organization membership plus contact role, read from `rolodex-contacts`
   `listContactsByOrganizationId` and `rolodex-organizations` `listOrganizationsByParentOrganizationId`
   (`…/membership-memberships/src/logic/flow-down/flowdown-membership.js:13-26,78-81`). No relationship SDK is imported
   anywhere in `membership-*`; the only consumer of `rolodex-relationships` outside Rolodex is
   `libraries-fundraising/src/soft-credits.js` (grep of every `package.json` under `~/rhythm`). **Flowdown never used
   contact-to-contact relationships — confirmed.**
6. The console never exposed `flowdown_membership_type_id`; the control is commented out
   (`/Users/andrew/rhythm/membership-ngx-console/src/app/setup/pages/membership/membershipTypes/create-membership-type/create-membership-type.component.ts:98`),
   so in practice a flowed-down contact carried the organization's own membership type
   (`…/write-inherited-membership.js:139-145`).
7. Switching flowdown **off** on a type does not stop processing: the processor forces `contact_role_ids = []`, and an
   empty array fails the "no roles configured" test, so every contact of every organization of that type goes through
   the revoke path on the next trigger (`…/flowdown-membership.js:96-104`; `…/contact-flowdown.js:27-35`).
8. Revocation had three different outcomes depending on history, and two of its three branches were wrong until 2026: a
   flowdown-only member who left for another organization was inactivated; one who simply had the organization removed
   lingered as a lapsed flowdown member until RB-5496 (2026-07-21); one whose own earlier membership had been
   overwritten by flowdown had the _oldest_ snapshot restored, sometimes as "active" with no expiration, until RB-7487
   (2026-09-15); and a principal member kept pointing at the old organization, appearing on its Member Contacts tab and
   in its billing notices, until RB-6105 (same PR) (`…/delete-inherited-memberships.js:71-87,97-125,144-164`;
   `git -C ~/rhythm/membership-memberships log` `cdbd822`, `70d7fff`).
9. The eligibility criterion `members` is satisfied by the **existence** of a membership record; status is only checked
   when `member_statuses` is configured (`/Users/andrew/rhythm/libraries-eligibility/src/membership-logic.js:32-53`).
   Member _pricing_, by contrast, requires `status === 'active'` or a membership package in the same order
   (`/Users/andrew/rhythm/commerce-order-logic/src/pricing/calculate-order-price.js:334-347`). The two notions of
   "member" differ.
10. No eligibility criterion references relationships or any household-like structure. The only "through another record"
    evaluations are through the **employer**: `use_organization_for_eligibility` swaps the contact for its organization
    (`…/libraries-eligibility/src/determine-eligibility.js:72-79`), organization restrictions read
    `contact.organization_id` (`…/organization-logic.js:42-56`), and the advanced-rule/expression facts include
    `organizationMembership` and `parentOrganization` (`…/advanced-logic.js:69-78,88-111`).

### 1. Relationships and relationship types

#### 1.1 The relationship type — **Implemented in 1.0** (2022-07-23 → last touched 2026-02-19, dependency bumps only)

Repository `rhythmsoftware/rolodex-relationship-types` (private; default branch `master`; first commit
`1388376 2022-07-23 First commit`; last `b04e737 2026-02-19 Updated NPM Dependencies`). Not cloned under `~/rhythm`.

| Field             | Shape                                                                  | Cite                                                   |
| ----------------- | ---------------------------------------------------------------------- | ------------------------------------------------------ |
| `id`              | string `^[\w:                                                          | -]+$`, server-generated (`client_provides_key: false`) | `…@master:src/schema/object.schema.js:15`; `…:config.js:24` |
| `name`            | string, required                                                       | `…:object.schema.js:16`                                |
| `left_side_type`  | required, one of `rolodex-contacts`, `rolodex-organizations`           | `…:object.schema.js:7,18`                              |
| `right_side_type` | same two values (description copy-pasted: "The type of the left side") | `…:object.schema.js:19`                                |
| `is_active`       | boolean, default true                                                  | `…:object.schema.js:21`                                |
| system properties | standard `sys_*`                                                       | `…:object.schema.js:28`                                |

Absent from the type: direction/reciprocity (no `is_directional`, no `reciprocal_name`, no `reciprocal_type_id`), portal
visibility, benefit flowdown, soft-credit, uniqueness, cardinality, "primary". Storage is a small per-tenant table
`rolodex_relationship_types` with no dependencies on other services (`…:config.js:5,19,61`). Portal routes are
unconfigured (`…:config.js:95-128`). The test payload is the template's "Andrew Ryan" contact→organization type
(`…:tests/json/test-payload.js:8-12`). Fixtures seed only OWNER (org→contact) and SPOUSE (contact→contact) (R6 §1.2).

Console setup form
(`/Users/andrew/rhythm/rolodex-ngx-console/src/app/setup/pages/relationships/create-relationship-type/create-relationship-type.component.html:8-31`):
the standard name/id/active panel plus two `ng-select`s, "Left Side Type" and "Right Side Type", each offering Contact
or Organization (`…/rolodex-ngx-console-controls/src/lib/relationships/relationship-constants.ts:3-14`). That is
everything an administrator could set.

#### 1.2 The relationship record — **Implemented in 1.0** (2022-07-23; remote head `0166663 2026-02-18`, schema identical to the local clone except the Joi upgrade)

Schema `/Users/andrew/rhythm/rolodex-relationships/src/schema/object.schema.js:17-30`: `type_id` (required), `name`
(calculated), `left_side_type/id/name`, `right_side_type/id/name`, `start_date`, `end_date` (ISO date-only), `notes`.

Save-time logic:

- `cleanBeforeSave` loads the type and both endpoints; refuses if the record's side types differ from the type's
  (`…/src/object/cleaning.js:37-41`), refuses missing endpoints (`:43-47`), and copies names into
  `name = "(Spouse) Larry Stevens -> Jesse Stevens"`, `left_side_name`, `right_side_name` (`:49-53`). Names are never
  refreshed afterwards.
- `validateRelationship`: start ≤ end (defaults 1900/2900) and not self-related (`…/src/object/validation.js:23-27`).
- Dependencies: `type_id` → `rolodex-relationship-types`, `deleteAction: none` (a type in use cannot be deleted);
  `left_side_id` and `right_side_id` → the service named by the side type, `deleteAction: cascade` (deleting either
  endpoint deletes the relationship) (`…/rolodex-relationships/config.js:70-85`). Because 1.0 merge hard-deletes the
  losing record (e06 P1), a merge **cascade-deleted** that record's relationships instead of re-pointing them —
  inference from the two facts; no ticket names it, consistent with the feature being unused.
- Reads: two GSIs, `ByLeftSideId` and `ByRightSideId`, keyed `tenant#type#id`
  (`…/src/lambda/list-by-left-side-id.js:39-43`; swagger `listRelationshipsByLeftSideId` / `…RightSideId`
  `…/build/swagger.pre.yaml:26,77`). There is no "list by type" and no cross-tenant/report index.
- Extensibility note in code: "relationships are only allowed between contacts and organizations, the idea is that this
  is extensible" (`…/src/logic/retrieve-item.js:1-3`).

Could a relationship be created in either direction? **Yes, for a type whose two sides are the same record type**, and
the direction stored depends only on which record's page the user started from (finding 2). For a mixed type
(org→contact) the side is fixed by the type; the Cypress test confirms that starting from a contact with the Owner type
puts the organization on the left and the contact on the right
(`/Users/andrew/rhythm/rolodex-ngx-console-controls/cypress/e2e/relationships.spec.js:87-93`), and that a peer (SPOUSE)
record can be edited from the record on its right side (`:125-144`). Nothing prevents A→B and B→A both existing, nor two
identical A→B rows (no uniqueness anywhere).

#### 1.3 The relationships tab — **Implemented in 1.0**

- Shown on the contact and organization 360 only when at least one active type has that record type on either side
  (`/Users/andrew/rhythm/rolodex-ngx-console/src/app/contacts/pages/view-contact/view-contact.component.html:31`;
  `…/rolodex-ngx-console-controls/src/lib/relationships/relationships-helper.service.ts:22-27`).
- Lists both GSIs merged, sorted by start, end, type (`…/relationships-helper.service.ts:29-58`). Filters Current / Past
  / Future by today against `start_date`/`end_date` (defaults 1900-01-01 / 2200-01-01)
  (`…/relationships-one-click/relationships-one-click.component.ts:131-139`); a free-text filter matches the other
  party's copied name (`:95-98`).
- Columns: Type (name resolved from Algolia), Relationship (the other party as a link; the current record's own side is
  suppressed), Start, End, edit, delete (`…/relationships-one-click.component.html:82-160`). Permissions
  `rolodex:user:relationships:create|edit|delete` (`:36,146,153`).
- Editor fields: Relationship Type (active types applicable to this record type,
  `…/create-relationship.component.ts:73-75`), the other party (organization or contact selector depending on the type's
  other side), Start Date, End Date, Notes (`…/create-relationship.component.html:13-52`). On save the toast shows the
  computed name.

#### 1.4 Queries, portal, other consumers

- `system-queries` and `rolodex-query-definitions`: zero occurrences of "relationship" (grep over `src` and
  `tests/json`). **Absent.** This is the code behind support's answer "You cannot query relationships at this time" (R6
  §1.2, ZD 10599).
- Portal: no `rolodex-relationships` client in any `*-ngx-portal-controls` or `ngx-portal` package. **Absent.**
- Consumers: `libraries-fundraising/src/soft-credits.js` (R6 §3) and its gift/pledge integration tests; a minified
  bundle under `customers-clarb-ngx-controls/test/` contains the SDK string but no readable logic. Membership, Events,
  Committees, Directories: none.

### 2. Membership flowdown and assigned memberships — **Implemented in 1.0**

#### 2.1 Configuration surface

Membership type (`/Users/andrew/rhythm/membership-types/src/schema/object.schema.js:26-38`):

- `enable_flowdown` (default false); `flowdown_membership_type_id` (type given to the inherited records; console control
  commented out, finding 6);
- `flowdown_options.contact_role_ids` — contacts of the organization holding one of these roles inherit; **empty or
  missing = every contact** of the organization;
- `flowdown_options.enable_cascading_flowdown` (default **true** in the schema; the console checkbox is behind feature
  flag `membership_cascading_flowdown`, `…/create-membership-type.component.ts:52`);
- `flowdown_options.organization_type_ids` — child organizations of these types inherit; empty = all children.

Console form text: "Which contact roles should receive organizational member benefits? … leave blank to flowdown to all
contacts"; "Note that changes made here will not take effect until you reprocess flowdown … Setup → Maintenance Tasks"
(`…/create-membership-type.component.html:22-60`). The section only appears for `customer_type === 'organization'`
(`:22`).

Membership settings (`/Users/andrew/rhythm/membership-settings/src/schema/object.schema.js:189,212`):
`conflicting_membership_preference: 'principal' | 'organization'` (default principal) — which wins when a person has
both their own and an inherited membership; `include_flowdown_members_in_kpis`.

Package (`/Users/andrew/rhythm/membership-packages/src/schema/object.schema.js`):
`organizational_membership_options. maximum_number_of_contacts` (:82), `allow_for_purchase_of_additional_contacts`
(:83), `additional_contact_purchasing_info.add_on_product_id` and `maximum_number_of_additional_contacts` (:85-86),
`assigned_members_are_required_to_join_a_chapter` and chapter `auto_assignment: off|suggest|assign` (:48-50),
`bill_organization` (:91), `does_not_receive_member_benefits` (:22, produces status `active without benefits`,
`…/membership-memberships/src/logic/fulfillment/update-membership-dates.js:76`).

#### 2.2 Record shape (`/Users/andrew/rhythm/membership-memberships/src/schema/object.schema.js`)

- `owner_type: contact | organization` with `contact_id` / `organization_id` (:78-86); id derived from them (finding 4).
- `origin: principal | flowdown | assigned` (:121); `parent_membership_id` required for the last two and must match
  `organization:*` (:94-97); `master_membership_id`, `master_membership_organization_id` (top of a cascade) (:99-100);
  `flowdown_depth` 1..10 required for flowdown (:124-127; `MAX_FLOWDOWN_DEPTH = 10`, `…/flowdown-membership.js:2`).
- `overridden_fields` — fields edited on the child that flowdown must not overwrite (:128). Populated automatically: any
  edit to a flowdownable non-date field or to a custom field on a membership is recorded as overridden; dates are
  deliberately excluded ("should NEVER EVER be out of sync with the parent", judgement call 2022-08-26)
  (`…/src/object/change-validation.js:82-116`). Also editable in the console membership form
  (`…/membership-ngx-console/src/app/members/pages/membership/create-membership/create-membership.component.html:204`).
- `assigned_memberships[{contact_id, chapter_ids, section_ids, add_on_ids}]` on the **organization's** record
  (:130-137); `total_seats_purchased` (RB-5875 seat model) (:139).
- `status: active | inactive | suspended | active without benefits` (:15); `bill_organization` (:162); `history[]` with
  events including `assignment`, `flowdown`, `merge` and snapshots carrying `origin` and `parent_membership_id`
  (:206-220).
- Fields copied parent→child (`FLOWDOWNABLE_FIELDS`, `…/write-inherited-membership.js:15-33`): status, status_reason_id,
  date_status, is_new, type_id, package_id, join_date, expiration_date, add_ons, renewal_date, reinstatement_date,
  termination_date, order_id, order_line_item_id, chapters, sections; plus custom field values merged by id (:149-168).

#### 2.3 Triggers (which event, which handler)

All asynchronous, through SQS queues fed by the system-wide "domain object changed" SNS topic
(`/Users/andrew/rhythm/membership-memberships/build/template-resources.yaml`):

| Queue                                             | Subscription filter                                                                                    | Handler                                                                                   | What it does                                                                                                                                                                                                                                                                                                                                                   |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `membership-flowdown-queue`                       | `Microservice: membership-memberships` (`:683-685`)                                                    | `events.handleFlowdownMessages` (`template-logic.yaml:180`; `src/lambda/events.js:25-36`) | Any saved membership → `flowdownMembership(newObject)`; returns immediately unless `owner_type === 'organization'` (`flowdown-membership.js:47-48`). This is how a renewal, drop, status or type change on the organization reaches the children.                                                                                                              |
| `membership-contact-change-queue`                 | `Microservice: rolodex-contacts, rolodex-organizations`; `EventName: anything-but IMPORT` (`:748-753`) | `events.handleContactChangeMessages` (`template-logic.yaml:139`; `events.js:39-48`)       | Contact: acts only if `organization_id` or `contact_role_ids` changed (`events.js:138-143`); first revokes the inherited membership when the organization changed (`:152-153`), then re-runs flowdown for the old and the new organization's memberships (`:155-173`). Organization: same for `parent_organization_id` / `organization_type_ids` (`:186-257`). |
| `membership-memberships-assigned-contact-changes` | `Microservice: membership-memberships` (`:1025-1027`)                                                  | `events.handleMembershipChanges` (`template-logic.yaml:118`; `events.js:334-369`)         | Diffs `assigned_memberships` old vs new (`handle-changes-to-assigned-memberships.js:37`); writes every current assignment (origin `assigned`, overlaying the per-contact chapters/sections/add-ons as the "parent" copy, `:54-63`); revokes removed ones (`:64`).                                                                                              |
| data-import completion                            | —                                                                                                      | `events.handleDataImportNotificationMessages` (`events.js:98-112`)                        | Re-enqueues a synthetic contact-change for **every organization** in the tenant with `sys_reprocessing: true` (`reprocess-all-flowdown.js:26-57`). Also exposed as the console maintenance task / step function (`template-logic.yaml:199,380`).                                                                                                               |

IMPORT events are skipped twice (filter and handler, `events.js:122,191`; RB-2259 "500k messages gummed up in the
queue", 2023-03-31). Reprocessing sets `securityContext.is_reprocessing` (RB-23) but the code that would delete
"improper" flowdowns during reprocessing is commented out (`flowdown-membership.js:86-92,118-136`).

#### 2.4 The grant algorithm, exactly (`flowdown-membership.js`, `contact-flowdown.js`, `organization-flowdown.js`, `write-inherited-membership.js`)

```
flowdownMembership(M):                                  # M = a saved membership
  if M.owner_type != organization: stop                 # :47
  if M.flowdown_depth > 10: stop                        # :59
  T = membership type of M; S = membership settings     # :67-70
  contacts = all contacts with organization_id == M.organization_id       # :79  (one-shot list, paged)
  children = all organizations with parent_organization_id == M.organization_id (id,name,type_id)  # :80
  opts = T.flowdown_options or {}
  if not T.enable_flowdown: opts.contact_role_ids = []; opts.enable_cascading_flowdown = false   # :96-104
  for each contact c (all settled, failures logged):                      # :108
      eligible = opts.contact_role_ids is missing/null
                 OR intersection(opts.contact_role_ids, c.contact_role_ids) non-empty   # contact-flowdown.js:27-28
      if eligible: writeInherited({contact_id: c.id}, M, S, origin='flowdown')
      else:        revoke({contact_id: c.id}, keepPrincipal=true, keepAssigned=true)
  for each child organization o:                                          # :109
      eligible = opts.enable_cascading_flowdown
                 AND (opts.organization_type_ids empty OR contains o.type_id)   # organization-flowdown.js:26-39
      if eligible: writeInherited({organization_id: o.id}, M, S, 'flowdown')   # saving it fires flowdownMembership again, depth+1
      else:        revoke({organization_id: o.id}, keepPrincipal=true, keepAssigned=true)

writeInherited(target, P, S, origin):                   # P = parent membership
  X = membership record 'contact:<id>' or 'organization:<id>' (the target's ONLY membership)   # :70
  if none: X = new record copied from P's CRM info, re-pointed to the target  # :80-90
  if X.status == 'active':                                                  # :99-111
      if X.origin == principal and S.conflicting_membership_preference == 'principal': stop   # person's own membership wins
      if P.status != 'active' and X.origin == principal: stop               # never let an inactive org overwrite a live personal membership
      # otherwise overwrite (an active principal membership IS overwritten when the setting is 'organization')
  X.origin = origin; if contact: X.organization_id = P.organization_id      # :113-116
  X.flowdown_depth = (P.flowdown_depth or 0) + 1; if >= 10: stop            # :118-124
  X.master_membership_id = P.master_membership_id or P.id; X.master_membership_organization_id likewise   # :127-128
  X.parent_membership_id = P.id                                             # :130
  copy FLOWDOWNABLE_FIELDS from P except X.overridden_fields                # :133-136
  X.type_id = T.flowdown_membership_type_id if set, else P.type_id          # :139-145
  merge P.custom_field_values by id except overridden                      # :149-168
  if deep-diff(X, previous) is empty: stop (no write, no event)            # :171-175
  append history event 'flowdown' | 'assignment' with old/new snapshots; save   # :182-186
```

Properties worth naming: the grant is idempotent and diff-gated; it fans out one write per contact per organization
membership save; it runs for assignment too (`handle-changes-to-assigned-memberships.js:62` calls the same writer with
the default origin `assigned`); and because the target has only one membership record, flowdown **replaces** a person's
own membership unless the conflict rules stop it — the `history` snapshot is the only place the personal membership
survives.

#### 2.5 The revoke algorithm, exactly (`delete-inherited-memberships.js`)

Called with `{contact_id}` or `{owner_type:'organization', organization_id}` and options `doNotDeletePersonalMembership`
(defaults **true**; passing false throws "No... we never delete this", `:55-56`) and `doNotDeleteAssignedMemberships`
(true from flowdown passes; unset from the assigned-roster diff and from the contact-change handler, so those paths do
revoke assigned memberships).

```
revoke(target, opts):
  X = target's only membership; if none: no-op                              # :60-69
  if X.origin == principal:                                                  # :71-87
      leave the membership itself alone
      clear parent_membership_id, master_*, flowdown_depth if set, and save  # RB-6105 (2026-09): otherwise the departed contact
                                                                             # stays on the org's Member Contacts tab and billing notices
      stop
  if opts.keepAssigned and X.origin == assigned: no-op                        # :88-92
  E = latest history entry whose OLD snapshot has origin 'principal'          # :195-219 (open-ended entry sorts last; RB-7487 fix)
  if E exists:                                                                # :99-125  "personal restore"
      X.organization_id = undefined; clear flowdown linkage
      restore every key of E.new from E.old                                   # :105
      keep previous join_date if the snapshot has none                        # RB-7487
      if snapshot has no expiration_date or it is past today: X.status = inactive   # RB-7487 (only ever downgrade)
  else:                                                                       # :126-164  "inherited"
      if target is a contact: sameCompany = ('organization:'+contact.organization_id == X.parent_membership_id)
                              noCompany = contact has no organization_id
      if sameCompany or noCompany: HARD DELETE X; stop                        # :155-159 (noCompany added by RB-5496, 2026-07)
      else (moved to another organization): X.status = inactive              # :162-163 history preserved
  append history 'cancellation'; save                                         # :166-167
```

Timing: there is no scheduled sweep. Revocation happens only when (a) a contact or organization record changes
organization/parent/roles/types (contact-change queue, seconds to minutes after the Rolodex save, subject to queue
depth), (b) an organization's `assigned_memberships` array changes, or (c) flowdown re-runs for the organization
membership and finds the contact no longer role-eligible. When the organization's membership itself lapses, the children
are not revoked; they receive the parent's new `status`/`expiration_date` by copy on the next `flowdownMembership` pass
(`FLOWDOWNABLE_FIELDS`), so they lapse _with_ the parent. Stale records are corrected only when the contact next
changes; RB-6105's PR notes "this does not backfill" and estimates ~650 silent no-op revocations per 30 days and ~11
known live bad records at ACN/DHI (PR #20 body, `gh pr view 20 -R rhythmsoftware/membership-memberships`).

#### 2.6 Assigned memberships and seats

- Who assigns: an organization administrator in the portal "Manage member contacts" (R6 §2) or staff on the console
  membership form ("Assigned Member Contacts" panel,
  `/Users/andrew/rhythm/membership-ngx-console/src/app/members/pages/membership/create-membership/create-membership.component.html:228`),
  and the application form's corporate-contact step at join/renew/change-affiliation.
- Seat cap: `maximum_number_of_contacts` from the package, or `total_seats_purchased` on the membership when present
  (`/Users/andrew/rhythm/membership-ngx-controls/projects/membership-ngx-controls/src/lib/webForms/applications/choose-corporate-member-contacts/choose-corporate-member-contacts.component.ts:98-104`).
- Charging (`/Users/andrew/rhythm/membership-applications/src/logic/apply-fees-for-extra-contacts.js`): only if the
  package allows additional contacts (:23-26); `extra = assigned contacts − available seats` where available seats =
  `total_seats_purchased` for change-affiliation applications, else the package maximum (:33-55); contacts being removed
  in the same change offset the extra (:71-74, RB-5898); the net extra becomes one add-on line item (:95-100).
  Fulfilment increments `total_seats_purchased` by the add-on quantity
  (`…/membership-memberships/src/logic/fulfillment/fulfill-add-on-purchases.js:100-114`); cleaning back-fills it from
  the package base for organization memberships that lack it (`…/src/object/cleaning.js:132-137`).
- The portal roster screen deliberately disables buying more seats (`…/manage-member-contacts.component.ts:89-91`).
- How "the organization's members" are resolved everywhere else: by the `ByParentMembershipId` GSI, i.e. the
  `parent_membership_id` pointer on each child record — console Member Contacts tab
  (`/Users/andrew/rhythm/membership-ngx-console/src/app/rolodex/360/oneClicks/member-contacts/member-contacts.component.ts:66`),
  billing notices (`/Users/andrew/rhythm/membership-billing-steps/src/actions/downstream-member-contacts.js:7,26`), seat
  substitution credit (`apply-fees-for-extra-contacts.js:19,71`). There are therefore two sources of truth for an
  assigned roster: the array on the organization's record and the child records pointing back.

#### 2.7 Could an INDIVIDUAL membership cover other people? — **Absent**

- `owner_type` is `contact | organization` only (schema :78); an individual membership has no `assigned_memberships`
  semantics (the assigned-change handler exits for non-organization owners,
  `handle-changes-to-assigned-memberships.js:29-30`) and flowdown exits for non-organization owners
  (`flowdown-membership.js:47-48`).
- Grep of `membership-*/src` and `events-*/src` (vendor bundles excluded) for `spouse`, `guest`, `dependent`,
  `additional_contact`, `covered`: `spouse` appears only as demo guest data in the events registration demo forms
  (`/Users/andrew/rhythm/events-ngx-controls/src/app/forms/registration/add-guests-demo/add-guests-demo.component.ts:33-35,58-59`);
  `additional_contact` only in the organizational seat-purchase options above; `guest` only in events; `dependent` and
  `covered` only in English prose. No field or flow lets a contact's membership extend to another contact.

#### 2.8 Defect and fix trail in the flowdown code itself (code comments and commits)

| Ticket            | Date       | What it was                                                                                                                    | Where                                                                                                 |
| ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| RB-23             | 2021       | Reprocessing should delete improper flowdowns (never finished; code commented out)                                             | `events.js:167-170`; `flowdown-membership.js:86-92`                                                   |
| RS-1170           | 2020       | Default to never deleting a personal membership                                                                                | `delete-inherited-memberships.js:38`                                                                  |
| RB-728            | 2022-04-10 | Delete (not inactivate) a flowdown membership when the person is still at the granting organization                            | `delete-inherited-memberships.js:138-143`; commit `8aeb7b9`                                           |
| RB-1459           | 2022-08-24 | Edits on a flowdown child were overwritten; added `overridden_fields`                                                          | `write-inherited-membership.js:134,157`; `change-validation.js:84-92`                                 |
| RS-2691           | —          | Chapters/sections/add-ons chosen per assigned contact must propagate to the child                                              | `handle-changes-to-assigned-memberships.js:56-61`                                                     |
| RB-2259           | 2023-03-31 | 500k queued contact-change messages; skip IMPORT events                                                                        | `events.js:121-123`                                                                                   |
| RB-2424           | 2023-04-30 | Cascading flowdown through child organizations                                                                                 | commits `d037eed`, `a87010b`                                                                          |
| RB-4726           | 2024-12    | Child-organization payloads too big                                                                                            | `flowdown-membership.js:23-24`                                                                        |
| RB-4262 / RB-5898 | 2024–2025  | Change-affiliation charged for seats already paid / removals not credited                                                      | `apply-fees-for-extra-contacts.js:62-91`                                                              |
| RB-5875           | 2026-03-10 | Substitutions charged; introduced `total_seats_purchased`                                                                      | schema :138-139; `fulfill-add-on-purchases.js:100-114`                                                |
| RB-6102 / RB-6130 | 2026       | Renewal double-counted chapter/section/add-on dues for existing contacts                                                       | `apply-fees-for-extra-contacts.js:24-26,58-60`                                                        |
| RB-5496           | 2026-07-21 | Flowdown-only member with organization removed stayed "inactive flowdown member"                                               | `delete-inherited-memberships.js:154-159`; `docs/changelog/RB-5496-flowdown-removal-cleanup.md:65-74` |
| RB-6105           | 2026-09-15 | Departed principal members still on Member Contacts tab and in billing notices (ACN, DHI)                                      | `delete-inherited-memberships.js:75-86`                                                               |
| RB-7487           | 2026-09-15 | "Contacts removed from Groups Not Having Membership properly cancelled": oldest snapshot restored as active with no expiration | `delete-inherited-memberships.js:107-122,200-206`                                                     |

Support/Jira history from the 2.0 CRM evidence file
(`git -C ~/rhythm2/services-platform show origin/crm-architecture:docs/proposals/crm/evidence/e06-jira-zendesk-history.md`;
the households worktree does not carry it), items touching flowdown, assignment, rosters and merges:

- P1 (merge orphans): RB-22 (2021-07-29) organization merge leaves inherited flowdown membership pointing at the deleted
  organization; RB-916 (2022-03-31, PSA) membership history lost; RB-1879 (2022-12-16, NAEMSP/PSA) membership
  sporadically not moved; ZD 8209 (2023-09-25, ISTSS); RB-6392 (2026-01-27) parent organization cannot edit membership
  after a child-organization merge; RB-7337 (2026-07-01) membership save fails with ForeignKeyViolation and loops in a
  DLQ; RB-7641 (2026-09-04, NLA) paid renewal never advances because of a stale contact id (e06 line 19).
- P5 (roles drive flowdown and billing, cardinality unenforced): RB-7662 (2026-09-09, SBAM) 828 organizations with more
  than one Owner, one with 57, "so an arbitrary person receives the dues invoice"; RB-7093 (2026-05) primary contact
  role missing on organization memberships; RB-7874 (2026-10-09) change-affiliation removes the primary chapter; RB-5829
  / ZD 16777 / ZD 20596 role limits by organization type ignored in the portal (e06 line 23).
- P7 (queries): "one-to-many outputs (logins, membership status, assigned memberships) multiplied rows"; RB-5996
  (2025-10) duplicate contacts when membership status is added (e06 line 25).
- P10 (portal roster): RB-5131 / RB-5222 / RB-5743 (2025) cannot add or remove contacts; RB-5646 / RB-6617 / RB-6986
  (2025-07 → 2026-04, MSA, DHI) child organizations not editable in the portal; RB-5630 (Agile Alliance) roster of 614
  shows none; ZD 21085 (2026-09, IDEC) profile save blocked by a ForeignKeyViolation to a non-existent organization
  membership; ZD 4026 (2022) institutional representatives cannot see their organization's contacts (e06 line 28).
- `~/rhythm2/briefs`: the only RB mentions of flowdown/assigned/seat are in R6 itself (lines 184-186); no other brief
  has tracked them.

### 3. Eligibility and benefits (`/Users/andrew/rhythm/libraries-eligibility`) — **Implemented in 1.0**

#### 3.1 Every criterion kind (`src/eligibility.schema.js`)

| Kind                          | Fields                                                                                                            | Evaluated in                       | Reads                                                                                                                                                                                            |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Evaluate the employer instead | `use_organization_for_eligibility` (:45)                                                                          | `determine-eligibility.js:72-79`   | `contact.organization_id` → organization becomes the customer; "contact has no organization" fails                                                                                               |
| Inversion / customer type     | `logic_operator: and                                                                                              | not`(:46);`customer_type` (:47)    | `:84-92,104-106`                                                                                                                                                                                 | —                                              |
| Membership                    | `members`, `non_members` (:48-49); `member_options.member_type_ids / member_package_ids / member_statuses (active | inactive                           | suspended) / member_status_reason_ids / chapter_members (+ids, types, and                                                                                                                        | not) / section_members (+ids, types)` (:50-70) | `membership-logic.js:28-117` | the customer's **own** record `contact:<id>` or `organization:<id>` (`determine-eligibility.js:35-38,117`) |
| Committee                     | `committee_members` + committee ids/types/positions (:72-80)                                                      | `committee-logic.js`               | committee memberships                                                                                                                                                                            |
| Certification                 | `certificants` + program ids (:82-86)                                                                             | `certification-logic.js`           | certifications                                                                                                                                                                                   |
| Contact/organization status   | `enable_status_restriction`, `status_restrictions[]` (:88-92)                                                     | `status-logic.js`                  | the CRM record's `status`                                                                                                                                                                        |
| Organization                  | `organization_restrictions.organization_type_ids`, `.contact_role_ids` (:94-99)                                   | `organization-logic.js:20-59`      | contact's `contact_role_ids`; employer's `type_id` via `organization_id`                                                                                                                         |
| Age                           | `minimum_age`, `maximum_age` (:100-104)                                                                           | `age-logic.js` (R6 §1.5)           | `date_of_birth`                                                                                                                                                                                  |
| Location                      | countries, negate (:106-110)                                                                                      | `location-logic.js`                | address country                                                                                                                                                                                  |
| Awards                        | award types/ids (:112-118)                                                                                        | `award-logic.js`                   | awards                                                                                                                                                                                           |
| Event registrations           | categories/events (:120-125)                                                                                      | `event-registration-logic.js`      | registrations                                                                                                                                                                                    |
| Donors                        | donor clubs (:127-131)                                                                                            | `donor-logic.js`                   | fundraising                                                                                                                                                                                      |
| Subscribers                   | publication ids, and                                                                                              | or                                 | not (:133-143)                                                                                                                                                                                   | `subscriber-logic.js`                          | subscriptions                |
| Advanced JSON rules           | `advanced_rules` (:145-151)                                                                                       | `advanced-logic.js`                | facts: `contact`, `organization`, `membership`, `organizationMembership` (employer's `organization:<id>` membership), `parentOrganization`, custom fields flattened (`advanced-logic.js:69-115`) |
| Expression                    | `expression` (:153-155)                                                                                           | `expression-logic.js:12-31`        | same facts                                                                                                                                                                                       |
| Custom Lambda                 | `custom_logic_options` (:157-161)                                                                                 | `determine-eligibility.js:179-197` | payload `{customer, customerType, membership, criteria}`                                                                                                                                         |
| Entitlements                  | `entitlement_restrictions[{type_id, quantity}]` (:163-169)                                                        | `entitlement-logic.js:18-29`       | `commerce-entitlements.checkEntitlement(owner_type, owner_id, type_id, quantity)`                                                                                                                |

Relationship, spouse, household, dependent, guardian: **Absent** in the schema and in every logic module. Evaluation
order is fixed (age, membership, committee, certification, award, status, organization, location, events, donors,
advanced, expression, entitlement, subscriber, custom; `determine-eligibility.js:112-198`) and the first failing reason
is returned.

#### 3.2 How "is a member" was evaluated

- One lookup: the membership record whose id is the customer's own id (`determine-eligibility.js:117`). A contact is a
  member only through **their own** record, which (§2) may be a principal, flowdown or assigned record — so flowdown
  _is_ how an employee became "a member" for eligibility purposes.
- `members: true` → fail only if **no record exists** ("A valid membership is required", `membership-logic.js:35-37`);
  package, type, status, status-reason, chapter and section filters apply only when configured (:43-108). An inactive or
  suspended record therefore passes a bare `members` rule (unit test "when no status specified",
  `tests/unit/membership-logic.spec.js:211-216`, covers only the active case). `non_members: true` → fail if any record
  exists (:114-116).
- `member_statuses` cannot name `active without benefits` (schema :53), so that status is treated like any other record
  unless excluded by an advanced rule.
- Through the employer: `use_organization_for_eligibility` evaluates the organization's record instead (:72-79), and
  advanced rules/expressions can test `organizationMembership.status` directly (`advanced-logic.js:73-77,110`). There is
  no "member of the same household/family" path.

#### 3.3 How a benefit or entitlement was represented

- **Member pricing**: every product carries `price` and `member_price` (also per variant and per foreign currency)
  (`/Users/andrew/rhythm/collect-products/src/schema/product.schema.js:26,143,156`). The order engine grants member
  pricing to the whole order when the order owner's own membership has `status === 'active'` **or** the order contains a
  membership package (`/Users/andrew/rhythm/commerce-order-logic/src/pricing/calculate-order-price.js:334-347`); a line
  item shipped to another contact/organization is re-evaluated against that recipient's membership (`:61-75`).
  `active without benefits` and flowdown/assigned records with `status inactive` do not qualify; a flowdown record with
  `status active` does, identically to a principal one.
- **Special prices**: `special_prices[{name, criteria (full eligibility schema), price, formula}]` on any product
  (`product.schema.js:104-120`) — the general mechanism for "member of type X pays Y".
- **Member-only purchase**: `enable_purchasing_eligibility` + `purchasing_eligibility_criteria` on products
  (`product.schema.js:98-102`); on event registration types `enable_restrictions` + `restrictions.criteria`
  (`/Users/andrew/rhythm/events-registration-types/src/schema/object.schema.js:44-49`); eligibility criteria are
  likewise attachable to application processes, add-ons, functions, certifications, submissions and portal policies (R6
  §1.5).
- **Entitlements** (a purchasable, countable right): `commerce-entitlements` rows with `type_id`,
  `owner_type contact | organization`, `quantity`, `expiration_date`, order linkage
  (`/Users/andrew/rhythm/commerce-entitlements/src/schema/object.schema.js:14-29`); types are name + active
  (`…/commerce-entitlement-types/src/schema/object.schema.js:14-17`). Granted by orders, subscriptions, certifications,
  awards, submissions, job boards, documents (SDK consumers by `package.json`); **never by membership-\*** and never
  inherited: an entitlement belongs to exactly one owner record.
- **Application eligibility by status**: `active without benefits` members may only join or change affiliation; active
  members renew within the window; inactive members reinstate
  (`/Users/andrew/rhythm/membership-application-processes/src/logic/get-eligible-processes.js:198-216`).

### 4. Assessment

#### (a) What 1.0's relationship model got right and wrong

Right:

1. Types were tenant-configurable and typed on both ends, and the service enforced the type's side types at save
   (`rolodex-relationships/src/object/cleaning.js:37-41`), so a Spouse could not silently link a contact to a company.
2. Relationships were dated (`start_date`/`end_date`) with Current/Past/Future views — the temporal shape a household
   needs (`relationships-one-click.component.ts:131-139`).
3. Both endpoints were indexed (`ByLeftSideId`, `ByRightSideId`), so "everything related to X" was one pair of reads
   regardless of side (`relationships-helper.service.ts:29-40`).
4. Referential integrity was declared: a type in use could not be deleted and a link died with either endpoint
   (`rolodex-relationships/config.js:70-85`) — no dangling relationship rows, unlike flowdown's copied ids.
5. The record was deliberately generic (`left_side_type` any service, `retrieve-item.js:1-3`) and separately
   permissioned, so a future consumer (soft credits proved it) could attach semantics without a schema change.

Wrong:

1. The type carried **no direction or reciprocal semantics** — one `name` served both sides
   (`rolodex-relationship-types@master:src/schema/object.schema.js:15-21`), so "Parent of" read the same on the child.
2. Direction was an artefact of which page the user started from, with no uniqueness, so A→B, B→A and duplicates
   coexisted (`create-relationship.component.ts:87-106`); the only direction-aware consumer (soft-credit
   `left to right | right to left | both`) therefore behaved inconsistently for symmetric types.
3. The relationship had no attributes beyond notes (no primary flag, no "lives with", no guardian/consent, no
   percentage), so every consumer needed its own side table (soft credits did).
4. Nothing consumed it except fundraising; no queries, no portal, no membership or events use — the link rotted and
   support advised against it (R6 §1.2). Names were copied at save and never refreshed (`cleaning.js:51-53`).
5. On merge, cascade-delete plus hard-delete of the loser meant the relationships of a merged record vanished rather
   than moving (inference from `config.js:76-85` and e06 P1).

#### (b) What 1.0's flowdown got right and wrong

Right:

1. Policy lived on the membership type as declarative options (roles, cascade, organization types, depth cap), not in
   code per tenant (`membership-types/src/schema/object.schema.js:26-38`).
2. The grant was idempotent and diff-gated (`write-inherited-membership.js:171-175`), with a tenant-wide reprocess that
   re-derives everything (`reprocess-all-flowdown.js:26-57`).
3. Lineage was explicit — `origin`, `parent_membership_id`, `master_membership_id`, `flowdown_depth`, history events —
   so a record could say why it existed (schema :94-128,206-220).
4. Local edits were protected automatically (`overridden_fields`, `change-validation.js:82-116`), and the "person's own
   membership wins" conflict preference existed (`membership-settings … :189`).
5. The seat model charged net-new seats only, credited removals and substitutions, and persisted `total_seats_purchased`
   across the term (`apply-fees-for-extra-contacts.js:33-100`; RB-5875).

Wrong:

1. **One membership record per person** (`generate-identifier.js:38-47`): a benefit inherited from an employer overwrote
   the person's own membership instead of being a second, derived grant; "restore the personal membership" from a
   history snapshot was the band-aid, and it was wrong until 2026 (RB-7487).
2. Benefits were **copied as data** (status, type, dates, chapters) rather than evaluated at use time, so every change
   on the parent had to fan out through queues (500k-message backlogs, RB-2259; manual "reprocess" after any config
   change, console alert `create-membership-type.component.html:27-31`), and children drifted whenever a message was
   lost.
3. Revocation keyed off CRM change events only, with three branches whose outcomes differed by history, and two of the
   three were wrong for years (RB-5496, RB-6105, RB-7487). Nothing swept stale records.
4. Membership-side copies of contact and organization ids were not registered references, so merges and deletions left
   dangling children, FK-violation loops and silent renewal failures (RB-22, RB-7337, RB-7641) — and the roster, billing
   notices and seat credits were all derived from the `parent_membership_id` back-pointer
   (`member-contacts.component.ts:66`; `downstream-member-contacts.js:26`), i.e. from the copy, not from the
   affiliation.
5. Eligibility and billing leaned on contact roles whose cardinality was never enforced, so "flow to Owners" and "bill
   the Owner" picked arbitrary people (RB-7662: 828 organizations with several Owners), and switching flowdown off ran a
   revoke pass over every contact (`flowdown-membership.js:96-104`) rather than simply stopping.

#### (c) Concept → 1.0 representation → defect pattern

| Concept                          | 1.0 representation                                                                                                                                                                                                                            | Defect pattern                                                                                                                                                                                                                                            |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Holder (who has the membership)  | The one record `contact:<id>` / `organization:<id>` with `origin principal                                                                                                                                                                    | flowdown                                                                                                                                                                                                                                                  | assigned` (`membership-memberships … object.schema.js:78-86,121`; `generate-identifier.js:38-47`) | One record per person: an inherited grant overwrites the personal membership; restore-from-snapshot resurrected lapsed memberships as active (RB-7487); `members` eligibility is "a record exists" (`membership-logic.js:35`) |
| Payer                            | `bill_organization` boolean on membership and package (`object.schema.js:162`; `membership-packages … :91`); dues invoice addressed via contact roles (Owner/Billing)                                                                         | Role cardinality unenforced → arbitrary invoice recipient (RB-7662); primary contact role missing on organization memberships (RB-7093); no "someone else pays" except the employer (R6 §5)                                                               |
| Beneficiaries — flowdown         | Child records written per contact/child organization by role and organization type; `parent_membership_id`, `master_*`, `flowdown_depth`; fields copied (`write-inherited-membership.js:15-33,113-136`)                                       | Copy drift and queue fan-out (RB-2259); revoke branches wrong (RB-5496/6105/7487); merges orphan children (RB-22, RB-7337, RB-7641); disabled flowdown still revokes (`flowdown-membership.js:96-104`)                                                    |
| Beneficiaries — assigned/seats   | `assigned_memberships[]` on the organization record + child records with origin `assigned`; `maximum_number_of_contacts`, `total_seats_purchased`, add-on product (`object.schema.js:130-139`; `membership-packages … :82-88`)                | Two sources of truth (array vs back-pointer); substitutions and renewals over-charged until RB-5875/5898/6102; portal roster failures and child organizations not manageable (RB-5131/5222/5743/5646/6617/6986, RB-5630); rows multiplied in queries (P7) |
| Eligibility                      | `libraries-eligibility` criteria object attached to products, registration types, processes; evaluates the customer's own membership, optionally the employer's (`eligibility.schema.js:45-169`; `determine-eligibility.js:72-79,117`)        | No relationship/household criterion; status not checked unless configured; "active without benefits" unaddressable in `member_statuses` (:53); member-only means "has a record"                                                                           |
| Entitlement (the benefit itself) | `member_price` + `special_prices[criteria]` on products; member pricing iff own status active or a package in the order (`calculate-order-price.js:334-347`); countable `commerce-entitlements` per owner record (`… object.schema.js:14-29`) | Benefits attach to exactly one owner record; flowdown gives benefits only by cloning the record; entitlements never inherited or shared; pricing and eligibility disagree on what "member" means                                                          |
| Relationships                    | `rolodex-relationships` typed, dated, two-GSI link; type = name + side types + active, no direction (`rolodex-relationships … object.schema.js:17-30`; `rolodex-relationship-types@master … :15-21`)                                          | Direction by page of origin, duplicates, no reciprocal label; no consumer but soft credits; not queryable, not in portal; cascade-deleted on merge; names stale                                                                                           |

### 5. What the code could not answer

1. Whether any tenant set `flowdown_membership_type_id` through the API (the console never offered it).
2. How many tenants run `conflicting_membership_preference = 'organization'` (the only setting that lets an inherited
   membership overwrite an active personal one).
3. Whether `enable_cascading_flowdown`'s schema default of `true` ever cascaded for tenants whose console had the
   `membership_cascading_flowdown` flag off (the server applies the option regardless of the flag).
4. Production counts of relationships per type and direction (needs a DynamoDB scan; not run).

## 28. W1 — How Blackbaud products model households, family relationships and family memberships

## W1 — How Blackbaud products model households, family relationships and family memberships

Research brief for the Rhythm 2.0 "Households" architecture proposal. Written 2026-10-21 (ET) from public sources only.

**Evidence labels used throughout**

- **Verified** — Blackbaud/Convio product documentation (help files on webfiles-sc1.blackbaud.com, PDF user guides on
  help.blackbaud.com, kb.blackbaud.com, secure2.convio.net product help).
- **Reported** — Blackbaud Community forum threads, the Raiser's Edge NXT Ideas portal (renxt.ideas.aha.io),
  implementer/partner blogs, customer procedure manuals.
- **Inferred** — my reasoning from the above. Never a description of an internal database model.

**Access caveat.** Most `community.blackbaud.com` discussion URLs redirect to a Blackbaud sign-in when fetched directly;
the content for those is taken from search-engine result summaries and is marked Reported with that caveat. Ideas-portal
pages, help files and PDF guides were read in full.

---

### 1. Raiser's Edge NXT (and the Raiser's Edge 7 "database view")

#### 1.1 The core shape: no household record; a spouse link between two constituents

**Verified.** Raiser's Edge has no household entity. Households are expressed as a _spouse relationship_ between two
individual records plus a _Head of Household_ flag on that relationship. The web view help says spousal relationships
"help ensure proper addressees and salutations" and help "prevent duplicate communications to the same household"
([RE NXT Relationships help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html)).
When you add an individual-to-individual relationship you can "select This individual is the spouse", enter "the dates
of the relationship, such as when the constituent and individual were married or divorced", and "Under Head of
household, select whether one of the spouses is the head of household" or "create this relationship without a head of
household"
([Add a relationship](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/en-au/content/bb-constituents-add-relationship.html)).

**Verified.** The spouse need not be a constituent. A relationship added without "the button" stays a non-constituent
record: "When you add a relationship, they aren't automatically a constituent" and marking "X is the spouse" "adds the
new record as a non-constituent record" (Blackbaud admin on
[RENXT-I-3003](https://renxt.ideas.aha.io/ideas/RENXT-I-3003)). Non-constituent spouses are excluded from
head-of-household logic: "If one spouse isn't a constituent, you can't mark either of them as head of household"
([Relationships help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html)).

**Verified (RE7).** In the database view the flag is the "This individual is the 'Head of Household'" checkbox on the
Spouse button of the constituent record
([RE7 Mail guide, p. ~1580 of text](https://help.blackbaud.com/docs/0/assets/guides/re/mail.pdf)). The Knowledgebase
states "The default head of household is the constituent on which the spouse was added"
([KB 48275, snippet](https://kb.blackbaud.com/articles/Knowledge/48275)); a Blackbaud Community blog says the same
differently: "By default, The Raiser's Edge marks the record that was entered into The Raiser's Edge first as HOH"
([Constituent Queries To-Go Part 3](https://community.blackbaud.com/blogs/10/2167), Reported, login-walled).

**Reported.** NXT web view gained head-of-household management only in June 2020, after a 338-vote idea opened in 2016;
before that it was database-view only. A 2019 commenter: "neither constituent is being automatically marked head of
household when linking/creating spouse relationships in NXT... those folks were being dropped off our mailing list since
we only mail to the head of household" ([RENXT-I-375](https://renxt.ideas.aha.io/ideas/RENXT-I-375)). A list filter
"Include head of household only" followed in July 2020 ([RENXT-I-1911](https://renxt.ideas.aha.io/ideas/RENXT-I-1911));
release notes say the filtered list "will still include constituents who aren't married or have spouses who aren't
constituents" ([What's New, June 16](https://community.blackbaud.com/blogs/13/6889), login-walled summary). One
third-party integrator states flatly that "Raiser's Edge NXT doesn't support native householding functionality"
([Avid AI help](https://help.avidai.com/hc/en-us/articles/48180816343059-Enable-spouseholding-for-Raiser-s-Edge-NXT),
Reported; page returned 403 on direct fetch, text from search summary).

**Answers to the checklist**

| Question                             | RE NXT / RE7 answer                                                                                                                                                                                                                                                    |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Households as distinct records?      | No. Two constituents linked by a spouse relationship; one flagged Head of Household. (Verified)                                                                                                                                                                        |
| Multiple households per contact?     | Not applicable; a constituent has at most one spouse link. Other relationship types (Parent/Child, Friend) are free-form and unlimited. (Verified: [Relationships help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html)) |
| Spouses/partners/children/dependents | Spouse (incl. domestic partner) is a special relationship with the spouse checkbox; children, extended family and friends are ordinary individual relationships with a type and reciprocal type, optionally non-constituents. (Verified)                               |
| Automatic vs manual                  | Manual. The spouse link, head of household, soft-credit rule and shared address are each set by hand. (Verified/Reported)                                                                                                                                              |

#### 1.2 Addresses and contact details

**Verified (RE7).** Addresses can be "copied and linked (shared)" between records; shared phones follow the link: "When
you copy and link (share) an address, all phone numbers designated as Shared... are copied and shared", and "When you
break the link between shared addresses, all shared phone numbers change to Not Shared"
([RE7 Configuration guide](https://help.blackbaud.com/docs/0/assets/guides/re/config.pdf)). Web view release notes
(Feb 2021) say you can "add new addresses or share them across other records, such as a constituent's spouse or
employer" ([What's New Feb 21](https://community.blackbaud.com/blogs/13/3205), Reported summary).

**Reported.** Shared addresses bite on divorce: "only Jane has a new address, the new address is applied to both of
them" ([Spouse addresses in RE](https://community.blackbaud.com/discussion/62696/spouse-addresses-in-re), login-walled
summary). A diocesan procedure manual instructs staff, after a divorce, to add a fresh address "Even for the spouse who
might be keeping the same address... to remove the 'shared address' with the former spouse"
([Diocese of Jefferson City RE NXT procedures, Oct 2024](https://diojeffcity.org/wp-content/uploads/2024/10/0-RE-NXT-Procedures-PRINT-THIS-ONE.pdf)).

**Reported.** Email is per constituent, so a couple sharing one record cannot each receive NXT email: "I have a husband
and wife who both want their own emails but they share the same record in RE"; another sends every email twice because
she "cannot make separate constituents of these people" ([RENXT-I-2806](https://renxt.ideas.aha.io/ideas/RENXT-I-2806),
merged into RENXT-I-3340, still open). Online Express (being retired) could email head of household plus spouse; NXT
email cannot ([same idea](https://renxt.ideas.aha.io/ideas/RENXT-I-2806)).

#### 1.3 Addressees and salutations

**Verified (RE7).** Addressee/salutation formulas are configured in Config with spouse fields and a "Smart" option that
drops the joiner when the spouse field is empty: "if the addressee is 'Mr. and Mrs. William Smith' and you select Smart
by the Field Name Spouse Title 1, 'and' is eliminated if no entry appears in the Spouse field"
([RE7 Configuration guide](https://help.blackbaud.com/docs/0/assets/guides/re/config.pdf)). There is also a business
rule "Automatically update spouse having the same last name" that renames the spouse when the constituent's last name
changes, "even if they are not constituents" (same guide).

**Verified (RE7 Mail).** When a mailing selects both spouses, "Head of Household Processing" offers three choices: "Mail
only to constituents marked 'Head of Household'", "Mail to first constituent found", "Mail to both constituents
separately". Crucially, "The Head of Household Processing frame is used only when both people of the household are
included in the query or mailing... If only the person not marked Head of Household is in the query, that person will
not be excluded" ([RE7 Mail guide](https://help.blackbaud.com/docs/0/assets/guides/re/mail.pdf); same wording in
[KB 48275](https://kb.blackbaud.com/articles/Knowledge/48275)). The "first constituent found" option is described by
users as unpredictable ([Addressee and queries](https://community.blackbaud.com/discussion/comment/230787), Reported
summary).

**Reported.** Users keep separate "household" addressee/salutation types ("The Smith Family") alongside the primary one
and want defaults applied to records created by online forms, which currently arrive with none
([RENXT-I-3178](https://renxt.ideas.aha.io/ideas/RENXT-I-3178), merged into RENXT-I-3828, Planned).

#### 1.4 Giving: hard credit, soft credit, household totals

**Verified (RE7).** The gift record has a Soft Credit tab; business rules decide whether reports/mailings/queries credit
"the Donor, Soft credit recipient, or Both" and how to split ("Use distribution on gift", "Split evenly", "Full amount
to all") ([RE7 Configuration guide](https://help.blackbaud.com/docs/0/assets/guides/re/config.pdf)).

**Verified (NXT).** The spouse relationship carries an opt-in: "If the spouse is also a constituent, you can track the
gifts received from either constituent as soft credit giving for the other"
([Relationships help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html)).
Web view only gained the switch on 12 Nov 2024: "under Relationships on the constituent record, select Edit
relationship... Select Automatically soft credit this individual for gifts"
([What's Recent 2024](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-whats-recent-date-list-2024.html)).
It had been requested since 2019: "I can't believe this isn't automatic"
([RENXT-I-2064](https://renxt.ideas.aha.io/ideas/RENXT-I-2064), merged into RENXT-I-6418, Shipped).

**Verified (NXT).** The constituent Giving summary includes soft credits
([Constituent Records help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-constituent-record.html));
admins choose which gift types and soft credits count toward "recognition" lifetime totals
([What's New: Recognition](https://community.blackbaud.com/blogs/13/5069), Reported summary).

**Reported.** The auto soft-credit rule is not retroactive: "The auto checkbox doesn't go back in time, only does it for
gifts entered going forward", so historic spousal credit needs a global change or import
([Soft Crediting Spouse](https://community.blackbaud.com/discussion/comment/221087/), login-walled summary). Policies
vary: some soft-credit every spouse, others only where a spouse influenced the gift
([Do you soft credit spouses?](https://community.blackbaud.com/discussion/62193/do-you-soft-credit-spouses)). The
diocese manual's rule: "all gifts be recorded on the Head of Household record... and a soft credit be recorded for the
spouse record"
([Diocese procedures](https://diojeffcity.org/wp-content/uploads/2024/10/0-RE-NXT-Procedures-PRINT-THIS-ONE.pdf)).

**Not found.** No vendor documentation of a _household_ giving rollup (sum of both spouses without double counting);
community posts describe building it by querying head of household + soft credits
([RENXT-I-375 comments](https://renxt.ideas.aha.io/ideas/RENXT-I-375)). There is no household-level action or proposal:
users ask for an action "shared" between spouse records so it "only feed[s] reports for the Head of Household"
([RENXT-I-534](https://renxt.ideas.aha.io/ideas/RENXT-I-534), open since 2016).

#### 1.5 Duplicates and merge

**Verified.** Web view flags "possible duplicate constituent records" with a strength and the matching fields (name,
address, phone, email); automatic matching on form submissions requires that "name and email fields must match exactly"
([Manage and Merge Duplicates](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-duplicate-records.html)).
In the merge screen you pick source/target ("Switch source and target"), can "Exclude soft credits when merging gifts",
and "Any fields that don't appear on this list must be merged in database view"
([Review Possible Duplicates](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/data-identify-duplicates.html)).
**Reported:** membership and education data are among the things web view does not merge
([Merging Constituent Records](https://community.blackbaud.com/discussion/comment/264082/), login-walled summary).
Spouse-specific merge behaviour is not documented.

#### 1.6 Memberships module (RE7 guide; same data exposed in NXT web view)

**Verified.** A membership belongs to one _primary member_. "Gifts cannot be linked to joint memberships because
memberships are stored by the name of the primary member"
([RE7 Membership Data Entry guide](https://help.blackbaud.com/docs/0/assets/guides/re/mem_data.pdf), p. 53). Each
Membership Category sets a default maximum number of members; a business rule copies it into "Total number of members on
this membership" (same guide, pp. 22, 41).

**Verified — joint members and cards.** Under **Members and Cards** ("Other Members and Card Information") you add "New
Joint Members/Card Recipients". Each is a _relationship_ of the primary member (constituent or not): "You must mark the
Joint Member checkbox [if] you want the name of the joint member to appear in membership reports"; the type can be Joint
or other; card fields include an editable "Name on card", "Card expires" = "Same dates as membership" or "Specific
dates", "Number to print", card address drawn from the primary member's or the joint member's addresses, and an optional
photo ID for scanning (pp. 41–45). Once saved, "You cannot edit the Relationship record field... you must delete the
Joint Member or Card Recipient record and add" again (p. 43). NXT web view now has a "Members and card information"
tile: "You cannot remove a primary member"; card (joint) members "may be linked to constituent records; if they are not,
they are not constituents and exist only on the membership"
([Membership Records help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-membership-records.html)).

**Verified — who pays, who is renewed.** "A constituent can pay dues for her own membership or any other membership",
recorded as a _Gift of Membership_; "you can specify whether the donor or the recipient of the membership receives the
renewal notices and benefits"; "Only individuals entered as constituents... can give a gift of membership" (pp. 46–50).
Business rule: "Automatically add member as soft credit recipient... This applies only to the primary member, not joint
members" (p. 24).

**Verified — lifecycle.** Transactions are Joined, Renew-Same/Upgrade/Downgrade, Upgrade, Downgrade, Drop, Rejoin;
"Dropped memberships must be rejoined"; a rule can "Do not allow multiple concurrent memberships" or warn; lifetime
memberships "do not expire, nor do they receive membership renewals" and should be dropped on death for accurate
reporting (pp. 23, 37, 92–106). Deleting the most recent transaction reverts the membership; deleting the Joined
transaction removes the record
([Membership Records help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-membership-records.html)).

**Reported.** Pulling joint members from the constituent side fails when "not all of the joint members were constituents
in RE" ([Problem Pulling Members from Memberships](https://community.blackbaud.com/discussion/comment/210354/),
login-walled summary). Web-view membership arrived March 2024 and only for contracts that include the database-view
module ([Membership Capabilities in Web View](https://community.blackbaud.com/blogs/13/9406);
[What's Recent 2024](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-whats-recent-date-list-2024.html)).

#### 1.7 Life events

**Verified.** The web view advises, for divorce, "add an end date rather than delete" the relationship
([Relationships help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html)).
RE7 has "Deceased Constituent Rules" including "rules for when a deceased constituent has a spouse"
([RE7 Configuration guide, TOC](https://help.blackbaud.com/docs/0/assets/guides/re/config.pdf)). Marking deceased in web
view is a "Mark as" action
([Constituent Records help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-constituent-record.html)).

**Reported — what organisations actually do.** Divorce: set marital status, note "Formerly married to...", delete the
spouse relationship ("This will also remove the spouse from the other record"), un-share the address; death: mark
inactive then deceased, update the surviving spouse's addressee/salutation, then verify the survivor is "1) marked Head
of Household; 2) marked widowed; 3) address is not marked [shared]"
([Diocese procedures](https://diojeffcity.org/wp-content/uploads/2024/10/0-RE-NXT-Procedures-PRINT-THIS-ONE.pdf)).
Others keep the deceased spouse linked with a "Deceased Spouse/Widow" relationship type
([Dissolve households when one member dies?](https://community.blackbaud.com/discussion/55584/dissolve-households-when-one-member-dies),
login-walled summary). Children "should not have their own record until they become the Head of a Household" and are
added as non-constituent relationships to _both_ parents
([Diocese procedures](https://diojeffcity.org/wp-content/uploads/2024/10/0-RE-NXT-Procedures-PRINT-THIS-ONE.pdf)).

#### 1.8 Self-service

**Verified.** NXT membership forms let you set "the number of additional members allowed" per membership and "Enable
gift of membership"; constituent codes are applied to "the member's record and the gift recipient's record"
([Membership forms tutorial](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/memform-get-started-tutorial.html)).
How additional members are captured and matched is not described. **Reported:** donation-form constituents arrive with
no addressee/salutation ([RENXT-I-3178](https://renxt.ideas.aha.io/ideas/RENXT-I-3178)); Luminate Online profile changes
that add or remove a spouse must be manually reviewed before syncing
([RE–LO integration example scenarios](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/relo/Content/ExampleScenario1.html)).

#### 1.9 Complaints, condensed (all Reported)

- Head of household silently drops people from mailings when unset or when one spouse is a non-constituent
  ([RENXT-I-375](https://renxt.ideas.aha.io/ideas/RENXT-I-375);
  ["Head of Household" filter doesn't work unless EVERYONE has a spouse](https://community.blackbaud.com/discussion/comment/229415/)).
- Gift officers "look at the spouse and don't see actions and gifts"
  ([RENXT-I-375](https://renxt.ideas.aha.io/ideas/RENXT-I-375)); "I don't need one household showing up twice in a gift
  officer list" (same).
- Spouses "taking up limited records" drives organisations to keep one record per couple, which then breaks per-person
  email ([RENXT-I-3003](https://renxt.ideas.aha.io/ideas/RENXT-I-3003);
  [RENXT-I-2806](https://renxt.ideas.aha.io/ideas/RENXT-I-2806)).
- Web/database-view parity gaps for years: HOH (2020), auto soft credit (2024), membership (2024), merge of memberships
  (still database view).

---

### 2. Blackbaud eTapestry

#### 2.1 One account per person or per couple, linked by a "household relationship"

**Verified.** Two sanctioned patterns: a single account for a couple that "will always participate and give to your
Organization together" (one set of Defined Fields, one Journal), or separate accounts linked by a relationship, which
"requires a lot more work to set up" but keeps "unique Defined Fields and Journal histories for each partner"
([Couples and Households best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html)).
Blackbaud's recommendation: "we recommend you create an account for each individual in a household when possible",
because separating one account later is complex and a combined account causes "incorrect salutations or names" and NCOA
problems
([Family and Couple Accounts](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapfamcoup.html)).

**Verified.** Households "link together multiple accounts in eTapestry in order to view their data as a whole group". On
any relationship (Spouse, Friend...) you set _Household Relationship_ = Primary for one side; the other "automatically
becomes a 'Member of the household'". "You cannot have more than one Primary in a Household"; "Accounts cannot, however,
be members of multiple households", yet "an account can be the primary account on more than one household". Dissolve by
setting Household Relationship to N/A
([Households help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etaphouseholds.html)).
Start/end dates on relationships are optional and suggested for "past spouses"
([best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html)).

**Reported.** Account-count pricing pushes some orgs to one account per couple
([Household vs Individual Accounts](https://community.blackbaud.com/discussion/comment/202224), login-walled summary).

#### 2.2 Personas: address sets, including a Joint persona

**Verified.** A persona is an address/contact set on an account; default types are Personal and Business, and orgs add
"Joint or Seasonal". "Each account requires a primary persona." The best practice for couples: a Personal persona on
each account plus "a Joint Persona to the Primary partner's account"; "The Joint Persona is used if you want to address
both partners of the Relationship in a mailing"; "The Joint persona reflects the family or couple; not individual
information"
([Personas page](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etappersonas.html);
[best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html)). Name formats
on a persona: Short Salutation ("John and Mary"), Long Salutation ("Mr. and Mrs. Smith"), plus a Family Name Format for
single-account households
([Family and Couple Accounts](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapfamcoup.html)).

#### 2.3 Head of household, mailings and reports

**Verified.** eTapestry's term is _Primary_; the docs stress it "doesn't necessarily mean they are considered dominant
in the Household". Mailings: group labels/documents by Household with the Joint persona first in the hierarchy so "only
the Primary's Joint Persona will be listed on the label" and "all Members of the Household's Journal Entries will be in
the single Household document". Queries: use the data return type "Head of Household Accounts" to "omit secondary
individuals". Household reports show "one line per household, with the Primary listed first"
([best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html);
[Family and Couple Accounts](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapfamcoup.html)).

#### 2.4 Giving and soft credits

**Verified.** Household giving is a two-step query (transactions in a date range, then households whose combined total
meets a threshold) — a reporting technique, not a stored rollup
([best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html)). Soft credits
are Journal entries (linked to a gift, or unlinked), enabled per organisation; "If you delete the original transaction,
the soft credit is also deleted"
([Soft Credits help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapsoftcredits.html)).
**Reported:** "the Spouse does not automatically get Soft Credited"
([Tribute report thread](https://community.blackbaud.com/discussion/comment/196914/), login-walled summary). No
automatic household soft credit was found.

#### 2.5 Duplicates and merge

**Verified.** Possible duplicates are found on Account Name, Email, Address, City, Postal Code, Phone; "You can only
merge accounts with the same role type"; "Journal items and relationships... will always be merged"; and household
membership blocks a merge: "If both accounts are in different household relationships, a warning message appears to
indicate that you cannot merge them" until one is removed from its household
([Merge Account](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapmergerole.html);
[Check for Possible Duplicates](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapcheckpossibleduplicates.html);
[Duplicate Report](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etappossibleduplicatesreport.html)).

#### 2.6 Memberships, life events, portals

**Not found / Reported.** No eTapestry membership module surfaced; organisations track membership "outside of eTapestry"
with defined fields and spreadsheets
([Starting to use membership?](https://community.blackbaud.com/discussion/comment/281815/), login-walled summary;
[UDF best practice](https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/UDF.html)). No documentation
of marriage/divorce/death household handling beyond "end dates" on relationships and dissolving via N/A. No household
functionality in eTapestry online forms was found.

---

### 3. Convio (historical) → Luminate

#### 3.1 Convio Common Ground (Force.com, 2008–2012): a true household account

**Verified.** Common Ground split constituents into _household contacts_, which "always belong to a special account
record called a household account", and _organization contacts_; "Household account records group contacts together who
abide in the same residence"; "Household records provide giving summary information for the entire household"
([Overview](https://secure2.convio.net/customer/commonground/v35/guides/Overview.htm)).

**Verified — the household account.** Fields: Account Name ("usually in the form of the head of household's given name,
surname, then the word household"); **Head of Household (required)** — "The primary household contact designated as the
head"; Secondary Contact (the second person in automatic salutations); Formal and Casual Salutation Formulas generating
system text, overridable only by choosing "Manually Set - do not use formula"; Addressee ("Mr. & Mrs. James Thompson");
Billing Address; **Parent Account** (to "track multigenerational giving summaries"); **Household Soft Credit** checkbox
— "other household members are automatically given soft credit on any member's gift"; rollups for Lifetime/YTD/Last Year
count and amount, First/Last/Largest, Active Sustainer, Open Major Gift, all defined as transactions "where someone in
this household was the primary donor"
([Household Account](https://secure2.convio.net/customer/commonground/v30/accounts/Household_Account.htm)).

**Verified — the household contact.** Leaving Account Name blank "creates a new household account for that contact";
"All household account records in Common Ground must have one contact belonging to that household designated as head";
the head can be set at either level and syncs to the other; per-contact giving summaries come in three flavours: hard
("primary donor"), soft ("soft credit transactions by members of the contact's household") and combined; Addressee "is
automatically populated when a contact is created... but can be edited"; address changes save "the current address being
saved in a historic related address record"
([Household Contact](https://secure2.convio.net/customer/commonground/v30/contacts/Household_Contact.htm);
[Manage Related Address Behavior](https://secure2.convio.net/customer/commonground/v30/guides/Manage_Related_Address_Behavior.htm)).

**Verified — rough edges.** "An error may occur when deleting contacts... if the contact is the head of household"; the
workaround is to "delete all of the non-heads of household before deleting the head of household contact or delete the
entire household"; a head "cannot be removed since a Head of Household is required if multiple contacts exist";
donations from organization contacts are attributed to the organisation unless you "promote the contacts to household
contacts" ([Known Issues](https://secure2.convio.net/customer/commonground/v35/Known_Issues_in_Common_Ground.htm)).
Duplicate guidance: search first, and "it's usually preferable to add a Contact to an existing Household... than to
create a new Household"; a "Show Household" button in gift batch narrows the search to the household's contacts; import
templates exist for Household Account, Head of Household Contact and Household Contact separately
([Data Hygiene best practices, Heller](https://teamheller.com/wp-content/uploads/2014/04/cgbestprac_datahygiene.pdf),
Reported — partner-hosted copy of a Convio guide).

#### 3.2 Luminate CRM (Salesforce) and Luminate Online (Constituent360)

**Verified.** Luminate CRM (2011, Salesforce-based) advertised "Householding. Link two or more constituents together as
a household to share household donation summaries" and "Soft Credits. Automatically create donation transactions for
household" members ([Luminate CRM datasheet](https://webfiles-sc1.blackbaud.com/files/luminatecrm_datasheet.pdf)). Its
salutation-formula setup lives under Salesforce "Customize > Accounts > Fields" with a Household checkbox
([Luminate CRM salutation formula help](https://webfiles.blackbaud.com/files/support/helpfiles/luminate-crm/Content/SysAdminTopics/ContactRecordFields_AutomaticSalutationFormulas_Create.html),
Reported — page 404'd on fetch; snippet from search). **Inferred:** Luminate CRM carried Common Ground's
household-account design forward (household = Account, people = Contacts, formula salutations). Blackbaud discontinued
Luminate CRM (announced end 30 June 2022,
[Cloud for Good](https://cloud4good.com/announcements/sunsetting-luminate-crm-what-nonprofits-should-expect/),
Reported).

**Verified / Not found.** Luminate Online's Constituent360 is a per-person database with configurable fields and
_groups_ (segments), not households; the Constituent API manages records and group membership
([ConsAPI](http://open.convio.com/api/apidoc/reference/apis/ConsAPI.html)); profile forms offer a "Significant Other"
field category
([Adding Constituent360 Data](http://help.convio.net/site/PageServer?pagename=Admin_CMS_Adding_Constituent360_Data)). No
household record was found in Luminate Online. The RE–LO integration treats spouse as constituent-level data and
requires review before "removing" a spouse link
([RE–LO example scenarios](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/relo/Content/ExampleScenario1.html)).

**Verified — TeamRaiser.** No "family team" concept. An event-level option "Allow multiple registrations on a single
credit-card" lets a registrant add _secondary registrants_ ("people registered by another participant, such as a family
member"); per-person discount codes must be entered for each
([Participant Registration report results](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/luminate-online/help/Subsystems/Reports/Content/Concepts/Admin_TeamRaiser_Reports_ParticipantRegistration_Results.html);
[Registration discounts](https://webfiles.blackbaud.com/support/howto/coveo/luminate-online/subsystems/teamraiser/concepts/Admin_TeamRaiser_Event_Configuration_08Discounts.html)).

---

### 4. Blackbaud Altru (built on Blackbaud CRM)

#### 4.1 Household is a first-class constituent, created automatically from a spouse relationship

**Verified.** "A household is a group of individuals at one address, and an individual can belong to only one household"
([BBCRM Add a Household](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMAddConstituentHouseholds.html));
"A household can have multiple constituents as members, but a constituent can be a member of only one household"
([Altru Constituents guide, ch. 2](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)). Creation is
automatic: when adding an individual relationship, "If you select This is the spouse relationship for [Constituent]...
the program automatically selects" _These individuals are members of the same household_; "If neither... are members of
households, the program automatically creates a record of the new constituent household when you save the relationship.
If only one of the two constituents is a member of a household, the program adds the other constituent as a member"
([Altru Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf);
[Constituent Household Record](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMHouseholdConstituentPage.html)).
"Each member of the household must be a constituent"; "The record of a household can stand on its own with no related
individuals"
([BBCRM Add a Household](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMAddConstituentHouseholds.html)).

**Verified — above the household.** _Constituent groups_ "include families made up of multiple households, giving
circles, and family foundations"; a household can itself be added to a group
([Altru Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)).

**Verified — members and primary contact.** The Add screen takes two members; more are added from the Members tab; each
member has a relationship to the primary contact, a start date, and a spouse/domestic-partner checkbox; a checkmark
marks the primary contact; you can "Set a Member... as the Primary Contact" and remove a member with or without keeping
history
([Household Members tab](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMAddHouseholdScreenHouseholdMembersTab.html);
[Group or Household Members](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMGroupPageMembersTab.html);
[Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)).

#### 4.2 Addresses copied, not shared

**Verified.** On creating the relationship a "Copy [Individual's] primary contact information" checkbox appears; when
adding an address, email or phone to a member, "the Copy address to household and members checkbox appears"; on adding a
member, "Copy household's primary contact information"
([Altru Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)). **Inferred:** copies
are independent after the fact; there is no live link like RE7's shared address.

#### 4.3 Household names, addressees and salutations

**Verified.** Administration holds "Default household name format options... Altru uses these defaults to create
addressees and salutations for the household"; a specific household can override them; on the Add screen you "Select
whether to use the standard household name format or a custom name"
([Household Settings](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/ADMManageHouseholdSettings.html);
[BBCRM Add a Household](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMAddConstituentHouseholds.html)).
Joint-format rules in mailings: "Altru lists the primary constituent first in addressees and salutations for joint
communications", with a selection-based override (treat the spouse who is in the mailing's selection as primary) and a
tie-breaker "when both or neither spouse appears"
([Add Name Format Options](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/COMMAddNameFormatOptionScreen.html)).
Altru auto-generates spouse name formats when a spouse relationship is entered
([Name Formats blog](https://community.blackbaud.com/blogs/14/1926), Reported summary).

#### 4.4 Giving: revenue stays with the person; households get _recognition credit_

**Verified.** "Revenue transactions cannot be linked directly to a household, and households cannot have the Donor
constituency"; "You can use recognition credits to recognize the amount of revenue your organization receives from the
household"
([Household Settings](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/ADMManageHouseholdSettings.html)).
Per household you set _Member revenue_ → credit the Household and/or Other members (all or specified, each with a credit
type and percentage) and _Household revenue_ → credit members
([Constituents guide, ch. 5](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf);
[BBCRM Define household recognition options](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/tkDefineHouseholdRecognitionOptions.html)).
Pairwise recognition is also set on the relationship itself ("Apply to [Individual] for revenue from [Related
individual]... select the type and percentage")
([Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)). BBCRM default: the household
receives credit for members' revenue; members do not receive credit for household revenue unless enabled
([HOUSEHOLDINFO table](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm-db/content/householdinfo.html)).

#### 4.5 Life events: dissolve, reinstate, deceasing options

**Verified.** "When a member leaves and the household does not remain intact, such as in the case of divorce, you can
dissolve a household", with a required date and reason code; the household becomes inactive and "you can assign its
members to other households"; previous members remain visible with start/end dates and comments; "You can reinstate a
dissolved household... all previous members... are automatically reinstated" unless now in another household, inactive
or deceased ([Constituents guide, ch. 5](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf)). Marking
someone deceased opens a configurable _Deceasing Options_ page whose Household/Groups tab lets you "set the deceased
constituent's spouse as the primary household contact", whose Names tab lets you "remove the deceased constituent's name
from the salutation", and whose Relationships tab lets you "change the marital status of the spouse"
([Constituents guide](https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf);
[BBCRM Deceasing Options](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMDeceasingOptionsPage.html)).
**Reported:** household name calculation excludes inactive constituents
([Atlas wiki on an Altru-family system](https://atlaswiki.partners.org/encyclopedia/personal-info-name-formats/)).

#### 4.6 Memberships: a primary member plus named "other members", governed by level limits

**Verified — configuration.** For every level you "specify the number of members, membership cards, and children
allowed"; a level can be set to downgrade automatically when the renewal amount matches a lower level; benefits (e.g.,
guest passes) and _add-ons_ attach to levels; the add-on type "additional member" lets you "add a second constituent to
the membership" but "does not change the number of cards allowed"; "Can purchase multiple" controls whether e.g. "one
additional guest member pass, but not more than one"
([Altru Memberships guide](https://help.blackbaud.com/docs/0/assets/guides/altru/members.pdf);
[Membership Add-ons](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMConfigAddOns.html)).

**Verified — members on a membership.** "On the Summary tab, in the Other members grid, include additional members for
the membership. To include an additional member, such as a spouse, select Add"; "You cannot change the number of members
allowed or add more members than the membership program and level allow"; "You cannot remove a member if the member is
marked as the primary member"; "Mark as primary" transfers primacy; **Remove** can "create a separate membership for the
other member and transfer active cards to it"
([Membership and Membership Card Tasks](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMSummaryTab.html);
search snippet of
[Edit Members](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/testhelp/content/memconstiteditmembers.html),
page now 404). In batch, additional members can be chosen from "constituent relationships of the primary member" or
searched
([Manage members and cards in batch](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/tkManageMembersCardsInBatch.html)).
**Reported:** children are often _not_ entered as constituents; one museum keeps "the names and birthdates of their
children in notes" partly "to prevent online sales from adding an endless amount of people onto a membership"
([Membership structure within Altru](https://community.blackbaud.com/discussion/comment/238858/), login-walled summary).

**Verified — cards.** "You are allowed to issue as many cards as the membership level allows"; a card has a member, a
name format, an expiration (defaults to the membership's) and a status Issued → Printed; cancel with a reason such as
"Lost" ([Memberships guide, p. 64](https://help.blackbaud.com/docs/0/assets/guides/altru/members.pdf);
[Summary tab help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMSummaryTab.html)).

**Verified — guest passes.** Issued automatically on join/renew; "Each person listed as a member on the membership
record is entitled to the guest pass benefit... two passes appear on the primary constituent's record as well as each
family members' records. There are two passes available for the family — not two per member"; on upgrade "new passes are
added to any remaining passes"; redeemed in Daily Sales against a card or driver's licence
([Guest Pass Benefits](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMGuestPasses.html)).

**Verified — lifecycle.** Mid-term upgrade only to "a higher level, such as from Individual to Family, or... a longer
term", only when paid in full, charging the price difference, retaining the later expiration; "You cannot upgrade a
membership from one membership program to another"; renew in window, rejoin after lapse/drop, cancel with reason;
**Transfer membership** moves "a membership and all related history from one constituent to another" ("The primary
member must be a constituent... If the constituent is already a member of the program... you cannot transfer")
([Memberships guide, pp. 63–64](https://help.blackbaud.com/docs/0/assets/guides/altru/members.pdf)). Renewal notices,
auto-renewal enrolment and the gift-of-membership "who gave the gift" display are documented in the same guide.

#### 4.7 Self-service

**Verified.** Web membership forms inherit default field settings ("select the fields to include and require")
([Membership Form Options](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/WEBMembershipOptions.html)).
**Reported:** a Blackbaud staff reply says Altru "does not have a mechanism of inquiring about relationships in online
forms such as memberships, pre-registered programs or special events"
([Web registration and households](https://community.blackbaud.com/discussion/51161/web-registration-and-households),
login-walled summary, possibly dated); partners sell portals and digital cards to fill the gap
([Social Good Software](https://socialgoodsoftware.com/solutions/memberships)).

---

### 5. Patterns that persisted across generations

1. **Spouse is a special relationship, everywhere.** RE7/NXT (spouse checkbox), eTapestry (Spouse relationship type with
   Primary/Member), Common Ground (Secondary Contact + marital status), Altru (spouse checkbox auto-triggers household).
   Every product treats the couple as the unit and everyone else (children, parents, guardians) as plain relationships —
   Altru is the only one that lets a third person be a household _member_ with the same standing.
2. **One designated "primary"/head of household per couple or household, chosen by humans, with a default that surprises
   people.** RE defaults to whichever record existed first; Common Ground made it a _required_ field and errored on
   deletion; eTapestry and Altru require exactly one Primary. In all four it exists to answer one question: who gets the
   one mailing piece.
3. **Joint addressee/salutation generated from the pair, with the primary named first** (RE7 "Smart" formulas; eTapestry
   Joint persona; Common Ground formal/casual formulas; Altru household name formats with selection-based override). All
   of them need an escape hatch for manual text.
4. **Giving is always recorded on a person; the household sees it through a derived credit.** RE: soft credit (opt-in
   per relationship; opinions differ on whether to grant it); Common Ground: a household-level "Household Soft Credit"
   switch plus hard/soft/combined rollups; eTapestry: soft credit journal entries, no automatic household credit;
   Altru/BBCRM: _recognition credit_ with type and percentage, configurable per household and per relationship. Nobody
   lets a household be the legal donor (Altru says so explicitly).
5. **Family membership = one primary member who owns the record, plus named "other"/"joint" members and a per-level cap
   on members, cards and children.** RE7 (Membership Category max members; Members and Cards; joint vs card-only
   recipients; gifts link only to the primary), Altru (level limits; Other members grid; additional-member add-on; cards
   per level; shared guest-pass pool; Remove = spin off a new membership; Transfer).
6. **Payer ≠ beneficiary is modelled explicitly** ("gift of membership" in both RE7 and Altru, with a choice of who
   receives renewal notices and benefits).
7. **Address sharing is either a live link (RE7) or a copy (Altru, eTapestry personas).** The live link is the one users
   complain about on divorce.
8. **Life events are checklists, not transactions.** Only Altru/BBCRM has first-class operations (dissolve with reason,
   reinstate, deceasing options with household/name follow-ups). RE and eTapestry rely on end dates and procedure
   manuals.
9. **Portals lag the back office.** Across all four, online forms capture a primary person and at most a count or
   free-text list of additional members; relationship data is reviewed and applied by staff.

---

### 6. Weaknesses worth avoiding

- **A flag instead of a record.** RE's head-of-household flag lives on the relationship, is unset unless someone sets
  it, and only works when both spouses are constituents and both are in the mailing. Result: silent omissions from
  mailings and duplicate rows in gift-officer lists ([RENXT-I-375](https://renxt.ideas.aha.io/ideas/RENXT-I-375);
  [RE7 Mail guide](https://help.blackbaud.com/docs/0/assets/guides/re/mail.pdf)). A household should be a record with an
  explicit, always-present primary.
- **Pricing by record count distorting the model.** eTapestry and RE customers merge spouses into one record to save
  licences, then cannot email each person ([RENXT-I-2806](https://renxt.ideas.aha.io/ideas/RENXT-I-2806);
  [Household vs Individual Accounts](https://community.blackbaud.com/discussion/comment/202224)). Don't let commercial
  limits decide whether a human gets a record.
- **Non-constituent shadows.** RE joint members and relationships that "exist only on the membership" cannot be queried,
  mailed or de-duplicated from the constituent side
  ([Membership Records help](https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-membership-records.html);
  [Problem Pulling Members](https://community.blackbaud.com/discussion/comment/210354/)). Every covered person should be
  a real contact, even a lightweight one.
- **Derived credit that is opt-in, non-retroactive and policy-laden.** Spousal soft credit in RE must be switched on per
  relationship, applies only going forward, and needs a global change to backfill
  ([Soft Crediting Spouse](https://community.blackbaud.com/discussion/comment/221087/)). Prefer a household rollup
  computed from membership-at-time-of-gift, with hard credit untouched and recognition rules declared once.
- **Live shared addresses with no household owner.** RE7's address link propagates a divorcee's move to the ex-spouse
  ([Spouse addresses in RE](https://community.blackbaud.com/discussion/62696/spouse-addresses-in-re)). Make the
  household own the shared address and let a member leave it in one step.
- **Lossy exits.** eTapestry refuses to merge two accounts in different households and makes you dissolve first; RE
  recommends end dates but users delete relationships, losing history; Common Ground errored when deleting a head.
  Altru's dissolve/reinstate with reason, date and "previous members" is the model to copy.
- **A single structural level.** RE has none, eTapestry one (and an account can head several households, which is a
  modelling smell), Common Ground one plus a "Parent Account" hack, Altru two (household, then group). Multi-household
  families, guardians and split custody need an explicit many-to-many with roles and dates — none of these products has
  it.
- **Membership caps expressed as counts of "adults" and "children" with children in notes.** Altru museums keep children
  out of the database to stop online forms adding "an endless amount of people"
  ([Membership structure within Altru](https://community.blackbaud.com/discussion/comment/238858/)). Eligibility rules
  (age, relationship, residence) should be data, not a headcount.
- **Portal parity gaps lasting years.** NXT took 4 years to expose head of household, 5 to expose auto soft credit, 9 to
  expose memberships in the web view. Design the household API once and let staff and self-service surfaces share it.

---

### 7. Sources

Raiser's Edge NXT / RE7 (Verified)

- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-relationships.html — spouse relationships,
  head of household rule, soft credit opt-in, end dates on divorce.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/en-au/content/bb-constituents-add-relationship.html
  — add-relationship form: spouse, dates, head-of-household choice, duplicates prompt.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-constituent-record.html — spouse in header,
  giving summary incl. soft credits, membership section, name formats, Mark as deceased.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-whats-recent-date-list-2024.html — Nov 2024
  "Automatically soft credit this individual for gifts"; Mar 2024 membership in web view; Sep 2024 merge enhancements.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-duplicate-records.html — duplicate detection
  criteria and automatic matching rules.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/data-identify-duplicates.html — merge:
  source/target, exclude soft credits, database-view-only fields.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/bb-membership-records.html — Members and card
  information tile, non-constituent card members, gift of membership, transactions.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/rex/content/memform-get-started-tutorial.html — NXT
  membership forms: additional members allowed, gift of membership.
- https://help.blackbaud.com/docs/0/assets/guides/re/mem_data.pdf — RE7 Membership Data Entry Guide: categories, Members
  and Cards, joint members, gift of membership, renewal recipient, lifecycle, lifetime.
- https://help.blackbaud.com/docs/0/assets/guides/re/mail.pdf — RE7 Mail Guide: Head of Household Processing options and
  limits.
- https://help.blackbaud.com/docs/0/assets/guides/re/config.pdf — RE7 Configuration Guide: shared address/phone link
  rules, soft credit defaults, Smart addressee formulas, spouse last-name rule, deceased-spouse rules.
- https://kb.blackbaud.com/articles/Knowledge/48275 — KB: Head of Household processing; default HOH is the constituent
  the spouse was added from (snippet; page is script-rendered).
- https://kb.blackbaud.com/articles/Article/38403 — KB: show head of household in search results (field path).
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/relo/Content/ExampleScenario1.html — RE–Luminate Online
  sync of spouse/marital-status changes.

Raiser's Edge NXT (Reported)

- https://renxt.ideas.aha.io/ideas/RENXT-I-375 — Indicate HOH in NXT (shipped June 2020); mailing drop-outs; duplicate
  household rows.
- https://renxt.ideas.aha.io/ideas/RENXT-I-1911 — HOH filter in lists (July 2020).
- https://renxt.ideas.aha.io/ideas/RENXT-I-2806 — Email spouse in NXT (merged into RENXT-I-3340, open).
- https://renxt.ideas.aha.io/ideas/RENXT-I-3003 — Add spouse in NXT; non-constituent spouse; record-count concern.
- https://renxt.ideas.aha.io/ideas/RENXT-I-2064 — Auto soft-credit spouse (merged into RENXT-I-6418, shipped).
- https://renxt.ideas.aha.io/ideas/RENXT-I-3178 — Default addressee/salutation incl. household formats (merged into
  RENXT-I-3828, planned).
- https://renxt.ideas.aha.io/ideas/RENXT-I-534 — Share an action/proposal across spouse records (open since 2016).
- https://diojeffcity.org/wp-content/uploads/2024/10/0-RE-NXT-Procedures-PRINT-THIS-ONE.pdf — customer procedure manual:
  HOH policy, shared address, children as relationships, divorce and death checklists.
- https://community.blackbaud.com/blogs/10/2167 — HOH default and query technique (login-walled; search summary).
- https://community.blackbaud.com/blogs/13/6889 — HOH in web view release post (login-walled; search summary).
- https://community.blackbaud.com/blogs/13/3205 — Feb 2021 shared addresses in web view (login-walled; search summary).
- https://community.blackbaud.com/blogs/13/5069 — recognition configuration incl. soft credits (login-walled; search
  summary).
- https://community.blackbaud.com/blogs/13/9406 — membership capabilities in web view, 2024 (login-walled; search
  summary).
- https://community.blackbaud.com/discussion/comment/229415/ — "HOH filter doesn't work unless everyone has a spouse"
  (login-walled; title + summary).
- https://community.blackbaud.com/discussion/comment/221087/ — spousal soft credit not retroactive (login-walled;
  summary).
- https://community.blackbaud.com/discussion/62193/do-you-soft-credit-spouses — policy variation (login-walled;
  summary).
- https://community.blackbaud.com/discussion/62696/spouse-addresses-in-re — shared address problem on divorce
  (login-walled; summary).
- https://community.blackbaud.com/discussion/55584/dissolve-households-when-one-member-dies — deceased-spouse practices
  (login-walled; summary).
- https://community.blackbaud.com/discussion/comment/264082/ — merge limits incl. memberships (login-walled; summary).
- https://community.blackbaud.com/discussion/comment/210354/ — joint members not constituents (login-walled; summary).
- https://help.avidai.com/hc/en-us/articles/48180816343059-Enable-spouseholding-for-Raiser-s-Edge-NXT — integrator claim
  of no native householding (403 on fetch; summary).
- https://www.arkusinc.com/archive/2020/constituent-wars-part-i-how-raiser2019s-edge-relationships-translate-to-salesforce
  — migrating RE spouse links into NPSP households; HOH chooses primary contact.

eTapestry (Verified)

- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etaphouseholds.html — household
  relationship, Primary/Member, one-household rule, dissolve via N/A.
- https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/households.html — Couples and Households best
  practice: one vs two accounts, Joint persona, mailings grouped by household, household giving query.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapfamcoup.html — Family and
  Couple Accounts: recommendation for individual accounts, Family Name Format, "Head of Household Accounts" return type.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etappersonas.html — persona
  types (Personal, Business, Joint, Seasonal), primary persona.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapsoftcredits.html — soft
  credits as journal entries; deletion cascade.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapmergerole.html — merge
  rules; household conflict blocks merge.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etapcheckpossibleduplicates.html
  — duplicate fields and exceptions.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etappossibleduplicatesreport.html
  — duplicate report limits.
- https://webfiles-sc1.blackbaud.com/files/support/guides/etap/bestpract/UDF.html — user-defined fields guidance
  (membership tracking workaround).

eTapestry (Reported)

- https://community.blackbaud.com/discussion/comment/202224 — household vs individual accounts; account-limit cost
  (login-walled; summary).
- https://community.blackbaud.com/discussion/comment/196914/ — spouse not automatically soft-credited (login-walled;
  summary).
- https://community.blackbaud.com/discussion/comment/281815/ — membership tracked outside eTapestry (login-walled;
  summary).

Convio / Luminate

- https://secure2.convio.net/customer/commonground/v35/guides/Overview.htm — Common Ground constituent model (Verified).
- https://secure2.convio.net/customer/commonground/v30/accounts/Household_Account.htm — household account fields,
  required head, salutation formulas, Household Soft Credit, Parent Account, rollups (Verified).
- https://secure2.convio.net/customer/commonground/v30/contacts/Household_Contact.htm — household contact fields;
  hard/soft/combined giving summaries; address history (Verified).
- https://secure2.convio.net/customer/commonground/v30/guides/Manage_Related_Address_Behavior.htm — historical address
  records (Verified).
- https://secure2.convio.net/customer/commonground/v35/Known_Issues_in_Common_Ground.htm — head-of-household deletion
  errors; promote org contacts (Verified).
- https://teamheller.com/wp-content/uploads/2014/04/cgbestprac_datahygiene.pdf — Convio best-practice guide on
  duplicates and households, partner-hosted (Reported).
- https://webfiles-sc1.blackbaud.com/files/luminatecrm_datasheet.pdf — Luminate CRM householding and household soft
  credits (Verified).
- https://webfiles.blackbaud.com/files/support/helpfiles/luminate-crm/Content/SysAdminTopics/ContactRecordFields_AutomaticSalutationFormulas_Create.html
  — Luminate CRM salutation formulas on Accounts with Household checkbox (Reported; 404 on fetch, search snippet).
- https://cloud4good.com/announcements/sunsetting-luminate-crm-what-nonprofits-should-expect/ — Luminate CRM
  discontinuation (Reported).
- http://open.convio.com/api/apidoc/reference/apis/ConsAPI.html — Luminate Online Constituent360 API, groups (Verified).
- http://help.convio.net/site/PageServer?pagename=Admin_CMS_Adding_Constituent360_Data — "Significant Other" profile
  fields (Verified).
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/luminate-online/help/Subsystems/Reports/Content/Concepts/Admin_TeamRaiser_Reports_ParticipantRegistration_Results.html
  — TeamRaiser secondary registrants under "Allow multiple registrations on a single credit-card" (Verified).
- https://webfiles.blackbaud.com/support/howto/coveo/luminate-online/subsystems/teamraiser/concepts/Admin_TeamRaiser_Event_Configuration_08Discounts.html
  — per-person discount codes for multi-registrant transactions (Verified).

Altru / Blackbaud CRM (Verified)

- https://help.blackbaud.com/docs/0/assets/guides/altru/constit.pdf — Altru Constituents Guide: household relationships
  (one household rule, auto-creation), copy contact info, recognition options, dissolve/reinstate, previous members,
  deceasing options, groups.
- https://help.blackbaud.com/docs/0/assets/guides/altru/members.pdf — Altru Memberships Guide: level limits (members,
  cards, children), add-ons, guest passes, Other members, cards, mid-term upgrade, transfer, renew/rejoin/cancel,
  auto-renewal, renewal notices.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMHouseholdConstituentPage.html —
  household record auto-created from spouse/shared-household relationship.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/ADMManageHouseholdSettings.html —
  default household name formats; no revenue on households; recognition credits.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMAddHouseholdScreenHouseholdMembersTab.html
  — primary contact, member 2, spouse checkbox, copy contact info.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/CRMGroupPageMembersTab.html — members
  grid, primary checkmark, previous members of dissolved household.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/COMMAddNameFormatOptionScreen.html —
  joint addressee/salutation rules, primary-first, selection-based override.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMSummaryTab.html — Other members grid,
  Mark as primary, Remove (spin-off membership), cards, Transfer.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/testhelp/content/memconstiteditmembers.html — Edit
  members limits (search snippet; page now 404).
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMConfigAddOns.html — additional-member
  add-on type; cards unchanged.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/MEMGuestPasses.html — guest pass pool
  shared by family members; upgrade stacking.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/tkManageMembersCardsInBatch.html —
  additional members from primary's relationships in batch.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/altru/help/content/WEBMembershipOptions.html — web
  membership form field options.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMAddConstituentHouseholds.html — BBCRM
  household definition, one-household rule, members must be constituents, standard vs custom name.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/CRMDeceasingOptionsPage.html — BBCRM
  deceasing options: spouse as primary, remove name from salutation.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm/us/40/Content/tkDefineHouseholdRecognitionOptions.html
  — BBCRM household recognition options.
- https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm-db/content/householdinfo.html — BBCRM default
  recognition flags (search snippet).

Altru (Reported)

- https://community.blackbaud.com/discussion/comment/238858/ — membership structure; children in notes to stop online
  over-adding (login-walled; summary).
- https://community.blackbaud.com/discussion/comment/236888 — member limits set per level (login-walled; summary).
- https://community.blackbaud.com/discussion/51161/web-registration-and-households — no relationship capture in online
  forms (login-walled; summary).
- https://community.blackbaud.com/blogs/14/1926 — Altru auto-generates spouse name formats (login-walled; summary).
- https://atlaswiki.partners.org/encyclopedia/personal-info-name-formats/ — inactive constituents excluded from
  household names (customer wiki).
- https://socialgoodsoftware.com/solutions/memberships — partner portals/digital cards for Altru.

Not found despite searching: a vendor page on RE web-view merge handling of spouse relationships; Luminate Online
household record (none appears to exist); eTapestry membership module (none appears to exist); Luminate CRM household
help pages (site largely offline since discontinuation).

## 29. W2 — How NetForum and iMIS model households, family relationships and family memberships

## W2 — How NetForum and iMIS model households, family relationships and family memberships

Research input for the Rhythm 2.0 "Households" architecture proposal. Written 2026-10-21 from public sources only.

### How to read this report

Every claim carries one of three labels:

- **Verified** — stated in vendor documentation or vendor release notes (NetForum Enterprise/Pro help, iMIS help / ASI
  documentation, Wild Apricot help, Fonteva docs, MemberClicks help).
- **Reported** — stated by customers, implementers or review sites (forums, G2/Capterra, integrator blogs,
  customer-written guides).
- **Inferred** — my reasoning from the verified and reported material. Never a statement about how the vendor's database
  is built.

"Not found" means I searched for the behaviour and found no public source either way; it does not mean the product lacks
it.

Source-access notes that affect confidence:

- `documentation.abila.com` (older NetForum Enterprise and all NetForum Pro help) has an expired TLS certificate; it
  could not be fetched directly. The same Enterprise pages are mirrored at `documentation.mip.com` and were fetched
  there. NetForum Pro pages were only readable through search-engine excerpts; they are marked "Verified (via excerpt)".
- The current NetForum Help Center (`nfesupport.zendesk.com`) refuses automated fetches (HTTP 403). Only search excerpts
  of it were used.
- `help.imis.com/enterprise/...` paths returned 404 to the fetcher; the same articles are served at
  `documentation.advsol.com/imis/docs/...` and were fetched there. Where only a `help.imis.com` URL exists, the claim
  comes from a search excerpt.
- `docs.fonteva.com` is a Salesforce Experience site that returns only a login/CSS-error shell to a non-browser client;
  Fonteva claims come from search excerpts of those pages plus a customer (AUSA) user guide. Fonteva's older Confluence
  pages require login.
- `help.memberclicks.com` returns HTTP 403 to the fetcher; MemberClicks claims come from search excerpts.
- `gethelp.wildapricot.com` articles are rendered client-side; the fetcher got only titles. Wild Apricot claims come
  from search excerpts of those articles and from the Wild Apricot user forum.

---

### 1. NetForum (Community Brands / Momentive Software)

NetForum Enterprise (the large-association product, formerly Avectra, then Abila, then Community Brands) has a
**first-class Household customer** since release 2015.1. NetForum Pro (the smaller product) does not document a
household record; it models shared benefits through "benefits flow down" over individual-to-organization and
individual-to-individual links.

#### 1.1 Constituent ("customer") model

- **Verified.** CRM "group items" are Individuals, Organizations, **Households**, Assignments and Requests. The CRM
  overview page lists Households as a peer of Individuals and Organizations.
  (`documentation.mip.com/.../2017.1/Content/CRM/CRM.htm`)
- **Verified.** "Households give you the ability to create a natural grouping of individuals." The stated purpose is
  mailing efficiency: where several donors live at one address it is "more efficient to send one piece of mail to the
  entire household." The section's subtopics are Adding a Household, Adding a Household Relationship, Enabling
  Membership Flowdown for Households, List Household, Find Household, Household Profile.
  (`.../2017.1/Content/CRM/Households/Households.htm`)
- **Verified.** Households arrived in 2015.1. The release notes list: "Relate an organization to a household" (CRM);
  "Configure membership flow-down for a household similar to membership flow-down for an organization" and "if a
  household customer gets a family membership then the household members gain an inherited membership" (Membership);
  "Track combined giving totals for a household by automatically giving soft credit to a constituent on gifts from
  household members" (Fundraising). (`.../2015.1/Content/GettingStarted/WhatsNew2015.1.htm`)
- **Verified.** A household is created with: Household name (required), email, phone (typed), fax (typed), website URL,
  Tax ID, A-score scale, and at least one household relationship. "The Household Profile appears" on save.
  (`.../2017.1/Content/CRM/Households/Adding a Household.htm`)
- **Verified.** Every portal (eWeb) user is a customer row: "all eWeb Users are Customers in NetForum, either an
  Individual or an Organization. Generally, most instances of netFORUM provide only for Individuals to login." A
  household therefore cannot itself log in. (`.../2017.1/Content/eWeb/EWeb_LoginTech_ Info.html`, via excerpt;
  `.../2015.1/Content/Admin/iWeb_User.htm`)

**Answers to the brief's questions**

- _Households as distinct records, groupings, or organizations?_ Distinct customer record (Verified). It has its own
  Accounting tab (invoices, payments, credits, refunds, write-offs, orders), Products, Subscriptions, Scoring,
  Correspondence and Committees tabs, i.e. it is a full transacting customer, not a label.
  (`.../2015.1/Content/CRM/Households/Household Profile/Household Profile.htm`)
- _Contacts in multiple households?_ Not found. The relationship pages never state a one-household limit, and the GDPR
  anonymisation message distinguishes "non-household relationship to another customer" from household ones, but nothing
  says whether an individual may hold relationships to two households. (Inferred: the data model is a relationship row
  per individual-household pair, so nothing visible prevents it; treat as unknown.)

#### 1.2 Family relationships (spouse, parent, child, dependants)

- **Verified.** Relationship types are configured under CRM > Overview > Customer Setup > Relationship tab. A type has a
  Category (examples "Individual, Organization, Corporate Role, Chapter Role, Sponsor Role, Award Entry, etc.") and a
  combination ("Individual / Organization, Individual / Individual, Individual / Chapter, Organization / Organization",
  plus Sponsor / Contact, Exhibitor / Contact). "These are hard coded and can not be changed." Individual/Individual
  relationships explicitly include personal and family ties: "spouse, parent, child".
  (`.../2017.1/Content/CRM/Overview/Relationship Setup.htm`;
  `.../2017.1/Content/CRM/Overview/Customer Setup/Setting Up Relationship Types.htm`)
- **Verified.** Per-type flags: optional **limit** (only for Individual/Organization and Organization/Organization:
  "restrict the number of times that relationship can be used"); **membership flow down?** ("if all participants in this
  relationship are eligible for membership flow down"); **local relationship** (union / collective dues); **show on
  line** ("if the relationship should appear in eWeb"); **area access flag** and **area access edit flag** (view / edit
  the related area on eWeb, e.g. Organization Info). (same two pages)
- **Verified.** On the Individual Profile the Relations tab holds child forms: Organizations (with a Primary column),
  Related Individuals ("Relationships listed here can include child/parent or spouse relationships, for example"),
  Chapter relationships, Chapter officers, **Household relationships**, and **Related individuals in same household**.
  (`.../2017.1/Content/CRM/Individuals/Individual Profile/Child Forms/Relations.htm`)
- **Verified.** Individual–organization links carry a start date and an end date; "if the end date is equal to or less
  than today's date, then the relationship is no longer valid and NetForum no longer displays the record in the child
  form." Dates may be null. A relationship type (e.g. staff, employee) is stored on the link. One Primary organization
  per individual; "Only one individual can be the Primary Contact" per organization. The page names the storage table
  (`co_individual_x_organization`). (`.../2017.1/Content/CRM/Individuals/Individual_Organization_Relationship.htm`)
- **Verified.** Household relationships: "one household member must be designated as the head of household. Only one
  head of household can be active at a time." "One member must be set as the head of household or you will not be
  allowed to save the household." Each member line has a relationship type (drop-down), a start date and an end date.
  Members are added at creation or later from the Household Profile (Manage Relationships / Relations tab > Add); a
  member line is removed with its Delete icon. (`.../2017.1/Content/CRM/Households/Adding a Household Relationship.htm`)
- **Verified.** Household roles are a separate list from general relationship types: a 2015.1 fix reads "When adding
  household relationship information, the Relationship Type drop-down list no longer contains names of house hold roles,
  such as Member or Head of Household." (2015.1 What's New, fixed issue 16468)
- **Reciprocal relationships — not found for individuals.** The only documented "reciprocal" checkbox is on
  chapter-to-chapter relationships (`.../2017.1/Content/Membership/Chapters/Chapters.htm`, via excerpt). Inferred: a
  spouse link is one row typed "spouse" shown on both profiles; there is no documented asymmetric pair (parent/child)
  mechanism beyond choosing different types.
- _Dependants and ageing out._ Not found. No page mentions date of birth driving household membership or benefit
  eligibility.

#### 1.3 Membership model

- **Verified.** "Members in NetForum can be Individuals or Organizations" (and, since 2015.1, households). Customer Type
  is derived automatically. A membership dues package per member type is sold as a product. Multiple memberships are
  governed by the association-level option `AllowMultipleMembershipsPerAssociation`; without it "a customer may have at
  most one active Member record within a single Association." A separate Renewal Package can change the membership at
  renewal. (`.../2017.1/Content/Membership/Members/Members.htm`)
- **Verified — flow down (benefit inheritance).** "This is also known as membership benefit flow down." It lets a
  related party be "eligible for member discounts without having to purchase an Individual membership." It must be
  enabled when the Member Type is created; the system options that auto-populate inherited membership "add overhead to
  the system, so it should only be turned on if needed." (same page)
- **Verified — household membership.** From the Household Profile, Add Membership opens the Membership Wizard with "the
  new member field … pre-populated with the name of the household and cannot be edited." Memberships appear on the
  household memberships child form (columns: Association, Mbr Type, Mbr Status, **Mbr Through** — "how the individual,
  organization, or household holds this membership", i.e. purchased or flow-down — Join, Rejoin, Effective, Expire,
  Terminate). The same exists for chapter memberships.
  (`.../2015.1/Content/CRM/Households/Adding a Membership from the Household Profile.htm`;
  `.../Adding a Chapter Membership from the Household Profile.htm`;
  `.../Household Profile/Household Memberships Child Form.htm`)
- **Verified — who is covered.** Benefits reach a household member only when **both** the member type (checkboxes
  "receives member benefits" and "benefit flow down" on Edit – Member Type) **and** the individual's household
  relationship type (checkbox "membership flow down?") are enabled. Each covered Individual Profile then shows a check
  mark in "receives member benefits" and the membership in an **inherited memberships** child form.
  (`.../2015.1/Content/CRM/Households/Enabling Membership Flowdown for Households.htm`)
- **Verified — who pays.** On the membership record, "The Bill To Information section specifies the member currently
  purchasing the membership. If you want to change who you are billing (versus who is obtaining the membership), this
  can be modified here." The household has its own Accounting tab, so a household can be the invoiced party.
  (Members.htm; Household Profile)
- **Verified (via excerpt) — NetForum Pro flow down.** Pro has no household record; "Allow Benefits Flow Down" on the
  Membership Type lets benefits "flow down" to linked organizations or individuals. Conditions: the parent's member type
  allows flow down; the parent's primary membership is not terminated; the Member Flag is set by an active purchase on
  the primary owner; linked individuals are tied to the parent directly or through a linked customer record; a "does not
  receive benefits" flag at any level blocks it; "flow down in netFORUM Pro is only available up to 10 degrees of
  separation from the owner." A Membership Flow child form appears on both Individual and Organization profiles. Caveat:
  "If an individual or organization have their own purchased and active membership, it will NOT be indicated on their
  customer record that they also receive flow down until then."
  (`documentation.abila.com/netforumpro/html5_help/.../Benefits_Flow_Down_Overview.htm`;
  `.../2019_1_html5_new/.../Allowing_Membership_Benefits_Flow_Down.htm`)
- _Named vs unnamed coverage._ Inferred: coverage is strictly named — benefits exist only for individuals who hold a
  flow-down-enabled relationship row. No guest or "+1 unnamed" construct was found.
- _Eligibility, entitlements, renewals._ Verified: entitlement is the member flag plus member pricing; renewal is a
  Renewal Package assigned at Expire Date, and the Membership Renewal Wizard. Nothing household-specific about renewal
  was found.
- _Proration on changes._ Not found for households.

#### 1.4 Addresses, communication preferences, naming and salutations

- **Verified — organization-to-individual address linking.** On an individual, the Business Address can take an
  **Address Linking Method** of Linking, Defaulting or None. "If the address linking method is set to Linking, the
  address information is populated, and the fields are read-only" and "automatically updates if there are any changes";
  Defaulting copies once and stays editable. Phone and fax link the same way. If an organization's address is put on
  hold, individuals using it as primary "will also have the Organization's linked address put on hold."
  (`.../2017.1/Content/CRM/Individuals/Adding_an_Individual.htm`)
- **Verified — household address.** The Household Profile's address "displays the address of the individual who is
  designated as the head of household." The Contact Info tab holds household address, telephone and fax; actions include
  "Update the household address." (Household Profile page)
- **Verified (release note, via excerpt).** A 2017 SP1 fixed issue is titled "Changing 'Head of Household' removes
  Household Primary Address – New Address not Linked", which confirms the household address is a _linked_ copy of the
  head's address. (search excerpt of `documentation.abila.com` 2017 release pages)
- **Household naming / salutation.** The only name field is the free-text Household name. Individual name parts are
  Prefix, Suffix, Designation (Ph.D.), first/last. No addressee, joint salutation, or formal/informal greeting field for
  households was found on any fetched page. Not found.
- **Communication preferences.** Not found at household level.

#### 1.5 Search, merge and dedupe

- **Verified.** List Household and Find Household exist. (Households.htm)
- **Verified — Combine/Merge Dups.** "The Combine/Merge Duplicate records feature allows you identify and merge
  duplicate individual or organization information into one master record." It does **not** detect duplicates — it "only
  merges records that you have already determined to be a duplicate"; detection is the Individual Duplicate Check Report
  with a run-time matching parameter. You pick the Keep record and tick data categories (all ticked by default); "The
  data selected is not checked for duplicates" (so addresses can double up). Afterwards "some of the records from the
  duplicate customer are deleted outright, some are hidden"; "the deleted records are unrecoverable"; there is no
  unmerge. (`.../2017.1/Content/CRM/Individuals/Combine_and_Merge_Duplicate_Records.htm`)
- **Verified (release notes).** 2017.1.6: merging fails when both constituents have fund-level Giving Summary Statistics
  (issue 23777); merge did not update change dates (23529).
  (`.../2017.1/content/2017 Build Release Pages/NF 2017.1.6 and Hotfixes.htm`)
- _Household merge._ Not found: the merge tool is documented for individuals and organizations only.
- **Verified — eWeb duplicates.** "It is technically possible for more than one individual to have the same email
  address … NetForum will find the first customer record that matches the same username / password combination." (EWeb
  Login technical page, via excerpt)

#### 1.6 Giving at household level

- **Verified.** Fundraising: "Soft credit a member of a household so you can have combined family/couple giving totals
  on the household." (`netforumams.com/blog/how-savvy-associations-use-technology-grow-non-dues-revenue/`; same
  capability in the 2015.1 What's New.) The Household Profile has "Add a gift" and "Add a household credit" actions.
  Inferred: the household is both a possible hard-credit donor and the roll-up point for its members' soft credits.

#### 1.7 Life events (marriage, divorce, move, ageing out)

- Verified mechanics only: a household relationship row has start/end dates and can be deleted; head of household can be
  changed (with the address-link defect above); the household address follows the head. No workflow for divorce/split,
  remarriage, or dependants ageing out was found.

#### 1.8 Portal (eWeb) for families

- **Verified.** Relationship types carry "show on line" and per-area eWeb view/edit access flags; the documented case is
  Organization Info (a related individual seeing/editing their organisation). (Setting Up Relationship Types)
- **Reported.** Integrators describe eWeb organisation rosters: "The primary organizational contact has permission to
  modify organizational information, approve individuals for organizational association, and manage staff rosters."
  (`associationengine.com/netforum-integration/`) A NASPAA member guide shows a principal representative switching to
  the institution's account to pay its invoices. (`naspaa.org/netforum-member-guide`)
- **Reported (marketing).** "Create parent/child account relationships, giving parent accounts the ability to manage
  contact information and membership status for their child accounts." (`netforumams.com/features/`)
- _Household self-service in eWeb._ Not found. Nothing documents a head of household managing household members online.

#### 1.9 Reported limitations

- **Reported.** G2: "difficult to avoid duplicating records and linking organizations to an individual record," making
  it hard to count members per organisation. Capterra: "Very complicated and poorly supported"; "Bugs are not always
  addressed in a timely manner." (`g2.com/products/netforum-ams/reviews`; `capterra.com/p/184340/NetForum/`)
- **Reported.** One association replaced eWeb with a separately built member centre because eWeb did not meet its needs.
  (`fatlabwebsupport.com/blog/nonprofit/netforum-wordpress-integration/`)
- **Verified.** Flow-down automation "adds overhead to the system"; merges are irreversible; merge tool creates
  duplicate child rows.
- **Inferred.** The household address being a linked copy of the head's address means a head change or a head moving out
  silently changes the household's mailing address (see the 2017 SP1 defect).

---

### 2. iMIS (Advanced Solutions International)

iMIS has **no household record type** in its public documentation (EMS, 2017, 20, Classic desktop). Searches of
`help.imis.com` and `documentation.advsol.com` for "household" return only the fundraising soft-credit relationship
type. Families are handled with the generic contact-relationship facility plus organisation-style constructs (customer
types, primary organisation, list billing, company administrators). This is the most important finding about iMIS for
the proposal.

#### 2.1 Contact model

- **Verified (developer docs).** A Party is "A Person or an Organization who plays a role in a business transaction."
  Person: "An individual contact." Organization: "A group of Persons in the broadest sense." ComboParty exists "to
  represent an iMIS record that holds both a contact and an institute." "A Person is associated with their primary
  Organization through the Person's PrimaryOrganizationInformation." FullAddress carries a CommunicationPreference, "a
  collection of reasons to use the address." No household type. (`developer.imis.com/docs/party-examples`)
- **Verified.** Customer types are the backbone: "Is an organization" lets iMIS "maintain relationships between
  organizations and individuals" for rosters; "Is a member" separates member from non-member types; system types M and
  NM must not be deleted; changing either flag on a populated type changes company/membership status for all its
  contacts. Dues billing per type includes a Primary fee that "controls the advancing of the overall membership Paid
  Thru date." (`documentation.advsol.com/imis/docs/contact-settings-customer-types`)

#### 2.2 Relationships between contacts

- **Verified.** Settings > Contacts > Relationship types: **code** (auto-capitalised, immutable), **description**,
  **Reciprocal type** (optional — "If left blank, two contacts connected with that relationship type will be displayed
  with that same type"), **Use description**, **Use dates** ("display beginning and ending date fields in the
  relationship editor"). The shipped `_ORG-ADMIN` type backs the Company Administrator role and "should not be deleted";
  with dates, after the end date "the role becomes inactive." Delete a type only after checking no relationships use it.
  (`documentation.advsol.com/imis/docs/contact-settings-relationship-types`)
- **Verified.** "Only Staff members and System Administrators can create and edit relationships"; anyone can view.
  "There is no limit to the number or the types of relationships you can create." Created from the About tab >
  Relationships > Add relationship, choosing the other party and a **Role** for each side; the row appears on both
  records (spouse example: Alex ↔ Carol Morgan). Deleting "is removed from both contacts and the deletion is recorded in
  their change logs." Relationships can be shown on RiSE pages with the Group List Editor (list "Both", type
  "Relationship", optional Allow editing for staff). (`documentation.advsol.com/imis/docs/relationships`)
- **Verified.** Contacts overview: "Create unique reciprocal relationships between individuals or organizations, such as
  Child of or Guest of." (`help.imis.com/enterprise/features/community/contacts/contacts.htm`, via excerpt)
- **Verified (Classic desktop / iMIS 20).** Relationship Detail window fields: Id (or Contact Name "when the ID is not
  in the contact database"), Relationship, Reciprocal (display-only, auto-filled), Status (Active / Marked for deletion
  / Suspended), Group Code (ROLE or DONOR), Note, Date Added. The Set up relationship types window adds Reciprocal Code
  (creates "paired relationship records", example MOTHER ↔ DAUGHTER), Use Title, Use Dates.
  (`documentation.advsol.com/imis/v2.0/docs/relationship-detail-window`; `.../set-up-relationship-types-window`)
- **Verified — primary organisation (the only structural link).** A contact has one Primary organization; "If you delete
  or change a person's Primary organization, they are completely removed from that organization." Contacts may add
  themselves to an organisation from their account page; company administrators "cannot search for contacts" (security)
  and can only add brand-new contacts. (`documentation.advsol.com/imis/docs/setting-primary-organization`;
  `.../managing-organizations`)
- _Multiple households / multiple organisations._ Verified: relationships are unlimited, so a person can be "Spouse of"
  one person and "Parent of" several. Only one Primary organization exists; secondary organisation membership is not
  documented on the fetched pages.
- _Dependants, ageing out._ Not found.

#### 2.3 Addresses and communication preferences

- **Verified — organization flow down.** Enabled by default: the organisation's main address, country and phone are
  "automatically added to the account pages of the organization's contacts" whose primary organisation it is; when the
  main address changes, iMIS updates each contact whose address still matched the old one and otherwise "preserves the
  individual's main address." Staff get a prompt to "flow down the new primary organization's address to the contact or
  leave the contact's address unaltered." Disable via Settings > Contacts > System options. Limits: "Flow down does not
  apply to an organization who is a contact of a parent organization"; user-defined flow-down fields "always overwrite
  existing child data"; such changes are not written to the change log.
  (`documentation.advsol.com/imis/docs/organization-flow-down`)
- **Inferred.** This is the only address-sharing mechanism; nothing equivalent exists between two individuals (spouses),
  so a family sharing an address either duplicates the address on each person or is modelled as an organisation.
- **Verified — communication preferences.** Communication types are defined per organisation as Opt-out ("Users will
  receive communications until they choose explicitly to no longer receive") or Opt-in; system types Primary, Shipping,
  Billing cannot be edited. Preferences are per contact; sharing across related contacts is not documented. On merge,
  "the retained contact's mailing preferences are saved, and the discard contact's mailing preferences are ignored."
  (`documentation.advsol.com/imis/docs/contact-settings-communication-preferences`; `.../understanding-what-is-merged`)
- _Household naming / salutation._ Not found; iMIS has no household object to name.

#### 2.4 Membership, billing and "family" memberships

- **Verified — Bill organization / list billing.** "List billing enables an organization to be directly billed for their
  members' membership fees." It is set on the customer type (Bill organization) or per member with **Bill to ID for
  renewals**, "the ID number of the third-party contact responsible for taking care of fees", which wins over the
  primary organisation. "The member must define their primary organization in order for the membership fees to be
  billed" to it; otherwise the member is invoiced personally. Company administrators use Renew Now on the organisation
  profile, can untick Pay per member and add/remove optional items; only staff change amounts and term dates; "DUES and
  MISC items can't be removed"; a cancelled line leaves the paid-thru date "not moved forward". "List billing is not
  supported with AutoPay." (`documentation.advsol.com/imis/docs/list-billing`)
- **Verified.** Settings can flow company pricing to linked records: "Flows company (parent) pricing to linked (child)
  records. For example, if you enable this option, a non-member gets customer pricing if their parent company is a
  customer." (`help.imis.com/enterprise/features/settings/membership.htm`, via excerpt) The Membership settings page
  also shows a main billing contact field annotated "Primary functionality not available."
  (`documentation.advsol.com/imis/docs/membership-settings`)
- **Verified — company administrator powers (the de-facto "head of household" role).** Manage the organisation profile;
  remove members, update roles, add new contacts; "View and update account information for organization members";
  "Process dues and pay event registration for existing and new contacts"; "Pay any outstanding invoices for members and
  new contacts"; "Join as a member on behalf of company contacts that are not currently a member"; bill event
  registrations and AutoPay memberships to the organisation. Only staff assign the role; it can carry an end date.
  (`documentation.advsol.com/imis/docs/managing-organizations`)
- **Verified — Join Now.** Non-members and inactive members (paid-through empty or past) see Join Now; the public are
  sent to a Sign Up page to create an account; "Staff users can perform membership joins on behalf of non-members and
  inactive members"; "Company administrators can perform membership joins on behalf of non-members and inactive members"
  within their own company. (`documentation.advsol.com/imis/docs/understanding-join-now`) Organisation joins use an
  organisation customer type and a membership billing cycle; "To prorate fees, you must bill on an 'Annual' basis"; a
  former member still active who rejoins "is handled as a renewal, so prorating doesn't apply."
  (`.../setting-up-organization-joins`; `.../membership-settings`)
- **Verified (SDK).** Adding a subscription accepts an optional `BillToPartyId`.
  (`developer.imis.com/v20.3.200/docs/adding-modifying-and-paying-for-a-subscription`, via excerpt)
- **Family / household membership — no vendor documentation found.** Searches for "family membership", "household
  membership", "spouse membership", "dependents" across help.imis.com, documentation.advsol.com and the named
  implementer sites (CSI, fusionSpan, Niche, Causeis, ATS) returned no iMIS-specific page. The ASI Features Guide and
  Membership Configuration Wizard describe only customer types, billing cycles and billing products.
- **Inferred — how implementers evidently build families.** Two patterns follow from the verified mechanics: (a) _family
  as an organisation_: create a customer type with "Is an organization" and "Bill organization", make the paying adult
  the Company Administrator, link each family member via primary organisation, and let list billing invoice the "family"
  record — this yields a roster, one bill, flow-down address and company pricing; (b) _family as separate individual
  customer types_ (e.g. PRIMARY and SPOUSE/CHILD types with different dues products), linked only by informational
  relationships, each billed personally unless a Bill to ID for renewals is set. Pattern (a) borrows every
  organisational constraint (one primary organisation per person, administrators cannot search existing contacts, flow
  down overwrites addresses); pattern (b) loses any single "household" view. Neither is documented as a family feature.
- _Named vs unnamed coverage._ Inferred: always named; coverage is a contact's own customer type/paid-through, never a
  seat count.
- _Entitlements._ Verified: pricing and member status derive from the contact's own customer type (M/NM) or, with the
  flag above, the parent company's.

#### 2.5 Search, merge and dedupe

- **Verified.** Community > Manage duplicates, or Actions > Resolve duplicates on a record; Standard licence = manual
  search; Duplicate Merge – Premium adds a potential-duplicates list and configurable formulas (e.g. "Strict – Matches
  contacts if they have the same first five characters of their last name, first three characters of their first name").
  Entry-time checking uses a key of ZIP5 + first four of last name + first initial and warns the operator.
  (`documentation.advsol.com/imis/docs/merging-individuals-and-organizations`; `.../managing-multiple-duplicates`;
  `documentation.advsol.com/imis/v2.0/docs/duplicate-record-checking`)
- **Verified — what merging does.** Kept record retains everything; empty fields are filled from the discarded record;
  Contact IDs on Orders, Invoices, Event Registrations and Group Memberships are repointed; "All financial information,
  including order history, orders, billing parties, and shipping parties, are merged"; "Members of the discarded
  organization are transferred to the retained organization" and it becomes their primary organisation; the discarded
  username is deleted; engagement scores are reassigned; with flow down on, the retained organisation's address flows
  down to the transferred contacts. (`.../understanding-what-is-merged`)
- **Reported.** A vendor of a duplicate add-on states that with online joins "it's inevitable that you'll find duplicate
  records in your database." (`causeis.com/smartduplicates`)
- _Household search / merge._ Not applicable (no household object).

#### 2.6 Giving at household level

- **Verified.** Soft credits are manual: a Type labels the relationship ("family member, friend, or co-worker"); one
  gift can be soft-credited to many contacts with even or custom percentages; "adding more than 100 may cause
  performance issues." Donor-entry duplicate checking uses the same name/ZIP key.
  (`help.imis.com/enterprise/features/fundraising/soft_credits.htm`, via excerpt;
  `documentation.advsol.com/imis/v2.0/docs/checking-for-duplicate-donor-records`)
- _Automatic household giving roll-up._ Not found. Inferred: a couple's combined giving requires manual soft credits or
  reporting that joins on relationship rows.

#### 2.7 Life events

- Verified mechanics only: dated relationships (Use dates), dated administrator roles, primary-organisation change with
  an address prompt, merge for duplicate people. No divorce/split or ageing-out workflow found.

#### 2.8 Portal (RiSE)

- **Verified.** Public users can view relationships but not edit them (staff only, see 2.2). Contacts can link
  themselves to an organisation from their account page if "Allow all users to link to an organization" is on. Company
  administrators get Renew Now / list billing, join-on-behalf, pay-for-others, and an alert per organisation with
  outstanding renewals. Join Now + Sign Up for new members; AutoPay enrolment at join/renew.
  (`.../setting-primary-organization`; `.../managing-organizations`; `.../understanding-join-now`;
  `help.imis.com/Q42015/Features/Membership/Billing/Automatic_membership_renewals.htm`, via excerpt)
- **Inferred.** A family head can only self-serve for relatives if the family is modelled as an organisation and the
  head is made its Company Administrator by staff.

#### 2.9 Reported limitations

- **Reported.** Reviews: "very steep learning curve"; frustration at paying "for different modules, on top of the
  already high licence costs"; one reviewer "often felt restricted and limited." (`capterra.com/p/15885/iMIS/reviews/`;
  `trustradius.com/products/imis/reviews`)
- **Verified** constraints relevant to families: one primary organisation per person; list billing incompatible with
  AutoPay; administrators cannot attach existing contacts; address flow down silently overwrites and is unlogged for
  user-defined fields; relationships are staff-only to edit; duplicate detection is a paid tier.

---

### 3. Secondary products

#### 3.1 MemberClicks and YourMembership (Community Brands / Momentive)

- **Verified (via excerpt).** MemberClicks (MC Professional) has Individual member types ("Individual profiles are not
  linked to any other profile … Each member is responsible for paying their own dues") and Organization member types:
  "organizations or companies that have one or more linked profiles attached to the organization profile. Organization
  profiles are managed by linked profiles that are assigned as key contacts." Key contacts "can pay dues on behalf of
  the organization, edit and update the organization profile, edit and update other linked profiles, and register other
  linked profiles for events." "An organization can only link members who belong to the linked member type associated
  with the organization member type." Key contacts cannot add linked profiles by default; "Support can enable this on
  your behalf. We don't recommend this if member type pricing is based on the number of linked profiles"; added-profile
  fees are not billed until the next renewal invoice.
  (`help.memberclicks.com/hc/en-us/articles/15442981361421-Managing-organization-member-types`;
  `.../15442884581133-Member-types-overview-and-default-settings`; `.../15442958225805-Adding-individual-member-types`)
- **Reported.** A customer review: custom attributes "don't 'cascade' from org-level profiles to their linked individual
  profiles." (`softwareadvice.com/membership-management/memberclicks-profile/reviews/`) A League of Women Voters
  chapter's own guide describes linking two members at one address "as a Household" with a primary and second member; I
  could not confirm which platform it runs on (the page would not fetch), so it is not attributed to MemberClicks.
- **Inferred.** MemberClicks family memberships are the organisation pattern re-labelled: the "family" is an
  organisation profile, the paying adult a key contact, children linked profiles of a child member type.
- **YourMembership.** No public help article on sub-accounts, family memberships or linked profiles was found; the
  product pages say only "multi-tiered membership models, including individual and organizational memberships" and a
  self-service portal. (`yourmembership.com/products/association-management-software/features/`) Not found.

#### 3.2 Wild Apricot "membership bundles"

- **Verified (via excerpts of gethelp.wildapricot.com).** A membership level can be a **bundle**; the person who applies
  becomes the **bundle coordinator** (formerly "bundle administrator"). "All bundle members share the same renewal date"
  and the same status and level; "only the bundle coordinator can renew on behalf of the entire bundle"; the bundle
  level has one total price invoiced only to the coordinator; admins can set a maximum bundle size. Coordinators add
  members from the Bundle summary ("Add member", completing the application form on their behalf) — they "cannot add an
  existing member – as identified by their email address – to their bundle"; recurring payments must be stopped before
  adding. Only an admin can remove a member or reassign the coordinator; a coordinator can archive a member; members
  cannot change level while in a bundle. (`gethelp.wildapricot.com/en/articles/2867-bundle-membership-levels`;
  `.../514-bundle-coordinator-guide`; `.../143-membership-tab`; `.../2863-changing-membership-levels`)
- **Reported (user forum).** Long-running requests: move a member between bundles; keep financial history when the
  coordinator changes; "any membership fields that are assigned to the bundle membership level are applicable to ALL
  records in a bundle" (shared fields cannot differ per member); suspending a bundle member turns them into a non-member
  contact; bundle macros in emails only arrived in release 5.13.
  (`forums.wildapricot.com/forums/308932-wishlist/suggestions/8825677-revising-bundle-membership-functionality`;
  `.../15278841`; `.../8827555`; `.../8827285`)
- **Inferred.** The bundle is the cleanest _shared-term_ model in the set: one price, one renewal date, one payer, named
  members, no household entity beyond the coordinator's own record.

#### 3.3 Fonteva (Salesforce) subscription plans and assignments

- **Verified (via excerpts of docs.fonteva.com; pages are JS-only).** A subscription Item can have **Enable
  Assignments** so the buyer "can assign the subscription and its benefits to other Contacts," typically
  Company/Organizational memberships; it must also be enabled on the Item Class with at least one active default
  **Assignment Role**. **Restrict Number of Assignments** + **Maximum Number of Assignments** cap seats ("Assignments
  Allowed = Item Maximum Number of Assignments × Line Quantity"; a validation rule keeps Active Assignments ≤ Allowed);
  **Require Assignments** forces all seats to be filled before checkout. In the portal store an assignment table lets
  the buyer tick contacts or create a new contact. Extra seats mid-term are an amendment (a "+1 Company Membership Seat"
  item with 1 seat). Subscription plans are Calendar, Termed or Lifetime.
  (`docs.fonteva.com/user/creating-a-subscription`; `.../subscriptions`; `.../amending-subscriptions`;
  `.../assignment-roles-for-subscription-item-class`; `.../purchasing-memberships`)
- **Reported (customer guide).** AUSA's chapter guide: "If there is more than one beneficiary of a
  subscription/membership, you add more than one subscriber as long as there is an availability or slot open," calling
  these donated assignments or designees; its reports filter membership types containing "Family".
  (`ausa.org/.../Fonteva Users Guide for Chapters Version 1 - 12.15.20.pdf`)
- **Inferred.** Fonteva separates _payer_ (the Account/Contact on the Sales Order) from _covered members_ (Assignments
  on the Subscription), with an explicit seat count — so unnamed capacity is representable (unfilled seats) and family
  is a configuration of a seat-based item, not a record type.

---

### 4. Patterns that persisted across generations

1. **Two primitives everywhere: a typed, dated link between two parties, and a "benefit inheritance" rule layered on
   top.** NetForum (relationship types with flow-down flags, start/end dates), iMIS (relationship types with reciprocal,
   Use dates; primary organisation with list billing), MemberClicks (linked profiles under an organisation), Fonteva
   (Assignments with roles), Wild Apricot (bundle members under a coordinator). Verified in each product.
2. **The "family" is almost always the organisation model wearing a different label.** iMIS, MemberClicks, Fonteva and
   Wild Apricot all cover relatives through the construct built for employers: one paying parent record, named children
   records, an administrator role, roster-style self-service. Only NetForum Enterprise added a dedicated Household
   customer, and it did so by _copying_ the organisation flow-down and bill-to mechanics ("similar to membership
   flow-down for an organization"). Verified.
3. **Exactly one designated head.** NetForum requires one active head of household; iMIS has one Primary organisation
   and staff-assigned Company Administrators; Wild Apricot has one coordinator; MemberClicks has key contacts; Fonteva
   has the purchasing contact. Verified.
4. **Coverage is named, never anonymous — except where seats exist.** Flow-down (NetForum), customer type (iMIS), linked
   profile (MemberClicks) and bundle member (Wild Apricot) all require a named person record. Only Fonteva's seat count
   allows unfilled capacity. Verified/Inferred as noted.
5. **Dual-key enablement for inherited benefits.** NetForum needs both the member type and the relationship type
   flagged; iMIS needs both Bill organization (type) and a primary organisation (contact). Verified.
6. **Payer is separable from beneficiary, but by a field, not a model.** NetForum Bill To on the membership; iMIS Bill
   to ID for renewals / BillToPartyId; Fonteva Sales Order vs Assignment. Verified.
7. **Address sharing is parent-to-child flow down with an overwrite prompt**, never a shared address object: NetForum
   Linking/Defaulting; iMIS organisation flow down; NetForum household address = head's address. Verified.
8. **Merge is a keep-one, repoint-children operation with weak or no undo**, and detection is separate (report, premium
   licence or add-on). Verified.
9. **Household giving = soft credits**, automatic in NetForum Enterprise, manual in iMIS. Verified.
10. **Relationship editing is a staff task**; portals expose rosters only to a designated administrator. Verified (iMIS,
    Wild Apricot, MemberClicks); NetForum per-type eWeb flags.

### 5. Weaknesses worth avoiding

1. **No household object at all (iMIS, MemberClicks, YourMembership, Fonteva, Wild Apricot).** Forces the organisation
   hack, which drags in employer constraints: one primary organisation per person (a child of divorced parents cannot be
   in both families), administrators who cannot attach existing contacts, flow-down that overwrites home addresses, and
   no place to hang a joint salutation or household giving total. Verified constraints; inferred consequence.
2. **Household address as a linked copy of one person's address (NetForum).** Changing the head silently changes the
   household's mailing address and once dropped the link entirely (2017 SP1 defect). A household should own its address
   or reference one explicitly chosen member address with a visible rule. Verified defect; inferred remedy.
3. **Dual flags that must both be set (NetForum member type + relationship type; iMIS customer type + primary
   organisation).** Partial configuration produces members who silently receive no benefits. Verified.
4. **Flow-down treated as an expensive optional batch ("adds overhead to the system"; Pro shows inherited status only
   after the person's own membership ends or a back-end process runs).** Entitlement should be computed, not
   materialised by a nightly job. Verified.
5. **Reciprocity as a code-table convention, not a guarantee.** iMIS pairs records by Reciprocal type; a blank
   reciprocal shows the same label both ways; NetForum documents no reciprocal for individuals at all. Parent/child
   asymmetry needs first-class direction. Verified.
6. **No dependant semantics.** None of the products documents date-of-birth-driven coverage, ageing-out, or
   guardianship; associations must police this by hand. Not found across all products (searched).
7. **No proration or mid-term change story for family composition** except Fonteva's seat amendment. Verified absence in
   fetched docs.
8. **Named-only coverage.** A "family of four" with a newborn, a visiting grandparent or a plus-one has no
   representation until a person record exists (and in Wild Apricot that person must not already be a member). Verified
   for Wild Apricot; inferred elsewhere.
9. **Shared fields forced identical across a group (Wild Apricot) or not cascading at all (MemberClicks).** Neither
   extreme fits households; per-field inherit/override is needed. Reported.
10. **Merge without undo and without duplicate-checking of merged children (NetForum); merge discards the losing
    contact's communication preferences (iMIS).** Household merges will be more common than individual merges (marriage,
    re-entry), so merge must be reversible and preference-preserving. Verified.
11. **Portal self-service gated on a staff-assigned administrator role (iMIS, MemberClicks), and administrators
    forbidden to link existing people (iMIS, Wild Apricot).** The common family case — "add my spouse who already has an
    account" — is exactly what these rules block. Verified.
12. **List billing incompatible with AutoPay (iMIS).** A household that pays once a year for everyone cannot be on
    card-on-file renewal. Verified.
13. **Login resolves to the first matching customer when emails are shared (NetForum eWeb).** Families share email
    addresses; identity must be separate from the person record. Verified.

---

### 6. Sources

NetForum Enterprise (documentation.mip.com mirror of the Abila help; fetched directly unless noted)

- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/CRM.htm — Households listed as a CRM group item.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Households/Households.htm — definition of
  households, mailing purpose, subtopic list.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Households/Adding%20a%20Household.htm — household
  creation fields.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Households/Adding%20a%20Household%20Relationship.htm
  — head of household rules, relationship type, start/end dates, add/remove members.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/CRM/Households/Household%20Profile/Household%20Profile.htm
  — household address = head's address, member flag, tabs and actions.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/CRM/Households/Household%20Profile/Household%20Memberships%20Child%20Form.htm
  — Mbr Through column, membership columns.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/CRM/Households/Adding%20a%20Membership%20from%20the%20Household%20Profile.htm
  — household as the member, flow-down to members.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/CRM/Households/Adding%20a%20Chapter%20Membership%20from%20the%20Household%20Profile.htm
  — chapter memberships for households.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/CRM/Households/Enabling%20Membership%20Flowdown%20for%20Households.htm
  — member-type and relationship-type flags.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/GettingStarted/WhatsNew2015.1.htm — 2015.1 household
  features (org-to-household, flow-down, automatic soft credit); household role list fix 16468.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Overview/Relationship%20Setup.htm — categories,
  hard-coded combinations, spouse/parent/child, limit, flow-down and eWeb flags.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Overview/Customer%20Setup/Setting%20Up%20Relationship%20Types.htm
  — relationship-type fields and flags.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Individuals/Individual%20Profile/Child%20Forms/Relations.htm
  — Relations tab child forms including Household relationships.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Individuals/Individual_Organization_Relationship.htm
  — dates, end-date hiding, primary affiliation, primary contact, table name.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Individuals/Adding_an_Individual.htm — address
  Linking/Defaulting/None, on-hold cascade, name fields.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/Membership/Members/Members.htm — members as
  individuals/organisations, flow-down, multiple memberships option, Bill To section, renewals.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/CRM/Individuals/Combine_and_Merge_Duplicate_Records.htm
  — merge process, no detection, no unmerge, duplicate child data.
- https://documentation.mip.com/netforum-enterprise/2017.1/content/2017%20Build%20Release%20Pages/NF%202017.1.6%20and%20Hotfixes.htm
  — merge defects 23529/23777, GDPR household-relationship message 23731.
- https://documentation.abila.com/netforum-enterprise/2017.1/Content/Membership/Chapters/Chapters.htm — (excerpt only;
  certificate expired) reciprocal flag on chapter-to-chapter relationships.
- https://documentation.mip.com/netforum-enterprise/2017.1/Content/eWeb/EWeb_LoginTech_%20Info.html — (excerpt) eWeb
  users are Individuals/Organizations; first-match login.
- https://documentation.mip.com/netforum-enterprise/2015.1/Content/Admin/iWeb_User.htm — (excerpt) eWeb user stored in
  co_customer.
- https://documentation.abila.com/netforum-enterprise/2017.1/content/2017%20Build%20Release%20Pages/ (2017 SP1 release
  notes, excerpt) — "Changing 'Head of Household' removes Household Primary Address – New Address not Linked".

NetForum Pro (documentation.abila.com; excerpt only, certificate expired)

- https://documentation.abila.com/netforumpro/html5_help/Content/netFORUM_Module_Topics/Membership/Benefits_Flow_Down_Overview.htm
  — flow-down conditions, 10 levels, does-not-receive-benefits flag, Membership Flow child form, overlap caveat.
- https://documentation.abila.com/netforumpro/2019_1_html5_new/Content/netFORUM_Module_Topics/CRM/Allowing_Membership_Benefits_Flow_Down.htm
  — Allow Benefits Flow Down checkbox.

NetForum community / marketing

- https://www.netforumams.com/features/ — parent/child accounts, multi-tiered memberships.
- https://www.netforumams.com/blog/how-savvy-associations-use-technology-grow-non-dues-revenue/ — household soft credit
  for combined family/couple giving totals.
- https://associationengine.com/netforum-integration/ — (Reported) eWeb organisation roster management by primary
  contact.
- https://www.naspaa.org/netforum-member-guide — (Reported) representative switching to institution account to pay.
- https://www.g2.com/products/netforum-ams/reviews — (Reported) duplicates and org-linking complaint.
- https://www.capterra.com/p/184340/NetForum/ — (Reported) complexity/support complaints.
- https://fatlabwebsupport.com/blog/nonprofit/netforum-wordpress-integration/ — (Reported) eWeb replaced by custom
  member centre.

iMIS (documentation.advsol.com = ASI documentation; fetched directly unless noted)

- https://documentation.advsol.com/imis/docs/contact-settings-relationship-types — relationship type fields, reciprocal,
  Use dates, _ORG-ADMIN.
- https://documentation.advsol.com/imis/docs/relationships — staff-only editing, no limit, roles, both-sides display,
  deletion, Group List Editor.
- https://help.imis.com/enterprise/features/community/contacts/contacts.htm — (excerpt) "Child of", "Guest of"
  reciprocal examples.
- https://documentation.advsol.com/imis/v2.0/docs/relationship-detail-window — Classic desktop relationship fields
  (Status, Group Code ROLE/DONOR, Date Added).
- https://documentation.advsol.com/imis/v2.0/docs/set-up-relationship-types-window — paired relationship records,
  MOTHER/DAUGHTER, Use Title/Use Dates.
- https://developer.imis.com/docs/party-examples — Party/Person/Organization/ComboParty, PrimaryOrganizationInformation,
  FullAddress, CommunicationPreference.
- https://documentation.advsol.com/imis/docs/contact-settings-customer-types — Is an organization, Is a member, Bill
  organization, M/NM, Primary fee.
- https://documentation.advsol.com/imis/docs/setting-primary-organization — single primary organisation, removal on
  change, self-linking, administrators cannot search contacts.
- https://documentation.advsol.com/imis/docs/organization-flow-down — address/phone flow down, prompt, disable option,
  limitations.
- https://documentation.advsol.com/imis/docs/contact-settings-communication-preferences — opt-in/opt-out types, system
  types.
- https://documentation.advsol.com/imis/docs/list-billing — list billing, Bill to ID for renewals, primary organisation
  requirement, Renew Now, AutoPay incompatibility.
- https://help.imis.com/enterprise/features/settings/membership.htm — (excerpt) "Flows company (parent) pricing to
  linked (child) records".
- https://documentation.advsol.com/imis/docs/membership-settings — renewal options, rejoin not prorated, billing contact
  "Primary functionality not available".
- https://documentation.advsol.com/imis/docs/managing-organizations — Company Administrator capabilities and
  restrictions.
- https://documentation.advsol.com/imis/docs/understanding-join-now — Join Now actors, Sign Up page, staff/company-admin
  joins on behalf.
- https://documentation.advsol.com/imis/docs/setting-up-organization-joins — organisation customer type, Bill
  organization, Annual proration rule, itemised renewals.
- https://developer.imis.com/v20.3.200/docs/adding-modifying-and-paying-for-a-subscription — (excerpt) BillToPartyId.
- https://documentation.advsol.com/imis/docs/merging-individuals-and-organizations — merge entry points, Standard vs
  Premium, keep-record rule.
- https://documentation.advsol.com/imis/docs/understanding-what-is-merged — what is repointed/merged/discarded.
- https://documentation.advsol.com/imis/docs/managing-multiple-duplicates — Premium duplicate pairs and formulas.
- https://documentation.advsol.com/imis/v2.0/docs/duplicate-record-checking — entry-time key and warning.
- https://help.imis.com/enterprise/features/fundraising/soft_credits.htm — (excerpt) soft credit type "family member",
  multiple recipients, 100 limit.
- https://documentation.advsol.com/imis/v2.0/docs/checking-for-duplicate-donor-records — donor duplicate formula.
- https://help.imis.com/Q42015/Features/Membership/Billing/Automatic_membership_renewals.htm — (excerpt) AutoPay
  enrolment at join/renew.
- https://help.imis.com/professional/Features/Community/Contacts/Managing_organizations.htm — (excerpt) Group Member
  Administrator cannot add existing contacts.
- https://www.causeis.com/smartduplicates — (Reported) duplicates from online joins.
- https://www.capterra.com/p/15885/iMIS/reviews/ and https://www.trustradius.com/products/imis/reviews — (Reported)
  learning curve, module cost, restriction complaints.

MemberClicks / YourMembership

- https://help.memberclicks.com/hc/en-us/articles/15442981361421-Managing-organization-member-types — (excerpt; 403 to
  fetcher) linked profiles, key contacts, pricing by linked-profile count.
- https://help.memberclicks.com/hc/en-us/articles/15442884581133-Member-types-overview-and-default-settings — (excerpt)
  groups vs member types.
- https://help.memberclicks.com/hc/en-us/articles/15442958225805-Adding-individual-member-types — (excerpt) individual
  profiles unlinked, pay own dues.
- https://www.softwareadvice.com/membership-management/memberclicks-profile/reviews/ — (Reported) attributes do not
  cascade to linked profiles.
- https://www.yourmembership.com/products/association-management-software/features/ — organisational memberships only;
  no family/sub-account documentation found.

Wild Apricot

- https://gethelp.wildapricot.com/en/articles/2867-bundle-membership-levels — (excerpt; JS-rendered) bundle level,
  single price, bundle limit.
- https://gethelp.wildapricot.com/en/articles/514-bundle-coordinator-guide — (excerpt) coordinator powers, shared
  renewal date, cannot add existing members, admin-only removal/reassignment.
- https://gethelp.wildapricot.com/en/articles/143-membership-tab — (excerpt) Add to bundle; status changes.
- https://gethelp.wildapricot.com/en/articles/2863-changing-membership-levels — (excerpt) members must leave bundle to
  change level.
- https://forums.wildapricot.com/forums/308932-wishlist/suggestions/8825677-revising-bundle-membership-functionality —
  (Reported) move members, keep finances on coordinator change, shared fields apply to all.
- https://forums.wildapricot.com/forums/308932/suggestions/15278841 — (Reported) suspending a bundle member makes them a
  non-member.
- https://forums.wildapricot.com/forums/308932/suggestions/8827555 — (Reported) shared organisation fields request.
- https://forums.wildapricot.com/forums/308932-wishlist/suggestions/8827285-allow-bundle-administrator-and-other-bundle-relate
  — (Reported) bundle macros added in 5.13.

Fonteva

- https://docs.fonteva.com/user/creating-a-subscription — (excerpt; JS-only) Enable Assignments, Restrict/Maximum Number
  of Assignments, Require Assignments.
- https://docs.fonteva.com/user/subscriptions — (excerpt) plan types Calendar/Termed/Lifetime.
- https://docs.fonteva.com/user/amending-subscriptions — (excerpt) +1 seat amendment.
- https://docs.fonteva.com/user/assignment-roles-for-subscription-item-class — (excerpt) assignment roles prerequisite.
- https://docs.fonteva.com/user/purchasing-memberships — (excerpt) portal assignment table.
- https://www.ausa.org/sites/default/files/filefield_paths/Fonteva%20Users%20Guide%20for%20Chapters%20Version%201%20-%2012.15.20.pdf
  — (Reported; 403 to fetcher, excerpt) beneficiaries/designees, "Family" membership types.

## 30. Households in nonprofit and association software: Salesforce NPSP, Salesforce Nonprofit Cloud, Tessitura, and eight smaller CRMs

## Households in nonprofit and association software: Salesforce NPSP, Salesforce Nonprofit Cloud, Tessitura, and eight smaller CRMs

Research for the Rhythm 2.0 Households architecture proposal. Written 2026-10-21.

### How to read this document

Every claim carries one of three labels:

- **Verified** — stated in vendor documentation, a vendor-published API reference, or open-source code (the NPSP
  repository is public: https://github.com/SalesforceFoundation/NPSP).
- **Reported** — stated by implementers, community forum posters, or vendor staff answering in a forum; not confirmed
  against documentation.
- **Inferred** — my reading of the evidence; not stated by any source.

Where a source could not be reached, I say so. Two systematic access problems shaped this research:

1. `help.salesforce.com` articles render through a JavaScript shell that returns only "Sorry to interrupt / CSS Error"
   to a fetcher, and `developer.salesforce.com` object-reference pages return HTTP 403. For NPSP I went to the
   open-source code instead, which is stronger evidence. For Nonprofit Cloud I relied on Trailhead (vendor-authored,
   fetchable) and on the field descriptions the search engine had indexed from the developer reference pages; those are
   quoted with their URLs but I could not read the full pages.
2. The Tessitura community forum (`community.tessitura.com`) requires a login. Everything from it is **Reported**, taken
   from search-engine excerpts of the threads, with the thread URL given. The Tessitura REST API help hosted publicly by
   the California Academy of Sciences (`ts-stg-appgw.calacademy.org/TessituraService/Help`) is Tessitura-authored and is
   treated as **Verified** for field names and field notes.

---

### 1. Salesforce Nonprofit Success Pack (NPSP)

#### 1.1 What a household is

**Verified.** In the Household Account model, a household is an Account record (record type "Household Account") and
every Contact points at it through the standard `Contact.AccountId` lookup. NPSP creates the household automatically
when a Contact is saved without an account, and names it from the contact's last name by default. (Trailhead, _Manage
Household Accounts_:
https://trailhead.salesforce.com/en/content/learn/modules/constituent-data-management-with-nonprofit-success-pack/manage-household-accounts;
Trailhead, _Configure Household Naming_:
https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/configure-household-naming-npsp.)

**Verified (code).** The repository still carries two older household mechanisms alongside the Household Account: a
legacy `npo02__Household__c` custom object (classes `LegacyHouseholds`, `LegacyHouseholdMembers`, `HH_HHObject_TDTM`)
and the "One-to-One" and "Bucket" account models handled by `ACCT_IndividualAccounts_TDTM`. The Household Account is the
current recommendation; the others survive for orgs that never migrated. (Repo tree:
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/domain.)

**Verified (code).** `Account.Number_of_Household_Members__c` and `Account.All_Members_Deceased__c` are NPSP custom
fields on Account; `Contact.HHId__c` is a formula convenience field.
(https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Account/fields and
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Contact/fields.)

The Salesforce Help article "What Is the Household Account Model?"
(https://help.salesforce.com/s/articleView?id=sfdo.NPSP_What_is_an_Account_Model.htm&language=en_US&type=5) exists but
could not be fetched.

#### 1.2 One household per person

**Verified.** Because membership is the single-valued `Contact.AccountId`, a Contact belongs to exactly one household.
Trailhead states each Contact "should be related to one household Account" and that other family links are expressed
with Relationship records. (Trailhead _Manage Household Accounts_, above.) The third-party NPPatch documentation states
the same constraint plainly: "Each contact belongs to a single household".
(https://nppatch.readthedocs.io/en/latest/features/household-management/ — third party.)

**Reported.** Bigger Boat Consulting describes a human-services agency that needed "which households have shared custody
of children" and which household should be primary for each child, and built a custom _Relationship Junction_ object
(Household master-detail to Account, Related Household lookup to Account, Description) plus Process Builder to create
the reverse record, with relationship types such as "Shared Custody" and "Split Household". That is, the two-household
child problem was solved by custom household-to-household links, not by letting a Contact belong to two households.
(https://biggerboatconsulting.com/advanced-npsp-household-management-for-human-services/.)

#### 1.3 Primary Contact

**Verified.** NPSP designates the first contact created as the household's Primary Contact; it is editable on the
account's Details tab. The primary contact "receives hard credit for household donations. Other members receive soft
credit", and the primary contact exists partly so that one household is not solicited several times. (Trailhead _Manage
Household Accounts_.)

**Verified (code).** The underlying field is the managed-package field `Account.npe01__One2OneContact__c`;
`LegacyHouseholds.updatePrimaryContactOnAccountsAfterInsert` sets it, and `Households.cls` sets
`npe01__One2OneContact__c = extractFirstId(members.ids())` when it has to pick one. When a contact is marked deceased,
`CON_DoNotContact_TDTM` re-queries the household's non-deceased contacts "ORDER BY npo02__TotalOppAmount__c DESC,
CreatedDate ASC" to choose a replacement primary, i.e. the biggest remaining donor becomes primary.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/CON_DoNotContact_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/domain/LegacyHouseholds.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/domain/Households.cls.)

#### 1.4 Household naming, formal greeting, informal greeting

**Verified (code and Trailhead).** Naming is driven by the custom setting `Household_Naming_Settings__c` with exactly
these fields: `Household_Name_Format__c`, `Formal_Greeting_Format__c`, `Informal_Greeting_Format__c`,
`Name_Connector__c`, `Name_Overrun__c`, `Contact_Overrun_Count__c`, `Implementing_Class__c`.
(https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Household_Naming_Settings__c/fields.)
Trailhead documents the defaults: household name `{!LastName} Household`, formal greeting with salutation and full name,
informal greeting first name only, connector "and", overrun text "Friends", overrun count 2. (Trailhead _Configure
Household Naming_.)

**Verified (code).** The format grammar, from `HH_NameSpec.cls`: a name spec looks like
`prefix {lastname} {{firstnamespec}} suffix`, where the first-name spec is "a combo of fields and literals like
`{salutation} {firstname} ({mailingcity})`". The generator groups contacts by last name so that "Sam and Jo Smith and
Pat Jones" collapses correctly, applies the connector between the last two names and commas before that, stops after
`Contact_Overrun_Count__c` contacts and appends the overrun word, and returns an "anonymous" name/greeting when the
contact list is empty. Contacts whose `npo02__Naming_Exclusions__c` string excludes them from a given string are dropped
from that string only. `Implementing_Class__c` names a class implementing `HH_INaming`, so an org can replace the whole
algorithm. (https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_NameSpec.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_INaming.cls.)

**Verified (code).** Per-contact exclusion is stored twice: three checkboxes `Exclude_from_Household_Name__c`,
`Exclude_from_Household_Formal_Greeting__c`, `Exclude_from_Household_Informal_Greeting__c` (unmanaged fields in the
repo) and the older managed multi-value string `npo02__Naming_Exclusions__c`; a `NamingExclusions` domain class keeps
the two in sync (`HouseholdNamingExclusionsCheckboxes`, `HouseholdNamingExclusionsString`).
(https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/domain.)

**Verified.** The Manage Household page exposes the three exclusions per member ("minor child or a deceased household
member" is Trailhead's example). Automatic naming can be turned off org-wide, and a user can still edit the name or
greeting on an individual account. "Refresh All Household Names" (NPSP Settings > Bulk Data Processes) overwrites every
household's name and greetings with the generated values, including custom ones, and cannot be undone; Trailhead tells
you to export ID, Name, Formal Greeting and Informal Greeting first. (Trailhead _Configure Household Naming_; _Manage
Household Accounts_.)

**Reported.** The custom-name override flag itself (`npo02__SYSTEM_CUSTOM_NAMING__c`) lives in the managed package and
is not in the public repo; NPPatch describes the mechanism as "existing household names are marked as custom-named,
which prevents automatic updates" when naming is first activated.
(https://nppatch.readthedocs.io/en/latest/features/household-management/.)

**Reported.** A Trailblazer Community post from June 2025 describes a Summer '25 known issue in which custom household
names and greetings revert to the automatic values after an edit, and contacts being automatically excluded from
greetings; "as of right now (6/17/25), there are no solutions."
(https://trailhead.salesforce.com/trailblazer-community/feed/0D54V00007igJt1SAE and the npsprelated topic
https://trailhead.salesforce.com/trailblazer-community/topics/npsprelated.)

#### 1.5 Relationships (person to person)

**Verified.** Relationships connect one Contact to another; Affiliations connect Contacts to organizations. Saving a
relationship creates the reciprocal on the other contact. Types can be gender-neutral (parent/child); NPSP ships 24
preconfigured types managed on the Relationship object. The Status field distinguishes Current from Former, and the
Relationship Viewer labels non-current lines "(Former)". (Trailhead _Connect Contacts Using Relationships_:
https://trailhead.salesforce.com/content/learn/modules/constituent-data-management-with-nonprofit-success-pack/connect-contacts-using-relationships;
_Create Custom Relationships_:
https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/create-custom-relationships-npsp.)

**Verified (code).** `REL_Relationships_TDTM` implements the reciprocal pair: on insert it creates the mirror record and
stores each record's id in the other's `npe4__ReciprocalRelationship__c`; the mirror is flagged
`npe4__SYSTEM_SystemCreated__c`; updates to Type or Status propagate to the mirror; deleting one deletes the other;
duplicate relationships for a contact are removed; and a configurable set of custom fields is synchronised across the
pair (`npe4__Relationship_Settings__c.Enable_Custom_Field_Sync__c` plus the `Relationship_Sync_Excluded_Fields__c`
object). Gender-based reciprocals: when `npe4__Reciprocal_Method__c` is "List Setting", the handler reads
`npe4__Relationship_Lookup__c` list settings, reads the contact field named in `npe4__Gender_Field__c`, matches its
value against the comma-separated labels `npe4.Male` / `npe4.Female`, and chooses the male, female or neutral reciprocal
name accordingly (so "Parent" becomes "Son" or "Daughter" or "Child").
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/REL_Relationships_TDTM.cls.)

**Verified (code).** `REL_Relationships_Con_TDTM` auto-creates relationships from lookup fields on Contact and reacts to
gender changes; `REL_Relationships_Cm_TDTM` auto-creates them from Campaign Members;
`npe4__Relationship__c.Related_Opportunity_Contact_Role__c` lets a relationship type generate an Opportunity Contact
Role (a soft credit) for the related contact on the donor's gifts.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/REL_Relationships_Con_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/npe4__Relationship__c/fields.)

**Reported.** Forum posts describe reciprocals failing to be created or being duplicated, traced to disabled or
duplicated trigger handlers in the org, not to the design.
(https://trailhead.salesforce.com/trailblazer-community/topics/npsprelationships.)

#### 1.6 Affiliations (person to organization)

**Verified (code).** `npe5__Affiliation__c` links a Contact to an organization Account with a status and a Primary flag;
`AFFL_Affiliations_TDTM` keeps `Contact.Primary_Affiliation__c` and the affiliation's primary checkbox in step, removes
duplicate affiliations, and ensures only one primary. `npe5__Affiliation__c.Related_Opportunity_Contact_Role__c` creates
a contact role on gifts from the organization.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/AFFL_Affiliations_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/npe5__Affiliation__c/fields.)

#### 1.7 Addresses: shared household address, seasonal addresses, per-contact override

**Verified (code).** The `Address__c` object has these fields: `Household_Account__c` (parent), `Default_Address__c`,
`Address_Type__c`, `MailingStreet__c`, `MailingStreet2__c`, `MailingCity__c`, `MailingState__c`, `MailingPostalCode__c`,
`MailingCountry__c`, `Seasonal_Start_Month__c`, `Seasonal_Start_Day__c`, `Seasonal_End_Month__c`, `Seasonal_End_Day__c`,
`Latest_Start_Date__c`, `Latest_End_Date__c`, `Undeliverable__c`, `Exclude_from_Updates__c`, `Verified__c`,
`Verified_Date__c`, `Verification_Status__c`, `Pre_Verification_Address__c`, `API_Response__c`, `Ambiguous__c`,
`Geolocation__c`, `Formula_MailingAddress__c`, `Formula_MailingStreetAddress__c`, `Administrative_Area__c`,
`County_Name__c`, `Congressional_District__c`, `State_Upper_District__c`, `State_Lower_District__c`,
`NCOA_Evaluation_Date__c`, `NCOA_Result_Code__c`. Seasonal windows are month/day pairs with no year, so they recur
annually; a scheduled job (`ADDR_Seasonal_SCHED`) swaps the household's current address when a window opens or closes.
(https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Address__c/fields;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/ADDR_Seasonal_SCHED.cls.)

**Verified (code).** Contact-side fields: `is_Address_Override__c`, `Current_Address__c`, `Undeliverable_Address__c`,
`Address_Verification_Status__c`. The trigger handler comment in `ADDR_Contact_TDTM` states the rules: inserting a
contact makes its address a new default household address unless Address Override is set; updating a contact's address
does the same; "moving an existing contact to a new household or different household without an address — make their
address a new default address, unless they say it is an override". Org settings:
`npe01__Contacts_And_Orgs_Settings__c.Household_Account_Addresses_Disabled__c`,
`Organizational_Account_Addresses_Enabled__c`, `Simple_Address_Change_Treated_as_Update__c`.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/tdtm/triggerHandlers/contact/ADDR_Contact_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/npe01__Contacts_And_Orgs_Settings__c/fields.)

**Verified.** Trailhead: a household can hold several address records; changing the household address creates a new
address record and syncs to every member; a member with Address Override stops receiving household updates "either for a
new address or one marked as undeliverable"; marking undeliverable on any of contact, household or address record
propagates to all; `Household Mailing Address` on Contact is a formula showing the household's address. Seasonal
addresses are entered on the household's Addresses related list with start and end dates. Address verification
(SmartyStreets, Google, Cicero, custom) applies only to records created after setup. (Trailhead _Manage Addresses_:
https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/manage-addresses-npsp.)

**Inferred.** The design is "one shared default per household, overridable per person, with time-windowed alternates" —
the same shape as Tessitura's `Months` string and Virtuous's seasonal address ranges (sections 3 and 4).

#### 1.8 Soft credits and household giving

**Verified.** Credit is carried by standard Opportunity Contact Roles (OCRs). Roles ship as Decision Maker, Donor,
Household Member, Influencer, and others; an admin decides per role whether it counts as soft credit. The primary
(donor) OCR is the hard credit. When household member soft credit is on, saving a gift creates a Donor OCR for the giver
and "Household Member" OCRs for every other household member automatically. Relationship-based and affiliation-based
soft credits exist but must be configured. When several automation rules apply, "NPSP uses an order of precedence to
avoid creating duplicate contact roles". Specific opportunity record types or types can be excluded (e.g. in-kind).
(Trailhead _Manage Contact Roles and Soft Credits_:
https://trailhead.salesforce.com/en/content/learn/modules/donation-soft-credit-management-with-nonprofit-success-pack/manage-contact-roles-and-soft-credits;
_Create and Manage Soft Credits_:
https://trailhead.salesforce.com/content/learn/modules/donation-soft-credit-management-with-nonprofit-success-pack/create-and-manage-soft-credits;
_Set Up Soft Credits_:
https://trailhead.salesforce.com/content/learn/modules/opportunity-settings-in-nonprofit-success-pack/set-up-soft-credits-npsp.)
The Help articles "Configure Automated and Manual Soft Credits" and "NPSP Logic for Creating Opportunity Contact Roles"
are named by Trailhead but could not be fetched.

**Verified (code).** `HH_OppContactRoles_TDTM` is the Opportunity trigger handler "for managing Opportunity Contact
Roles for Household members" and includes a routine that "Recreates Soft Credit OCR's for all Opportunities for the
specified Household Accounts" (used after merges and moves). `OPP_OpportunityContactRoles_TDTM` fills the Account from
the Contact (or the Contact from the account's primary contact), creates the primary OCR, and manages Honoree and
Notification Recipient OCRs.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_OppContactRoles_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/OPP_OpportunityContactRoles_TDTM.cls.)

**Verified (code).** Partial Soft Credit: object `Partial_Soft_Credit__c` with fields `Amount__c`, `Contact__c`,
`Contact_Name__c`, `Contact_Role_ID__c`, `Opportunity__c`, `Role_Name__c`; `PSC_PartialSoftCredit_TDTM` matches each PSC
to an OCR (creating one if needed) and propagates role changes; the "Manage Soft Credits" page is
`PSC_ManageSoftCredits_CTRL`. Account Soft Credit: object `Account_Soft_Credit__c` with `Account__c`, `Amount__c`,
`Opportunity__c`, `Role__c`, rolled up by `CRLP_RollupAccSoftCredit_SVC`. Contact soft-credit rollup fields in the repo:
`Number_of_Soft_Credits__c`, `..._This_Year__c`, `..._Last_Year__c`, `..._Two_Years_Ago__c`, `..._Last_N_Days__c`,
`Soft_Credit_Last_N_Days__c`, `First/Last/Largest_Soft_Credit_Amount__c` and `_Date__c`. The settings object
`npo02__Households_Settings__c` carries `Matched_Donor_Role__c` (the role used for matching-gift soft credits).
(https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Partial_Soft_Credit__c/fields;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Account_Soft_Credit__c/fields;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Contact/fields;
https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/npo02__Households_Settings__c/fields.)

**Verified.** Rollups (including "Household Donor" style totals on the account and soft-credit totals rolled from
contacts to the household) are computed by Customizable Rollups in a nightly batch; a Recalculate Rollups button forces
one record. (Trailhead _Manage Contact Roles and Soft Credits_.)

**Reported.** Practitioners note soft-credit amounts "commonly don't line up" with the gift total because soft credit is
a stewardship device, not an accounting one, and prefer the Manage Soft Credits button to hand-created Partial Soft
Credits. (https://massmailer.io/?p=17643; https://trailhead.salesforce.com/trailblazer-community/topics/softcredit.)

#### 1.9 Manage Household UI, moving a contact, merging and splitting

**Verified.** The Manage Household page (`HH_ManageHH_CTRL`, a Visualforce page with a field set) lets a user add an
existing or new contact, set the three naming exclusions per member, change the household address (which syncs to
members without override), and choose the primary contact. Address Override is set on the contact edit page, not here.
(Trailhead _Manage Household Accounts_;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_ManageHH_CTRL.cls.)

**Verified (code).** What happens to gifts when a contact changes household:
`ContactService.moveOppsForContactsSwitchingAccounts` collects contacts whose `AccountId` changed between old and new
household accounts, then `moveOppsToContactAccount` queries
`OpportunityContactRole WHERE IsPrimary = TRUE AND Opportunity.AccountId IN (old households) AND ContactId IN (moving contacts)`
and re-points each such Opportunity's `AccountId` to the contact's new household. So: gifts where the mover is the
primary contact role travel with them; gifts where they were only a soft-credited household member stay with the old
household. Afterwards `householdService.updateHouseholds` and `Households.updateNameAndMemberCount` rename both
households and recount members, and household-member soft-credit OCRs are regenerated by `HH_OppContactRoles_TDTM`.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/service/ContactService.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/domain/LegacyHouseholds.cls.) Address behaviour on
a move is in 1.7.

**Verified (code).** After a Contact Merge, `CON_ContactMerge_TDTM` runs an async fix-up that will "update the Account
name and greetings; move Opps to the Master Contact's Account; recalculate Household Soft Credit OCRs; recalculate
Opportunity Rollups", and performs "affiliation and relationship cleanup on the master contacts".
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/CON_ContactMerge_TDTM.cls.)

**Verified.** Trailhead: use NPSP Contact Merge rather than the standard Salesforce merge, because the standard tool
"does not distinguish between a household and an organizational account" and "any merged contact in NPSP will end up
with an anonymous household"; merging is destructive and irreversible. Households can be merged or split ("if household
members go through a divorce" is the stated reason for a split) "without losing data"; the procedure is in the Help
article _Merge or Split Households_
(https://help.salesforce.com/s/articleView?language=en_US&id=sfdo.npsp_merge_or_split_households.htm&type=5), which
could not be fetched. (Trailhead _Merge Duplicate Contacts and Accounts_:
https://trailhead.salesforce.com/en/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/merge-duplicate-contacts-and-accounts-npsp;
_Manage Household Accounts_.)

**Verified (code).** Empty legacy `npo02__Household__c` records are deleted (`LegacyHouseholds.deleteEmptyHouseholds`).
I did not find equivalent code deleting an empty Household _Account_ automatically; treat that as unconfirmed.

#### 1.10 Deceased and Do Not Contact

**Verified (code).** `CON_DoNotContact_TDTM` "Handles changes to the deceased and do not contact fields on Contact".
Setting `Deceased__c` sets `Do_Not_Contact__c`, which sets `HasOptedOutOfEmail` and `DoNotCall`; if the household has
other non-deceased members, the deceased contact is excluded from household name, formal greeting and informal greeting
(all three exclusion checkboxes set and synchronised) and the household's primary contact is reassigned to the remaining
member with the highest total giving. Unchecking Deceased reverses the flags. A 2022 batch (`DeceasedBatch`) maintains
`Account.All_Members_Deceased__c`.
(https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/CON_DoNotContact_TDTM.cls;
https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/DeceasedBatch.cls.)

**Reported.** Despite that code, community posts report that after marking a spouse deceased "the informal and formal
greetings still include the name of the deceased" and the person still counts toward household size, with only the
account name updating. (https://trailhead.salesforce.com/trailblazer-community/feed/0D54V00007igJt1SAE.) A
customer-written NPSP guide ("When Someone Dies", Nonprofits101, updated Feb 2024) says the deceased "doesn't get a
NAMED Household, but goes into the Anonymous Household bucket", tells staff to check Do Not Mail on the address, move
survivors out, change donation primary contacts and contact roles, and merge any duplicate created by the death — a
reminder that the automation covers flags and naming but not gift re-attribution.
(https://nonprofits101.screenstepslive.com/s/npsp3/m/111936/l/1781130-when-someone-dies.)

#### 1.11 Portal

No NPSP-specific household portal behaviour was found; NPSP has no bundled constituent portal (Experience Cloud sites
are built per org). **Not found.**

#### 1.12 Known limitations and complaints (summary)

- One household per contact; children in shared custody need custom junctions (1.2). **Reported/Verified.**
- Rollups are nightly; totals lag until recalculated (1.8). **Verified.**
- Automatic naming fights manual overrides: Refresh All wipes custom names (**Verified**); a 2025 known issue reverts
  custom names (**Reported**).
- Deceased handling updates flags and names but community reports greetings not refreshing (1.10). **Reported.**
- Standard Salesforce merge strands contacts in anonymous households (1.9). **Verified.**
- Soft-credit totals are not expected to reconcile to gift totals (1.8). **Reported.**

---

### 2. Salesforce Nonprofit Cloud (2023+, now branded "Agentforce Nonprofit")

#### 2.1 The party model

**Verified.** Nonprofit Cloud (NPC) is built on Salesforce Industries. People are **Person Accounts** ("records that
combine fields from the Account and Contact objects into one record"); organizations and households are **business
accounts**. Three relationship objects connect them: **Account Contact Relationship** (person to business account, e.g.
to a household or employer), **Contact Contact Relationship** (person to person; "two records are used for an inverse
pair, such as spouses or parent and child"), and **Account Account Relationship** (business account to business
account). Two definition objects describe them: **Party Role Relationship** ("each direction of a reciprocal pair gets
its own role record (for example, parent-child and child-parent)") and **Party Relationship Group**, which is "attached
to a business account to classify it as a household or a group" and "also stores financial and other details about that
account". (Trailhead _Track Stakeholders and Their Relationships in Nonprofit Cloud_:
https://trailhead.salesforce.com/content/learn/modules/stakeholder-management-in-nonprofit-cloud/track-stakeholders-and-their-relationships-in-nonprofit-cloud.)

**Verified (developer reference, via indexed excerpts; full pages returned 403).**

- `PartyRelationshipGroup`: "a group of people living together such as a household, or a group of people affiliated with
  each other"; API 56.0+; `AccountId` (master-detail to Account), `Category` picklist (values seen: Extended Family,
  Meals together, Staying under same roof), `Subtype` picklist (Extension Household, Nuclear Household), `GroupSize`
  ("counts the active members"), `Status` (Active/Inactive).
  (https://developer.salesforce.com/docs/atlas.en-us.industries_reference.meta/industries_reference/sforce_api_objects_partyrelationshipgroup.htm.)
- `AccountContactRelation` Industries fields: `IsPrimaryMember` ("whether the member is a primary contact of a group"),
  `IsPrimaryGroup` ("whether the group is a primary group for the member"), `IsIncludedInGroup` ("whether the data of a
  group or a business account is included in the Household"), and `Roles`, a multi-select picklist whose family values
  include Daughter, Father, Husband, Mother, Other, Son, Wife.
  (https://developer.salesforce.com/docs/atlas.en-us.262.0.industries_reference.meta/industries_reference/sforce_api_objects_accountcontactrelation.htm.)
- `ContactContactRelation`: `ContactId`, `RelatedContactId`, `PartyRoleRelationId`, `RelatedInverseRecordId` (lookup to
  the inverse ContactContactRelation); API 57.0+.
  (https://developer.salesforce.com/docs/atlas.en-us.262.0.industries_reference.meta/industries_reference/sforce_api_objects_contactcontactrelation.htm.)
- `PartyRoleRelation`: `RelatedRoleName`, `RelatedInverseRecordId` (the inverse role record), `RelationshipObjectName`
  (Account_Account_Relationship or Contact_Contact_Relationship).
  (https://developer.salesforce.com/docs/atlas.en-us.264.0.sfFieldRef.meta/sfFieldRef/salesforce_field_reference_PartyRoleRelation.htm.)

**Inferred.** Reciprocity in NPC is therefore explicit and symmetrical in both the type layer (a PartyRoleRelation and
its inverse) and the instance layer (two ContactContactRelation rows pointing at each other), the same pattern NPSP
implemented with `npe4__ReciprocalRelationship__c`, but with no gender inference; the user picks the role on each side.

#### 2.2 Households as Party Relationship Groups

**Verified.** "A household is a business account. It's treated as a household because it's related to a party
relationship group with the type Household." Members are linked by Account Contact Relationship records. The same group
object also models "clubs, boards, congregations, neighborhoods, or program cohorts" distinguished by Category and
Subtype. A guided flow (Party Relationship Groups > New Group) creates the business account, the group, and the member
links; step 1 collects Members (at least one), Related Groups, Related Contacts (non-members such as a lawyer), Related
Business Accounts; step 2 requires a Roles value for every member and optional party role relationships between members,
each with a status. "Person accounts can belong to several party relationship groups at once." Groups can record
temporary situations (roommates) and can be **merged** (marriage) or **split** (adult child moving out) with included
guided flows. Primary Address, Group Income and Group Size are optional fields on the group. The household name is typed
free-form ("Campbell-Lozano Household"). (Trailhead _Manage Households in Nonprofit Cloud_:
https://trailhead.salesforce.com/content/learn/modules/stakeholder-management-in-nonprofit-cloud/manage-households-nonprofit-cloud.)

**Reported (Salesforce.org community sprint site).** The NPC best-practices site describes the "four record model"
(household Business Account, Person Account members, Account Contact Relationships, Party Relationship Group) and says
households are optional: organizations that don't need grouping can use Person Accounts directly. It warns that a custom
household flow must not skip the Party Relationship Group because it is "critical for some areas of NPC household
functionality, particularly in Fundraising".
(https://sfdo-community-sprints.github.io/npc-best-practices/stakeholder-management/stakeholder-management-prerequisites/.)
Its _Household ARC Recipe_ adds: the Account Contact Relationship "Role" is always "Household Member" and "Primary
Member" identifies "the head of the household"; **each individual belongs to only one household**, a rule stated "to
prevent donor giving summaries from double-counting donations"; when a parent leaves, the Account Contact Relationship
"must be deleted, not just deactivated, because the Active checkbox on the ACR record does not impact the rollup
calculation"; separated parents' Contact Contact Relationship is set inactive; children moving household get their old
ACRs deleted and new ones created; separated households are linked with an Account Account Relationship; organizations
should decide whether a household is residence-based or family-based.
(https://sfdo-community-sprints.github.io/npc-best-practices/Industry%20Common%20Components/Household%20ARC%20Recipe/.)

**Inferred.** The platform permits many groups per person (Trailhead), but the fundraising rollups assume one household
per person (community recipe). The flexibility is real at the data-model layer and constrained again at the reporting
layer.

#### 2.3 Naming, salutations, addresses

**Not found.** No NPC equivalent of NPSP's household naming settings (formula-generated household name, formal/informal
greeting, exclusions) was found in Trailhead or indexed Help content; the Trailhead flow has the user type the name. No
documentation of address inheritance from the household business account to person accounts was found; the group has an
optional Primary Address field only. Treat both as gaps relative to NPSP unless Salesforce documentation not reachable
here says otherwise.

#### 2.4 Gift attribution

**Verified (developer reference, via indexed excerpts).** Fundraising objects: `GiftTransaction` ("a completed
transaction from a gift"), `GiftSoftCredit` ("the soft credit attributed to a person or organization for the gift
transaction"; `GiftTransactionId` master-detail; `PartialAmount`, `PartialPercent`, calculated `SoftCreditAmount`;
`Role` picklist Influencer, Matched Donor, Other, Soft Credit (default), Solicitor, Third Party Donor),
`GiftDefaultSoftCredit` (API 62.0+; "default allocation for soft credits on gift commitment transactions that are
created by a recurrence engine"; `RecipientId`; `Role` includes Honoree and **Household Member**),
`GiftDefaultDesignation` ("default designation for gifts that originate from an opportunity, campaign, or commitment"),
`GiftCommitment` ("the commitment made by a donor") and `GiftCommitmentSchedule`.
(https://developer.salesforce.com/docs/atlas.en-us.nonprofit_cloud.meta/nonprofit_cloud/npc_fundraising_standard_objects.htm;
https://developer.salesforce.com/docs/atlas.en-us.262.0.nonprofit_cloud.meta/nonprofit_cloud/npc_fundraising_api_objects_giftsoftcredit.htm;
https://developer.salesforce.com/docs/atlas.en-us.262.0.nonprofit_cloud.meta/nonprofit_cloud/npc_fundraising_api_objects_giftdefaultsoftcredit.htm.)

**Verified.** Rollups ("Donor Gift Summary", "Outreach Summary", "Gift Designation" templates) are computed by the Data
Processing Engine; Donor Gift Summary "stores rollups for person and organization accounts". Household is not mentioned.
(Trailhead _Meet Fundraising Rollups and Data Processing Engine_:
https://trailhead.salesforce.com/content/learn/projects/create-fundraising-rollups-with-data-processing-engine/meet-fundraising-rollups-and-data-processing-engine.)

**Inferred.** Household soft credit in NPC is explicit data (a GiftSoftCredit row, or a default rule on a commitment
with Role "Household Member") rather than NPSP's implicit "every household member gets an OCR" trigger; the community
recipe's warning that ACR deletion is required for correct rollups suggests the household rollup walks the ACR links at
calculation time.

#### 2.5 How NPC differs from NPSP on households, and why Salesforce changed

**Verified / Reported.**

| Aspect           | NPSP                                                                                           | Nonprofit Cloud                                                                      |
| ---------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Person           | Contact under a Household Account                                                              | Person Account (one record)                                                          |
| Household        | Account record type; mandatory container for every Contact                                     | Business account + Party Relationship Group of type Household; optional              |
| Membership       | `Contact.AccountId` (exactly one)                                                              | Account Contact Relationship rows (many allowed by platform; one assumed by rollups) |
| Head             | `Primary Contact` on the account                                                               | `IsPrimaryMember` on the ACR                                                         |
| Person-to-person | `npe4__Relationship__c` pair, gender-aware reciprocals                                         | ContactContactRelation pair + PartyRoleRelation inverse types                        |
| Person-to-org    | `npe5__Affiliation__c`                                                                         | Account Contact Relationship                                                         |
| Household naming | Generated from format strings with exclusions and overrides                                    | Free text (no generator found)                                                       |
| Soft credit      | Opportunity Contact Roles, auto for household members; Partial and Account Soft Credit objects | GiftSoftCredit rows with roles and partial amounts; GiftDefaultSoftCredit rules      |
| Rollups          | Customizable Rollups nightly batch                                                             | Data Processing Engine into Donor Gift Summary                                       |
| Merge / split    | NPSP Contact Merge; Merge or Split Households help article                                     | Guided merge and split flows on the group                                            |

(Sources: sections 1 and 2 above; Salesforce Ben migration article
https://www.salesforceben.com/salesforce-nonprofit-success-pack-to-nonprofit-cloud-migration/ for the object mapping
"Household Accounts → Person Accounts + Party Relationship Groups" and "Recurring Donations → Gift Commitments".)

**Why Salesforce changed (Reported; no primary Salesforce statement found).** The searchable Salesforce statement
closest to a rationale is a Trailblazer Community post saying that "rather than layering nonprofit and grantmaking
applications on top of the platform, the opportunity was to build more directly into the core of Salesforce, starting
from a catalog of common components available from other industries"
(https://trailhead.salesforce.com/trailblazer-community/feed/0D54S00000PeXu6SAF). Implementers add: Person Accounts are
the model of Financial Services Cloud and Health Cloud, so NPC inherits platform investment such as Data Cloud, Flow and
AI that are "built on standard objects and Person Accounts"
(https://ynexgen.com/blog/nonprofit-cloud-person-accounts-explained); the party model "allows us to group as little or
as many things together under one banner" and avoids the bloated-account timeouts of the old Individual/Bucket model
(https://silversoftworks.com/nonprofit-cloud-hello-person-accounts/); it treats "every human as an individual first" so
one person can be linked to several organizations and families
(https://www.ksolves.com/blog/salesforce/nonprofit-cloud-vs-npsp-key;
https://www.soapboxengage.com/blog/2478-salesforce-new-nonprofit-cloud-tips-contacts-vs-person-accounts). NPSP remains
supported and migration is a re-implementation, not an upgrade
(https://www.salesforceben.com/the-state-of-salesforce-nonprofit-offerings-in-2026/).

---

### 3. Tessitura (arts and culture)

Public, Tessitura-authored sources are limited to the REST API help (hosted publicly by a licensee) and the product
marketing site; the user-forum material is **Reported**.

#### 3.1 Constituents, households and affiliations

**Reported (Tessitura training deck, 2017).** "Household: group of individuals who share a household. The individuals in
the household all have individual constituent records, to hold personal data, that are linked to the household record,
which holds data shared by members of the household."
(https://www.slideserve.com/ochoa/tessitura-training-constituents-part-1-powerpoint-ppt-presentation.)

**Verified (API).** A constituent has a `ConstituentType` (with a `ConstituentGroup`), names, `SortName`, `NameStatus`,
`MailIndicator`, `PhoneIndicator`, `EmarketIndicator`, `Inactive` and `InactiveReason`, `ProtectionType`,
`LastGiftDate`, `LastTicketDate`. The household is itself a constituent; members are linked by **Affiliation** records.
`Affiliation` fields: `AffiliationType`, `GroupConstituent`, `IndividualConstituent`, `PrimaryIndicator`, `StartDate`,
`EndDate`, `Inactive`, `NameIndicator`, `IsIncludedInSearchResults`, deprecated `IsAllowedToTransact`, `Title`,
`Salary`, and foreign keys `AddressId`, `ElectronicAddressId`, `SalutationId` (so an affiliation can carry its own
address, email and salutation for the person-in-this-group). `AffiliationType` fields include `RelationshipCategory`,
`UseSalary`, `UseTitle`, `IsIncludedInSearchResultsDefault`, `IsAllowedToTransactDefault`. `GET CRM/Affiliations`
accepts `primaryOnly` and `showWithGroup`.
(https://ts-stg-appgw.calacademy.org/TessituraService/Help/ResourceModel?modelName=Affiliation;
…?modelName=AffiliationType; …?modelName=Constituent;
https://ts-stg-appgw.calacademy.org/TessituraService/Help/Resources/Affiliations.)

**Reported (Tessitura Network documentation specialist, forum).** Data from individuals shows on the household record
only when the affiliation is **primary**; for non-primary affiliations the household gets only the individual's name,
and only if the affiliation type is set to "Show With Group".
(https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/19372/output-household-information-including-constituency-on-an-affiliate.)

**Reported.** The household record holds two name slots, A1 and A2 (also called N1/N2); "Swap A1/A2" under Constituent >
Household Operations regenerates the household salutation; a third adult is hard to fit, and one museum adds the third
person as an alias and prints a separate card "usable only with the member in their care".
(https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_next_generation-0/8170/adding-a-wife-n2-to-a-record-in-v11/25584;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/30129/third-adult-on-a-membership.)
**Verified (API).** `POST CRM/Constituents/{id}/Snapshot/SwapA1A2` "swaps the A1 and A2 names on a household account and
regenerates its salutation".
(https://ts-stg-appgw.calacademy.org/TessituraService/Help/Api/GET-CRM-Constituents-constituentId-Snapshot and the
Constituents resource list https://ts-stg-appgw.calacademy.org/TessituraService/Help/Resources/Constituents.)

#### 3.2 Address sharing and salutations

**Verified (API).** On `Address`: "Owning constituent of the address. If the constituent is a household, primary
affiliates may inherit the address"; `IsFromAffiliation` "indicates that this address is returned because of a
constituent's affiliation"; `AffiliatedConstituent` is the "Constituent which is linked to this address through an
Affiliation"; `AltSalutationType` is an "overriding salutation type to be used when this address is chosen"; `Months` is
a 12-character Y/N string for seasonal availability ("NNNNNYYYNNNN" for summer); `PrimaryIndicator`, `Inactive`,
`Label`, `StartDate`, `EndDate`. `GET CRM/Addresses` takes `includeAffiliations` (default true) and `primaryOnly`.
(https://ts-stg-appgw.calacademy.org/TessituraService/Help/ResourceModel?modelName=Address;
https://ts-stg-appgw.calacademy.org/TessituraService/Help/Api/Get-CRM-Addresses_constituentId_includeAffiliations_primaryOnly_includeFromAffiliations.)

**Verified (API).** `Salutation` fields: `EnvelopeSalutation1`, `EnvelopeSalutation2`, `LetterSalutation`,
`BusinessTitle`, `SalutationType` (a signer/"TR_SIGNOR" type, so different salutations per signer or purpose),
`DefaultIndicator`, `Label`, `IsFromAffiliation`.
(https://ts-stg-appgw.calacademy.org/TessituraService/Help/ResourceModel?modelName=Salutation.) **Inferred.** Tessitura
therefore stores salutations as records per constituent (and per affiliation), with a type dimension and an
address-level override, rather than generating them from a format string as NPSP does.

#### 3.3 Memberships and household memberships

**Verified (API).** `GET CRM/Memberships/Details` takes `constituentId`, `includeAffiliations` ("if true, will also
return details for affiliates of the constituent"), `membershipOrgIds`, `onlyShowCurrent`, `onlyShowDefault`;
`POST CRM/Memberships/Calculate` previews membership changes. `MembershipLevelSummary`: `MembershipOrganization`,
`Code`, `StartAmount`, `EndAmount`, `Rank`, `ConstituencyType`. The help flags the Memberships resource as due to
"change significantly in an upcoming major Tessitura release".
(https://ts-stg-appgw.calacademy.org/TessituraService/Help/Resources/Memberships;
…/ResourceModel?modelName=MembershipLevelSummary.) **Not found:** a public field list for member cards or named members
on a membership.

**Reported.** Where the membership lives is a site decision and a recurring argument: one centre keeps every membership
on the household; San Francisco Opera keeps all transactions (ticketing and contributions) on the household; a UK
gallery rejected households and keeps "a main record for Joint 1 and an affiliation for Joint 2". Tessitura blocks
assigning a membership to an affiliate of a household: "can't assign to an affiliate on a household and it defaults to
the household". A membership created on an individual stays there after the person joins a household; Tessitura Network
Support's fix is "Move Transactions to Household" (Constituents > Household Operations), which requires the
individual-to-household affiliation to be primary, may need a security right, and in one older test moved contributions
but not memberships; some items (events, donated seats, solicitations) are not moved; "Convert to Household" is the
heavier tool that also moves events, activities, salutations and affiliations. Stray orders on an A1/A2 arise "from a
merge or someone forgetting to move transactions to household when affiliating them".
(https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/31684/household-vs-individual-constituents;
https://communitytest.tessitura.com/topical_groups/uk-museums-tug/f/discussions/35444/joint-membership-management;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/35299/issue-with-the-header-for-a-member-account;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/10842/subscription-history-not-moving-from-individual-to-household;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_next_generation-0/8622/convert-to-household-move-transactions/26943;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/7910/move-transactions-to-hh.)

**Reported.** Version 12 added "Enhanced Constituent Tracking on Transactions": a transaction has an Owner (may be the
household) and an **Initiator** (the individual who made the gift or order), and ticket sub-line items have
**Recipients**; "keep the membership (with the $80 total) on the household and use the initiator field to track which
individual initiated the gift". Reporting caveat: pairing Total Household Giving with Initiator double-counts; "omit
Initiator from the selection".
(https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/9679/transactions-individual-vs-household-take-2-3-4;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/33210/contributions-attributed-to-households-automatically.)

**Reported.** Membership-as-product offers a "recipient" field for which person in the household is the member; both
membership methods support household grouping, card scanning and member-mode ranks. Barcoded cards may be tied to
individuals or to the household; a site with two cards per level asked which is better and got no answer in the excerpt.
(https://communitytest.tessitura.com/topical_groups/zoos-aquariums-tug/f/discussions/21591/membership-options-as-contributions-vs-as-products;
https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/12210/membership-card-tied-to-individual-or-household-account.)
Tessitura's product page claims unlimited programs and levels with configurable benefits and entitlements.
(https://www.tessitura.com/features/memberships.)

#### 3.4 TNEW (the web portal)

**Reported.** A customer whose web login is on the individual record, while the renewal order is owned by the household,
"will not see the renewal order unless the individual account is added as the Initiator for that order", a manual fix
per order. UK sites report that benefits can fail when members log into accounts linked to a household membership.
(https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_ticketing-0/22381/households-and-rollover-renewals-online-in-tnew/69366;
https://communitytest.tessitura.com/topical_groups/uk-museums-tug/f/discussions/35444/joint-membership-management.) The
National Zoo's public TNEW guide confirms TNEW is the member-facing purchase and account site but says nothing about
adding household members online (https://nationalzoo.si.edu/guide/tnew-guide). **Not found:** a Tessitura document
describing how a member adds or names family members in TNEW.

#### 3.5 Merge and dedupe

**Not found** in public documentation. Forum posts treat merges as a source of stray household/individual transactions
(3.3). **Reported.**

---

### 4. Other donor and member CRMs (brief)

#### 4.1 Neon CRM

**Verified (vendor developer docs).** Neon has individual and company accounts; "households group together individual
accounts that are part of a common household", with contact records linking individuals to the household, "however,
unlike company accounts, households are not accounts themselves and cannot have custom properties or 'own'
transactions". (https://developer.neoncrm.com/accounts/.) **Reported (indexed excerpt of a Neon developer page that has
since moved; https://developer.neoncrm.com/api/accounts/households/ now 404):** a household "is not itself a person or
an account, so it can't make donations or register for events"; its primary contact is "the person whose email and
physical address the household uses"; "an individual cannot be the primary contact (address holder) of more than one
household" but "can still belong to several households as a secondary member"; a household stores a Household Name and a
Household Salutation. **Reported (Neon support article behind a JavaScript wall;
https://support.neoncrm.com/hc/en-us/articles/218529607):** Household Group Memberships are bought by one member and
extended by adding "New Household Contact" members, each of whom must already have an account and is visible from the
primary member's account. The Account Match duplicate feature does not match household contact records
(https://support.neoncrm.com/hc/en-us/articles/360044215754-Account-Match-Queue). Neon's blog treats household giving as
a standard soft-credit use case (https://neonone.com/resources/blog/soft-credits/).

#### 4.2 Bloomerang

**Verified.** "Households combine individual constituents together so you can work with aggregate constituent data."
Only individuals can be members. Each household has one **Head of Household** whose contact information is used for
household communications ("select a Head of Household who has the strongest relationship with your organization"); it
can be changed at any time; if the HoH lacks a piece of contact information in a two-member household, the other
member's is used. "You can't add a donation to a household account" — gifts go on the member and appear on the household
timeline; a truly joint gift is recorded on the HoH. Letters can be "Create One Letter Per: Household"; household email
filters use only the HoH's interests; a communication preference such as Do Not Call applies to the household only when
every member has it. "A constituent may only belong to one household to avoid mailing conflicts and double-counting
donations." Adult children at their own address belong in Relationships, not the household. Membership information shows
only on the member who joined, not on the household. When a member dies, mark them deceased and "review the household
name". Households count as one constituent for licensing.
(https://help.bloomerang.com/en/articles/12632740-about-relationships-and-households.) Soft credits are entered
separately and "are not included in revenue or raised totals"
(https://help.bloomerang.com/en/articles/12632409-add-or-edit-soft-credits). Conversion guidance: households are built
from a Spouse relationship or a shared primary address
(https://help.bloomerang.com/en/articles/12632524-conversions-household-review).

#### 4.3 Little Green Light (LGL)

**Verified.** LGL offers two patterns and documents both. (a) One constituent record for the couple: "it's common to
create a single record representing the couple", with formal and informal addressee/salutation pairs; this "simplifies
donation tracking and mailings… soft credits are unnecessary". (b) Two linked records: the head is coded Contact Type
"Primary", the other "Spouse/Partner"; related records share address and phone data (editable only on the master
record); mailing queries add "Contact Type = Primary" to get one piece per household; Alt. Addressee / Alt. Salutation
tokens let a letter address the household while the record represents the individual; spouses are soft-credited on each
gift; email requires separate records because only one email per constituent can be used.
(https://help.littlegreenlight.com/article/77-managing-households-in-lgl;
https://help.littlegreenlight.com/article/78-creating-relationships-between-constituents.) Life changes under pattern
(a) require surgery: on divorce, "Clone constituent", generate soft credits on the clone so "you'll preserve the
couple's giving history on both records", relate them as "divorced spouse/partner", stop contact sharing, fix names and
pledges (https://help.littlegreenlight.com/article/551-updating-a-record-when-theres-a-divorce); on death, either edit
the joint record (status widowed, remove the name from addressee/salutation) or clone, mark the original deceased,
remove its "Primary" contact type, and put "Dec'd" in name fields as a fail-safe for envelope review
(https://help.littlegreenlight.com/article/88-managing-data-for-deceased-constituents). Addressee and salutation are
calculated from name settings at creation and can be bulk-recalculated only by export and re-import
(https://lgl.helpscoutdocs.com/article/569-bulk-updating-calculated-addressee-salutation).

#### 4.4 Virtuous

**Verified (vendor developer docs).** "A Contact in Virtuous represents a household or organization — not a person."
Contact types (Household, Organization, Foundation, configurable) are fetched from `GET /api/Contact/Types`. A Contact
holds many `ContactIndividual` records; "exactly one individual is the primary; at most one is the secondary"; the
primary is the default for receipts, communications and gift attribution, the secondary (spouse/partner) appears in
joint-giving displays, children carry neither flag; `canBePrimary`/`canBeSecondary` mark eligibility. Addresses belong
to the Contact, not the individual, with `isPrimary` controlling mailings and labelled seasonal date ranges
(winter/summer). "For Household Contacts, gifts go to the household even when a specific individual made the donation."
(https://docs.virtuous.org/crm/concepts/contacts.) **Reported (support articles behind a JavaScript wall).** Soft credit
is called "Passthrough": "Passthrough Giving is also commonly referred to as a Soft Credit"; one article says "Add up to
100 Passthrough Givers on a single Gift" while a migration article says only a single soft credit is tracked, a conflict
I could not resolve (https://support.virtuous.org/hc/en-us/articles/360051316471-What-Are-Passthrough-Gifts;
https://support.virtuous.org/hc/en-us/articles/6164646317069-How-Do-I-Add-a-Passthrough-Giver-to-a-Gift;
https://support.virtuous.org/hc/en-us/articles/4424186878861-Prepare-for-Database-Migration). Relationships link whole
households or an individual to a household; most types are symmetric ("Friend"), some are inverse pairs (Parent/Child,
Employer/Employee); "network giving" sums giving across relationships
(https://support.virtuous.org/hc/en-us/articles/28949895597965-How-Do-Relationships-Work-in-Virtuous-CRM). Migrations
merge spouses into one Household Contact
(https://support.virtuous.org/hc/en-us/articles/4424186878861-Prepare-for-Database-Migration).

#### 4.5 DonorPerfect

**Verified (vendor manual and blog).** The Link screen "stores relational links between two donor records, such as a
parent and child or an employee and employer", with link defaults so the reciprocal link is created automatically and
users can jump between linked donors; data is stored in `DPLINK`
(https://www.softerware.com/doclib/DP/Manuals/DPVE_SUP_UserManual_V10.pdf; the online manual
https://www.softerware.com/doclib/DP/Manuals/DPO_SUP_Manual.pdf exceeded the fetch size limit). A "Linked" report shows
contributions for a donor and the donors linked by a chosen relationship type. Households are typically one donor record
for the couple: the blog's example "Mary and Ken Hawley" share a record whose salutation lists both names; to address
them individually you create additional address records ("Copy Address from Main… then type in the unique salutation"),
tag each with a mailing type, and DonorPerfect tracks seasonal dates on addresses
(https://www.donorperfect.com/nonprofit-technology-blog/donor-management-software/manage-multiple-donor-addresses/). A
training video describes a mailing option that sends one piece per household using the primary address
(https://www.donorperfect.com/video/creating-a-mailing-list/). Marketing copy describes "relational 'household' records"
and soft credits that "create links between records"
(https://www.donorperfect.com/fundraising-software/donor-management-software/). **Not found:** a knowledgebase article
listing link types or a head-of-household field.

#### 4.6 CiviCRM

**Verified.** Household is one of three core contact types ("typically families that share a physical location"); core
types are mutually exclusive; only a name is required to create one; a household that no longer exists is "closed";
greetings and addressee are calculated from name fields using global formats with per-contact override; addresses can be
shared between contacts and "are synchronized in both directions".
(https://docs.civicrm.org/user/en/latest/organising-your-data/contacts/.) Relationship types are label pairs (A-to-B and
B-to-A); two household types ship, "Head of Household for" and "Household Member of"; the guide recommends "Household
Member of" in most cases and warns CiviCRM "will not stop you from adding multiple contacts as Head of Household for a
single household"; the "Household relationship" type "enables household members to have the 'household address' applied
to their record"; relationships have start/end dates, an Enabled flag, and can grant view or edit permissions on the
related contact. (https://docs.civicrm.org/user/en/latest/organising-your-data/relationships/.)

**Verified (issue tracker / project blog).** Known problems of household-as-contact-type: a couple needs three records;
mailing a household risks duplicates; household giving totals needed a custom report; online donations create new
individuals that break the grouping; the 2009 proposal was to make the Household type optional ("we would actually be
fine with eliminating the HH contact type altogether") and lean on soft credits (shipped in 2.2) and shared addresses
(https://civicrm.org/nl/node/558). CRM-4572 replaced "Use Household Address" with general shared addresses stored as
read-only copies linked by `master_id` (https://issues.civicrm.org/jira/browse/CRM-4572). Relationships are not
transitive: a membership inherited via "Household Member of" does not reach the Head of Household; closed Won't Fix
(https://issues.civicrm.org/jira/browse/CRM-1687). Export merging ignored the head-of-household type until 4.1
(https://issues.civicrm.org/jira/browse/CRM-9295); the shared-address code checks only "Household member of" and creates
a duplicate relationship for heads (https://lab.civicrm.org/dev/core/-/issues/3443); dedupe rules on address flag every
shared address as a duplicate (https://lab.civicrm.org/community/feature-request/-/issues/2); "Merge All Contacts with
the Same Address" ignores households (https://lab.civicrm.org/dev/core/issues/1364). Community workaround for
memberships: a Head-of-Household-to-member individual relationship so a membership bought by the head extends to members
(https://forum.civicrm.org/index.php%3Ftopic=10442.0.html; https://forum.civicrm.org/index.php%3Ftopic=23833.0.html).

#### 4.7 Kindful (now part of Bloomerang)

**Verified (thin).** The only reachable Kindful documentation is the Contact Filters Glossary, whose Household filter
offers All / Primary / Dependent, "will show contacts with primary, dependent, or either status in a household
relationship", and notes that contacts not in a relationship "are considered primary"
(https://support.kindful.com/hc/en-us/articles/360005909594-Contact-Filters-Glossary). Virtuous's Kindful migration
guide says "each Gift will have only one Soft Credit Link mapped to Passthrough Giver" and spouses are merged into a
household contact, implying Kindful stores a per-gift soft-credit link
(https://support.virtuous.org/hc/article_attachments/37817023846541 — Reported). **Not found:** a Kindful help article
describing households; the help centre home no longer lists one.

#### 4.8 Keela

**Verified (vendor glossary).** "A Household Record groups together individuals who live at the same address or share
familial ties"; when "the Smiths donate as a family unit, their gift is attributed to the household"
(https://www.keela.co/glossary/household-record). **Reported (support articles behind a JavaScript wall).** Households
are created from the Contacts view or an individual profile and behave like Smart Lists, "constantly updating as data
changes" (https://support.keela.co/hc/en-us/articles/43096248548749-Households); the Household Enhancements release
added a "Primary Household Filter for choosing the main point of contact in a household, which reduces duplicate
outreach" and a "Household Salutation Field" with filtering and export
(https://support.keela.co/hc/en-us/articles/43096219303437-Release-Notes-Household-Enhancements); Keela's glossary
defines a "primary contact" as the one person designated as main point of contact for a record
(https://www.keela.co/glossary/primary-contact-assignment).

---

### 5. Patterns that persisted across generations

| Pattern                                                                                                              | Products where it appears                                                                                                                                                                                                                                                                                                                         | Why it persists                                                                                                                                                                                                                                        |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **One designated head / primary member per household**                                                               | NPSP (Primary Contact), NPC (`IsPrimaryMember`), Bloomerang (Head of Household), Neon (primary contact = address holder), LGL ("Primary" contact type), Virtuous (`isPrimary` individual), Keela (Primary Household Filter), Kindful (Primary/Dependent), Tessitura (primary affiliation / A1), CiviCRM (Head of Household, unenforced)           | Every mailing, receipt and solicitation needs exactly one recipient and one donor of record; the head is the tie-breaker. NPSP and Bloomerang state the purpose explicitly: avoid soliciting one household twice and avoid double-counting.            |
| **Household as a separate record that people link to** (account, group, or constituent) rather than a flag on people | NPSP (Account), NPC (business account + PRG), Tessitura (household constituent), CiviCRM (Household contact), Bloomerang, Neon, Keela                                                                                                                                                                                                             | Shared data (address, name, greetings, giving total, membership) needs a home that outlives any single member. The exceptions (LGL, DonorPerfect, Virtuous) embed the couple in one record and pay for it at divorce and death.                        |
| **Three ways to represent a household, each with the same three consequences**                                       | _Household-as-account/group_: NPSP, NPC, Neon, Bloomerang, Keela. _Household-as-contact-type_: CiviCRM, Tessitura. _Household-as-the-record_: LGL, DonorPerfect, Virtuous                                                                                                                                                                         | Account/group: clean totals, but one-household-per-person. Contact-type: household can transact, but duplicates mailings and needs move-transaction tooling (Tessitura, CiviCRM). Embedded couple: simplest mailing, but cloning on life events (LGL). |
| **One household per person, enforced or assumed**                                                                    | NPSP (single `AccountId`), Bloomerang (explicit rule), Neon (primary of one; secondary of many), NPC (platform allows many; rollups assume one), eTapestry and Givebutter (explicit)                                                                                                                                                              | The constraint is driven by rollups and mailings, not by family reality; vendors that relax it (NPC, Neon) immediately re-impose it in the reporting layer.                                                                                            |
| **Soft credit as the bridge between individual donor and household recognition**                                     | NPSP (auto Household Member OCR, Partial and Account soft credit), NPC (GiftSoftCredit, default Household Member rule), Bloomerang, LGL, Virtuous (Passthrough), CiviCRM (added in 2.2 to fix households), DonorPerfect, Kindful, Neon                                                                                                            | Money has one legal source; stewardship wants everyone thanked. A separate credit row with a role and optional partial amount is the only design that satisfies both without double-counting the ledger.                                               |
| **Generated addressee / salutation with per-record override and per-person exclusion**                               | NPSP (format strings, exclusions, custom flag), Tessitura (salutation records with types and address-level override, A1/A2 swap), LGL (calculated addressee/salutation + Alt fields), CiviCRM (calculated greetings with override), Keela (household salutation field), Neon (household name + salutation), DonorPerfect (per-address salutation) | Letters need "Dear Sam and Jo" and envelopes need "Mr and Mrs Smith"; children and deceased members must drop out. Every product ends with generated defaults plus a manual override and a way to exclude people.                                      |
| **Shared household address with per-person override and seasonal alternates**                                        | NPSP (default address, override, seasonal month/day), Tessitura (primary affiliates inherit; `Months` string), Virtuous (contact-level addresses with seasonal ranges), LGL (shared with master record), CiviCRM (shared addresses via `master_id`), DonorPerfect (seasonal dates), Bloomerang (HoH address)                                      | Address is the most-shared and most-changed fact; one source of truth per household with explicit exceptions beats copying.                                                                                                                            |
| **Reciprocal relationships stored as a pair of records with a type and a current/former status**                     | NPSP (`ReciprocalRelationship__c`, Status), NPC (two ContactContactRelation rows, inverse PartyRoleRelation), CiviCRM (A-to-B / B-to-A labels, start/end, enabled), DonorPerfect (reciprocal links), Virtuous (inverse pairs), LGL (relationship types)                                                                                           | Each side must be queryable from its own record; history (ex-spouse, former guardian) must be kept without being active.                                                                                                                               |
| **Deceased is a status with consequences, not a deletion**                                                           | NPSP (`Deceased__c` sets Do Not Contact, naming exclusion, new primary; All_Members_Deceased), Bloomerang ("mark deceased and review the household name"), LGL (clone + "Dec'd" fail-safe), Tessitura (inactive reason)                                                                                                                           | Giving history and memorial gifts must survive; the household must keep functioning for survivors.                                                                                                                                                     |
| **Explicit tooling to merge, split and move people between households**                                              | NPSP (Contact Merge, Merge or Split Households, opp-move on account change), NPC (merge/split flows), Tessitura (Move Transactions to Household, Convert to Household, Swap A1/A2), LGL (Clone constituent), CiviCRM (merge-by-address, with household bugs)                                                                                      | Marriage, divorce, children leaving and death are the normal case; where tooling is missing (CiviCRM address merge, Tessitura stray orders) data quality fails.                                                                                        |
| **Membership owned by one record with other people covered**                                                         | Neon (Household Group Membership: purchaser + household contacts), Tessitura (membership on household or individual; v12 Owner/Initiator/Recipient; cards per person or household), CiviCRM (membership inheritance via relationship; non-transitive), Bloomerang (membership on the joiner only), NPSP (Membership fields on the account)        | Payment and entitlement differ: one payer, several beneficiaries. Products that do not model the covered people explicitly (CiviCRM, Tessitura A1/A2) hit "third adult" and inheritance problems.                                                      |

---

### 6. Weaknesses worth avoiding

1. **Hard-wiring one household per person into the person record** (NPSP `AccountId`, Bloomerang, Neon's primary-of-one
   rule). Shared custody, adult children, carers and roommates all break it; NPSP customers build custom
   household-to-household junctions. If totals and mailings need a single household, make that a _role_ on the
   membership (primary household, as NPC's `IsPrimaryGroup` and Neon's secondary membership already hint) rather than a
   cardinality constraint.
2. **Letting rollups silently depend on link state** (NPC: the ACR Active checkbox is ignored by rollups, so links must
   be deleted; Tessitura: Initiator double-counts household totals). Define once which links count toward household
   giving and make that the same rule the UI shows.
3. **Automatic soft credit to every household member as a trigger side-effect** (NPSP). It creates rows the user did not
   ask for, must be regenerated after every move or merge, and produces totals that "don't line up". NPC's move to
   explicit `GiftSoftCredit` rows plus default rules on commitments is the better shape.
4. **Nightly rollups** (NPSP) and recalculation buttons as the fix for stale totals.
5. **Generated names that overwrite manual edits** (NPSP Refresh All; the 2025 revert bug) and deceased handling that
   updates the name but not the greetings (community reports). Keep generated and overridden values as separate fields,
   never regenerate over an override without an explicit choice, and recompute greetings from the same exclusion set as
   the name.
6. **Household as a contact type that can transact in parallel with its members** (CiviCRM, Tessitura). It yields three
   records per couple, duplicate mailings, stray orders on the wrong record, and a permanent "move transactions" chore.
   If the household can own transactions, the product must also own the migration of transactions on every life change.
7. **Fixed slots for household members** (Tessitura A1/A2; LGL and DonorPerfect spouse fields). The third adult, the
   second spouse and the dependent child do not fit; sites resort to aliases and extra cards.
8. **Embedding the couple in one person record** (LGL, DonorPerfect, Virtuous's Contact-is-the-household). Divorce and
   death require cloning with synthetic soft credits to preserve history, and email marketing cannot reach the second
   person.
9. **Portal identity bound to the individual while entitlements live on the household** (Tessitura TNEW: renewal
   invisible unless the individual is set as Initiator; benefits failing for household logins). The portal must resolve
   a person to every household they belong to and the roles they hold there.
10. **Non-transitive relationship semantics for benefits** (CiviCRM: head of household does not inherit a membership
    extended to "household members"). Model covered members of a membership explicitly instead of deriving them from
    relationship types.
11. **Silent data loss in generic tooling** (standard Salesforce merge strands contacts in anonymous households; CiviCRM
    address merge ignores households; dedupe flags shared addresses). Household-aware merge and dedupe must be
    first-class.

---

### 7. Sources

Each entry: URL — what it supports — evidence class.

**Salesforce NPSP (Trailhead, vendor)**

- https://trailhead.salesforce.com/en/content/learn/modules/constituent-data-management-with-nonprofit-success-pack/manage-household-accounts
  — household as Account, one household per Contact, Primary Contact and hard/soft credit, Manage Household page,
  seasonal addresses, merge/split without data loss — Verified.
- https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/configure-household-naming-npsp
  — naming settings, defaults, exclusions, Refresh All warning — Verified.
- https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/manage-addresses-npsp
  — address object behaviour, override, undeliverable, verification, settings — Verified.
- https://trailhead.salesforce.com/content/learn/modules/constituent-data-management-with-nonprofit-success-pack/connect-contacts-using-relationships
  — relationships, reciprocals, Status, Relationship Viewer — Verified.
- https://trailhead.salesforce.com/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/create-custom-relationships-npsp
  — 24 shipped types, gendered terms — Verified.
- https://trailhead.salesforce.com/en/content/learn/modules/donation-soft-credit-management-with-nonprofit-success-pack/manage-contact-roles-and-soft-credits
  — OCR roles, soft credit by role, exclusions, nightly rollups — Verified.
- https://trailhead.salesforce.com/content/learn/modules/donation-soft-credit-management-with-nonprofit-success-pack/create-and-manage-soft-credits
  — automatic Household Member soft credit, relationship-based soft credit, precedence — Verified.
- https://trailhead.salesforce.com/content/learn/modules/opportunity-settings-in-nonprofit-success-pack/set-up-soft-credits-npsp
  — household member OCR example, settings — Verified.
- https://trailhead.salesforce.com/en/content/learn/modules/contact-and-account-settings-in-nonprofit-success-pack/merge-duplicate-contacts-and-accounts-npsp
  — NPSP Contact Merge vs standard merge (anonymous household) — Verified.
- https://help.salesforce.com/s/articleView?language=en_US&id=sfdo.npsp_merge_or_split_households.htm&type=5 — Merge or
  Split Households (exists; not fetchable) — n/a.
- https://help.salesforce.com/s/articleView?id=sfdo.NPSP_What_is_an_Account_Model.htm&language=en_US&type=5 — Household
  Account Model (exists; not fetchable) — n/a.

**Salesforce NPSP (open-source code, GitHub SalesforceFoundation/NPSP, branch main)**

- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Address__c/fields — Address__c
  field list — Verified.
- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Contact/fields — Deceased, Do
  Not Contact, exclusions, address override, soft-credit rollups, Primary_Affiliation — Verified.
- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Account/fields —
  Number_of_Household_Members, All_Members_Deceased, Membership_Status/Span — Verified.
- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Household_Naming_Settings__c/fields
  — naming settings fields — Verified.
- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Partial_Soft_Credit__c/fields
  and
  https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/Account_Soft_Credit__c/fields —
  soft credit objects — Verified.
- https://github.com/SalesforceFoundation/NPSP/tree/main/force-app/main/default/objects/npo02__Households_Settings__c/fields,
  …/npe4__Relationship__c/fields, …/npe5__Affiliation__c/fields, …/npe01__Contacts_And_Orgs_Settings__c/fields —
  settings and related-OCR fields — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_NameSpec.cls and
  HH_INaming.cls — name spec grammar, exclusions, overrun, pluggable class — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/CON_DoNotContact_TDTM.cls and
  DeceasedBatch.cls — deceased and do-not-contact behaviour — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/REL_Relationships_TDTM.cls,
  REL_Relationships_Con_TDTM.cls — reciprocal pairs, gender lookup, field sync, auto-create — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/AFFL_Affiliations_TDTM.cls —
  primary affiliation, dedupe — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/tdtm/triggerHandlers/contact/ADDR_Contact_TDTM.cls
  and https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/ADDR_Seasonal_SCHED.cls —
  address default/override on move, seasonal job — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_OppContactRoles_TDTM.cls,
  OPP_OpportunityContactRoles_TDTM.cls, PSC_PartialSoftCredit_TDTM.cls, PSC_ManageSoftCredits_CTRL.cls — household soft
  credit OCRs, partial soft credit sync, Manage Soft Credits — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/service/ContactService.cls and
  https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/domain/LegacyHouseholds.cls — opportunities
  moved when a contact changes household; primary contact selection; empty legacy households deleted — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/CON_ContactMerge_TDTM.cls and
  CON_ContactMerge_CTRL.cls — post-merge fix-ups — Verified.
- https://github.com/SalesforceFoundation/NPSP/blob/main/force-app/main/default/classes/HH_ManageHH_CTRL.cls — Manage
  Household controller — Verified.

**Salesforce NPSP (community / third party)**

- https://biggerboatconsulting.com/advanced-npsp-household-management-for-human-services/ — shared custody workaround
  with custom junction — Reported.
- https://nppatch.readthedocs.io/en/latest/features/household-management/ — third-party description of custom-naming
  flag, one household per contact — Reported.
- https://trailhead.salesforce.com/trailblazer-community/feed/0D54V00007igJt1SAE — deceased greetings not updating;
  Summer '25 naming known issue — Reported.
- https://trailhead.salesforce.com/trailblazer-community/topics/npsprelated and
  https://trailhead.salesforce.com/trailblazer-community/topics/npsprelationships and
  https://trailhead.salesforce.com/trailblazer-community/topics/softcredit — community reports on naming, reciprocals,
  soft-credit rollups — Reported.
- https://nonprofits101.screenstepslive.com/s/npsp3/m/111936/l/1781130-when-someone-dies — customer procedure for a
  death — Reported.
- https://massmailer.io/?p=17643 — soft credit totals do not reconcile; three objects — Reported.

**Salesforce Nonprofit Cloud**

- https://trailhead.salesforce.com/content/learn/modules/stakeholder-management-in-nonprofit-cloud/track-stakeholders-and-their-relationships-in-nonprofit-cloud
  — party model objects — Verified.
- https://trailhead.salesforce.com/content/learn/modules/stakeholder-management-in-nonprofit-cloud/manage-households-nonprofit-cloud
  — households as PRG, flow, roles, multiple groups, merge/split — Verified.
- https://trailhead.salesforce.com/content/learn/projects/create-fundraising-rollups-with-data-processing-engine/meet-fundraising-rollups-and-data-processing-engine
  — DPE rollups, Donor Gift Summary scope — Verified.
- https://developer.salesforce.com/docs/atlas.en-us.industries_reference.meta/industries_reference/sforce_api_objects_partyrelationshipgroup.htm
  — PRG fields (indexed excerpts; page 403) — Verified (partial).
- https://developer.salesforce.com/docs/atlas.en-us.262.0.industries_reference.meta/industries_reference/sforce_api_objects_accountcontactrelation.htm
  — IsPrimaryMember, IsPrimaryGroup, IsIncludedInGroup, Roles — Verified (partial).
- https://developer.salesforce.com/docs/atlas.en-us.262.0.industries_reference.meta/industries_reference/sforce_api_objects_contactcontactrelation.htm
  — ContactContactRelation fields — Verified (partial).
- https://developer.salesforce.com/docs/atlas.en-us.264.0.sfFieldRef.meta/sfFieldRef/salesforce_field_reference_PartyRoleRelation.htm
  — PartyRoleRelation fields — Verified (partial).
- https://developer.salesforce.com/docs/atlas.en-us.nonprofit_cloud.meta/nonprofit_cloud/npc_fundraising_standard_objects.htm
  — gift object descriptions — Verified (partial).
- https://developer.salesforce.com/docs/atlas.en-us.262.0.nonprofit_cloud.meta/nonprofit_cloud/npc_fundraising_api_objects_giftsoftcredit.htm
  and …giftdefaultsoftcredit.htm — soft credit fields and roles — Verified (partial).
- https://sfdo-community-sprints.github.io/npc-best-practices/stakeholder-management/stakeholder-management-prerequisites/
  and
  https://sfdo-community-sprints.github.io/npc-best-practices/Industry%20Common%20Components/Household%20ARC%20Recipe/ —
  four-record model, optional households, one household per person for rollups, ACR deletion rule — Reported.
- https://trailhead.salesforce.com/trailblazer-community/feed/0D54S00000PeXu6SAF — Salesforce wording on building into
  the core from common components — Reported.
- https://www.salesforceben.com/salesforce-nonprofit-success-pack-to-nonprofit-cloud-migration/ — NPSP-to-NPC object
  mapping — Reported.
- https://ynexgen.com/blog/nonprofit-cloud-person-accounts-explained,
  https://silversoftworks.com/nonprofit-cloud-hello-person-accounts/,
  https://www.ksolves.com/blog/salesforce/nonprofit-cloud-vs-npsp-key,
  https://www.soapboxengage.com/blog/2478-salesforce-new-nonprofit-cloud-tips-contacts-vs-person-accounts,
  https://www.salesforceben.com/the-state-of-salesforce-nonprofit-offerings-in-2026/ — implementer rationale for the
  model change; NPSP support status — Reported.

**Tessitura**

- https://ts-stg-appgw.calacademy.org/TessituraService/Help/ResourceModel?modelName=Affiliation, …=AffiliationType,
  …=Constituent, …=Address, …=Salutation, …=MembershipLevelSummary — field names and notes — Verified.
- https://ts-stg-appgw.calacademy.org/TessituraService/Help/Resources/Affiliations, …/Resources/Memberships,
  …/Resources/Constituents — endpoints and parameters incl. SwapA1A2 — Verified.
- https://ts-stg-appgw.calacademy.org/TessituraService/Help/Api/Get-CRM-Addresses_constituentId_includeAffiliations_primaryOnly_includeFromAffiliations
  — address inheritance note, seasonal Months — Verified.
- https://www.tessitura.com/features/memberships — product claims — Verified (marketing).
- https://www.slideserve.com/ochoa/tessitura-training-constituents-part-1-powerpoint-ppt-presentation — household
  definition from training deck — Reported.
- Forum threads (login required; cited from indexed excerpts):
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/31684/household-vs-individual-constituents;
  https://communitytest.tessitura.com/topical_groups/uk-museums-tug/f/discussions/35444/joint-membership-management;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/19372/output-household-information-including-constituency-on-an-affiliate;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/35299/issue-with-the-header-for-a-member-account;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/10842/subscription-history-not-moving-from-individual-to-household;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_next_generation-0/8622/convert-to-household-move-transactions/26943;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/7910/move-transactions-to-hh;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_next_generation-0/8170/adding-a-wife-n2-to-a-record-in-v11/25584;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_memberships-9/30129/third-adult-on-a-membership;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_technical-9/9679/transactions-individual-vs-household-take-2-3-4;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/33210/contributions-attributed-to-households-automatically;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_ticketing-0/22381/households-and-rollover-renewals-online-in-tnew/69366;
  https://communitytest.tessitura.com/topical_groups/zoos-aquariums-tug/f/discussions/21591/membership-options-as-contributions-vs-as-products;
  https://communitytest.tessitura.com/tessitura_software_forums/f/tessitura_development-9/12210/membership-card-tied-to-individual-or-household-account
  — household vs individual memberships, Move Transactions, A1/A2, Initiator, TNEW — Reported.
- https://nationalzoo.si.edu/guide/tnew-guide — TNEW as member site — Verified (customer-published).

**Other CRMs**

- https://developer.neoncrm.com/accounts/ — Neon households are not accounts and cannot own transactions — Verified.
- https://developer.neoncrm.com/api/accounts/households/ (now 404; excerpt indexed) — primary contact rules, secondary
  membership of several households, name and salutation — Reported.
- https://support.neoncrm.com/hc/en-us/articles/218529607 — Household Group Memberships (JavaScript wall; excerpt) —
  Reported.
- https://support.neoncrm.com/hc/en-us/articles/360044215754-Account-Match-Queue — household contacts excluded from
  matching — Verified.
- https://neonone.com/resources/blog/soft-credits/ — household giving as soft credit — Verified (vendor blog).
- https://help.bloomerang.com/en/articles/12632740-about-relationships-and-households — Bloomerang households, HoH, one
  household per constituent, no gifts on household — Verified.
- https://help.bloomerang.com/en/articles/12632409-add-or-edit-soft-credits — soft credits excluded from revenue —
  Verified.
- https://help.bloomerang.com/en/articles/12632524-conversions-household-review — households from spouse relationship or
  address — Verified.
- https://help.littlegreenlight.com/article/77-managing-households-in-lgl,
  https://help.littlegreenlight.com/article/78-creating-relationships-between-constituents,
  https://help.littlegreenlight.com/article/551-updating-a-record-when-theres-a-divorce,
  https://help.littlegreenlight.com/article/88-managing-data-for-deceased-constituents,
  https://lgl.helpscoutdocs.com/article/569-bulk-updating-calculated-addressee-salutation — LGL household patterns,
  divorce and death procedures, addressee calculation — Verified.
- https://docs.virtuous.org/crm/concepts/contacts — Virtuous Contact/Individual model, primary/secondary, addresses,
  gift attribution — Verified.
- https://support.virtuous.org/hc/en-us/articles/360051316471-What-Are-Passthrough-Gifts,
  https://support.virtuous.org/hc/en-us/articles/6164646317069-How-Do-I-Add-a-Passthrough-Giver-to-a-Gift,
  https://support.virtuous.org/hc/en-us/articles/4424186878861-Prepare-for-Database-Migration,
  https://support.virtuous.org/hc/en-us/articles/28949895597965-How-Do-Relationships-Work-in-Virtuous-CRM — passthrough,
  relationships (JavaScript wall; excerpts) — Reported.
- https://www.softerware.com/doclib/DP/Manuals/DPVE_SUP_UserManual_V10.pdf — DonorPerfect Link screen, reciprocal links,
  DPLINK — Verified.
- https://www.donorperfect.com/nonprofit-technology-blog/donor-management-software/manage-multiple-donor-addresses/ —
  per-address salutations, Copy Address from Main, seasonal dates — Verified.
- https://www.donorperfect.com/video/creating-a-mailing-list/ and
  https://www.donorperfect.com/fundraising-software/donor-management-software/ — one per household mailing option;
  household records marketing — Verified (vendor).
- https://docs.civicrm.org/user/en/latest/organising-your-data/contacts/ and
  https://docs.civicrm.org/user/en/latest/organising-your-data/relationships/ — CiviCRM contact types, household
  relationship types, shared address, permissions — Verified.
- https://civicrm.org/nl/node/558 — 2009 "Better Household Management" problems and proposal — Verified (project blog).
- https://issues.civicrm.org/jira/browse/CRM-1687, https://issues.civicrm.org/jira/browse/CRM-9295,
  https://issues.civicrm.org/jira/browse/CRM-4572, https://lab.civicrm.org/dev/core/-/issues/3443,
  https://lab.civicrm.org/community/feature-request/-/issues/2, https://lab.civicrm.org/dev/core/issues/1364 — CiviCRM
  household defects — Verified (tracker).
- https://forum.civicrm.org/index.php%3Ftopic=10442.0.html and https://forum.civicrm.org/index.php%3Ftopic=23833.0.html
  — membership workarounds — Reported.
- https://support.kindful.com/hc/en-us/articles/360005909594-Contact-Filters-Glossary — Kindful Primary/Dependent
  household filter — Verified.
- https://www.keela.co/glossary/household-record and https://www.keela.co/glossary/primary-contact-assignment — Keela
  household and primary contact definitions — Verified (vendor glossary).
- https://support.keela.co/hc/en-us/articles/43096248548749-Households and
  https://support.keela.co/hc/en-us/articles/43096219303437-Release-Notes-Household-Enhancements — Keela household
  behaviour and enhancements (JavaScript wall; excerpts) — Reported.

**Comparators cited only for the pattern table**

- https://www.blackbaud.com/files/support/helpfiles/etapestry/etapestry/content/etaphouseholds.html — eTapestry:
  accounts cannot be in multiple households — Verified.
- https://help.givebutter.com/en/articles/7321435-how-to-use-contact-households-and-relationships — Givebutter: one
  household per contact, primary contact — Verified.

## 31. W4 — Association-management platforms: households, constituent relationships and membership coverage

## W4 — Association-management platforms: households, constituent relationships and membership coverage

Research date: 2026-10-21. Scope: Fonteva, Nimble AMS, MemberSuite, GrowthZone (ChamberMaster lineage), YourMembership,
Novi AMS, Impexium, Protech UX 365, Naylor AMS (Timberlake), Wild Apricot bundles, Glue Up. Earlier research (NetForum,
iMIS, and the first pass on Wild Apricot/MemberClicks/Fonteva) is not repeated here except where this pass goes deeper.

Evidence levels used throughout:

- **Verified** — vendor documentation, an object/API reference, an OpenAPI/Swagger document, or vendor-published code.
  Field and object names are given only where a reference documents them.
- **Reported** — implementer blogs, customer-facing guides written by an association using the product, forum threads,
  reviews, or search-engine excerpts of vendor pages that could not be fetched directly (noted as such).
- **Inferred** — my conclusion from Verified or Reported facts; stated as inference.
- **Unavailable** — not findable in public material.

Where a vendor's internal storage is not documented, the section says "functional behaviour only". Pages that could not
be fetched are listed at the end.

The ten questions answered for each product:

1. Is a household a separate entity, a constituent type, an organization, a group, or a collection of relationships?
2. Can a household independently own a membership?
3. Can an individual purchase a membership covering several people without creating a household?
4. How are spouses, children, guardians and other family relationships represented (types, direction/reciprocity,
   dates)?
5. How are household/family members added or removed?
6. How are membership beneficiaries identified (named seats, roles, relationship-derived)?
7. How do benefits flow from the holder to covered individuals (materialised records, dynamic evaluation, flags)?
8. Can eligibility be determined dynamically through relationships?
9. How are household donations, communications, addresses and histories handled?
10. What limitations or architectural compromises are visible?

Plus, per product: payer vs holder; renewal, cancellation and composition changes; entitlement explanation ("is this
person entitled, and why").

---

### 1. Fonteva (Salesforce)

**Primary evidence.** The Fonteva object reference mirrored at `fonteva-devdocs.herokuapp.com` (full field tables for
the `OrderApi` package, downloaded and grepped locally), the Apex service docs at
`api.fonteva.io/TechnicalDocs/ApexDocs/FDService/` (Subscription, Assignment, BusinessGroup wrappers), and Higher
Logic's Fonteva integration guide (which documents the queries it runs against Fonteva objects). `docs.fonteva.com`
pages render a "CSS Error / Loading" shell to a non-browser client; what they say is taken from search-engine excerpts
and marked Reported.

**Model (Verified from the object reference).**

- Constituents are standard Salesforce `Account` and `Contact`. Individuals get their own Account; the package setting
  `OrderApi__Individual_Account_Name_Extension__c` says: "The individual account name extension is used when naming new
  individual/person accounts created in the system. The default value is "Household" which produces "John Doe
  Household"." This is a naming convention for the one-person Account, not a household object.
- `OrderApi__Subscription__c` is the membership record ("Subscriptions track the activation, expiration, and renewal
  dates for all Customer Subscriptions"; Fonteva's 20Spring schema notes call this object "Membership" — Reported). Key
  fields: `OrderApi__Entity__c` picklist — "Select "Account" if the primary subscriber is the linked Account, otherwise
  select "Contact""; `OrderApi__Account__c` / `OrderApi__Contact__c` (required according to Entity);
  `OrderApi__Assignments_Allowed__c` ("Max number of assignments allowed … Rolling up from related Subscription Line's
  "Assignments Allowed""); `OrderApi__Active_Assignments__c`; `OrderApi__Is_Cancelled__c` — "When checked, the system
  will inactivate all related Subscribers and unassign any related benefits."; `OrderApi__Is_Suspended__c` —
  "Subscriptions can be suspended to prevent automatic payment … or to revoke benefits (Badges, etc...) granted by the
  Subscription Item."; `OrderApi__Billing_Contact__c` (text, derived from the Account or Contact "based on "Entity"");
  `OrderApi__Payment_Method__c`; `OrderApi__Subscription_Plan__c`; `OrderApi__Paid_Through_Date__c`;
  `OrderApi__Grace_Period_End_Date__c`.
- `OrderApi__Subscription_Line__c`: `Assignments_Allowed__c` — "If the Item "Enable Assignments" and "Restrict Number of
  Assignments" fields are checked, the Item "Maximum Number of Assignments" x the Line "Quantity" is set.";
  `Assignments_Available__c` = allowed − active.
- Item / Item Class: `OrderApi__Enable_Assignments__c` ("When checked, the Item can be assigned to Contacts indicating
  benefit or ownership over goods and services. Automatically calculated based on the parent Item Class "Type""),
  `Restrict Number of Assignments`, `OrderApi__Maximum_Number_of_Assignments__c`, `OrderApi__Require_Assignments__c` ("a
  user will be unable to complete a sale for an Item until all assignments available have been entered"), Item Class
  `Enable Assignment Inventory`.
- `OrderApi__Assignment__c` — "Assignments track Contacts assigned to purchases of Items". Fields: `Contact__c`
  (required), `Sales_Order_Line__c` (required source line), `Subscription__c`, `Subscription_Line__c`, `Term__c` (lookup
  to `Renewal__c`), `Assignment_Role__c`, `Is_Active__c` (default checked), `Is_Primary__c`, `Is_Unassigned__c` ("auto
  check to true when a Subscriber (Assignment) is marked as "Is Active" unchecked AND is related to a "Is Active"
  checked Subscription"), `Assigned_Date__c`, `Unassigned_Date__c`, `Receives_Email_Notifications__c`, plus snapshot
  copies of the contact's name, email, phone, title, company name and mailing address, and `EventApi__Seat__c`.
- `OrderApi__Assignment_Role__c` — "Configure custom roles for Contacts to be assigned to against "Assignable" Items."
  Fields: `Is_Default__c`, `Is_Primary__c` ("any Contact "Assignment" linked to the Assignment Role will be considered a
  "Primary Contact" for the "Assigned To Record""), `Receives_Email_Notifications__c`, `Item_Class__c`.
- Badges carry benefits: `OrderApi__Badge__c` — "Badges can enable Contacts for special Price Rules - as well as
  permission to read, update, or delete certain content types." `OrderApi__Badge_Workflow__c` `Type__c`: "Select "Line
  Contact" to assign the Badge to the purchasing Customer … Select "Assigned Contacts" to give the Badge to all "Active"
  assigned Contacts." (a "Line Account" option also exists). `Auto_Expire__c`: "the Badge will automatically unassign
  itself from the Contact record when the Assignment record becomes in active." `Badge__c.Assignment__c`: "the link to
  the "Assignment" record is used to auto-unassign the badge when the Contact has been "Unassigned"."
- Account-level badge inheritance: `OrderApi__Badge_Type__c.Enable_Badge_Flow_down__c` — "When checked and when the
  badge is assigned to an account, all contacts of that account, child accounts, and child account's contacts will
  inherit the benefits. Limited to only one level of child accounts." Account `OrderApi__Disable_Badge_Flow_down__c`
  turns it off per account. Contact `OrderApi__Badges__c` is a long-text field that "lists all the active badges …
  including all badges inherited from it's parent account or it's parent account through flowdown (when enabled)". The
  docs page "Badge Flowdown" (Reported, excerpt) says inherited badges do not get their own badge records on the child
  account or its contacts; they appear only in that Badges field.
- Relationships: `OrderApi__Connection__c` — "Connections allow you to relate one Contact or Account to other Contacts
  and Accounts in the database." Fields: `Contact__c`/`Account__c` (primary side),
  `Connected_Contact__c`/`Connected_Account__c`, `Connection_Type__c` ("Connection Type for the Primary Entity"),
  `Connection_Type2__c` ("Connection Type for the Connected Entity"), `Start_Date__c` (required), `End_Date__c`,
  `Termination_Reason__c`, `System_Created__c` ("Automatically checked when the Connection was created by a trigger as
  the result of a new Connection inserted"). `OrderApi__Connection_Type__c`: `Allow_Accounts__c`, `Allow_Contacts__c`,
  `Category__c`, `Active__c`. `OrderApi__Connection_Type_Mapping__c` maps a type to its reciprocal
  (`Mapped_Connection_Type__c`, trigger-created). No field on Subscription, Assignment, Badge or Badge Workflow
  references Connections.
- Account `OrderApi__Primary_Contact__c`: "The Primary Contact for the Account will auto-populate on all Account
  Transactions, Payments, etc... by default."; Contact `OrderApi__Is_Primary_Contact__c`.

**Reported behaviour (docs.fonteva.com excerpts, deprecated Confluence excerpts, AUSA chapter guide 2020).** "Enable
Assignments" on a subscription Item Class lets the buyer extend the subscription to others in their organization; at
least one Assignment Role must exist; "Assignment Roles determine who within the organization can manage the
subscription for assigned Contacts in the Community Site"; the option is "commonly used with Company or Organizational
Memberships". "Users that are the Primary Contact for their Account will see all subscriptions where the Account is the
purchasing entity." Assignments can be made manually on the Subscription, automatically through a sales order line,
through the shopping cart, or manually on the sales order line. AUSA's Fonteva guide for chapters reports family
memberships as a membership type whose name contains "Family", with "more than one subscriber … added as long as there
is an availability or slot open", and calls the practice "donated assignments/designees".

**Answers.**

1. Household: not an entity. The one-person Account is literally named "<Name> Household" by default (Verified), which
   is cosmetic. Multi-person coverage is a Subscription with assignment slots. Relationships between people are typed,
   dated, reciprocal `Connection` records with no documented tie to benefits (Verified).
2. A household cannot own a membership because there is no household; an Account (company or individual Account) or a
   Contact owns the Subscription (`Entity` = Account | Contact) (Verified).
3. Yes: an individual Contact purchases an Item whose class allows assignments; `Assignments_Allowed` = max × quantity;
   other people are added as Assignments (Verified mechanism; family use Reported).
4. Family relationships would be `Connection` records with custom Connection Types (e.g., Spouse ↔ Spouse, Parent ↔
   Child via the mapping object), dated with required start date and optional end date and termination reason
   (Verified). Nothing documents a family-specific type set.
5. Members of a covered group are added/removed as Assignments (activate/deactivate, `Unassigned_Date`); cancellation
   "will inactivate all related Subscribers" (Verified). Community-side management is by the contacts holding a role
   whose `Is_Primary` is set (Reported).
6. Named seats: each beneficiary is an Assignment row linked to a Contact, with a role and `Is_Primary` (Verified).
7. Materialised: Badge Workflow of type "Assigned Contacts" creates Badge records on each active assigned Contact;
   badges auto-expire when the assignment goes inactive (Verified). Account-level badge flow-down to child
   accounts/contacts is _not_ materialised; it is a computed text field (Verified) — so the two mechanisms differ in
   shape.
8. Dynamic evaluation through relationships: not documented. Badge flow-down evaluates the Account→child Account→Contact
   hierarchy one level deep; Connections are not used for eligibility (Verified absence).
9. Household donations/communications/addresses/history: nothing household-specific; Assignment rows snapshot the
   contact's name and mailing address at assignment time (Verified), which is a data-duplication choice rather than a
   shared-address model.
10. Limitations: no household; family = seat count on an item; Connection relationships are inert with respect to
    membership; badge flow-down limited to one level and invisible as records; assignment snapshot fields can drift from
    the Contact; one Primary Contact per Account.

**Payer vs holder.** Holder is the subscribing entity (Account or Contact); `Billing_Contact__c` is derived text; an
Account's Primary Contact auto-populates transactions (Verified). **Renewal/cancel/composition.** Assignments link to a
`Term__c` (Renewal) so seat membership is per term; `Is_Cancelled` inactivates all subscribers; `Is_Suspended` revokes
badges while keeping the subscription (Verified). **Entitlement explanation.** A covered person's Badge record carries
`Assignment__c`, so "why does this contact have member pricing" can be traced Badge → Assignment → Subscription
(Verified); inherited account badges cannot be traced to a record (Verified).

---

### 2. Nimble AMS (Salesforce)

**Primary evidence.** help.nimbleams.com articles: Managing Specific Affiliations, Manage the Primary Affiliation,
Affiliations, Affiliations in Community Hub, Affiliation Configuration, Information Flow Down, Create/Edit a Membership
Type, Memberships, Memberships with Community Hub, Membership Bundle Components Data Source, Winter '19 Permission
Changes (which lists package field API names).

**Model (Verified).**

- Individuals are Salesforce Person Accounts ("an individual constituent is represented by a person account record,
  which is Salesforce's B2C solution combining an account and contact record"). Companies are Accounts.
- Affiliation (`NU__Affiliation__c`) links a parent account to a child account. Documented fields (Winter '19 page):
  `NU__Account__c`, `NU__ParentAccount__c`, `NU__Role__c`, `NU__IsPrimary__c`, `NU__IsPrimaryContact__c`,
  `NU__IsCompanyManager__c`, `NU__DoNotFlowdownAddress__c`, `NU__StartDate__c`, `NU__EndDate__c`, `NU__Status__c`,
  `NU__StatusFlag__c`, `NU__RemovalDate2__c`, `NU__RemovalReason__c`. UI semantics: Role is multi-select; "An account
  can be affiliated to many accounts, but can only have one primary affiliation"; setting a new primary clears the old;
  Primary Contact "adds the individual's name and email to the company account"; Is Company Manager lets the individual
  act for the company in Community Hub; Status = Inactive sets Removal Date and keeps history (recommended over delete).
- Affiliations are not restricted to individual→company: the Information Flow Down article states "When both parent and
  child are individuals, Mailing Address and Other Address also flow down", so individual-to-individual parent/child
  affiliations exist. Affiliation types are configurable ("employer/employee, location/company, chapter/member, or
  create your own").
- Membership (`NU__Membership__c`) "represents a membership for a specific Account"; "Nimble AMS currently supports only
  one membership by default" (concurrent memberships need a custom solution). Membership Type (`NU__MembershipType__c`)
  fields include `Category` (Individual | Company | Both), `NU__FlowdownManagerClass__c` (None or
  `NU.StandardMembershipFlowdownManager2`; custom Apex allowed), `NU__AccountMembershipField__c` and
  `NU__AccountJoinOnField__c` (the Account lookups written when a membership is created), grace and reinstate periods,
  renewal type.
- Membership flow-down: "Membership flow down occurs for those membership types where Category is set to Company or Both
  and does not occur for Individual membership types." Membership and Join On flow "from company accounts to each
  primarily affiliated individual or company location account" and are reflected on the child's
  `Status Membership Flag`, `Member Type`, `Member`, `Lapsed`, `Member Thru`, `Lapsed On`. Triggers: primary affiliation
  added or changed; parent joins or renews. Flow-down does **not** occur when: the child already has a personal
  membership; the order process sets the new membership to Pending; the affiliation is added/updated but is not primary;
  "information on the child's primary affiliated parent is updated". Address flow-down (billing/shipping) follows the
  same primary rule with `Do Not Flowdown Address` as the opt-out. Asynchronous flow-down above a configurable threshold
  (default 1,000 children; batches of 200; emails on completion). "Changes to the Flowdown Manager Class will not
  automatically revert updates made by previously executed information flow down operations."
- Community Hub: individuals "can view, create, edit, or remove their affiliations"; an affiliation an individual adds
  becomes primary by default; one a company manager adds for an account that already has a primary does not; company
  managers manage individuals and locations on the Company Roster, see company membership options, renew and pay company
  balances. "If your organization does not flow down company membership benefits", a company manager without a personal
  membership sees "not a member" but can still pay the company's renewal. A company manager choosing a Company
  membership type gets the products "available to the user's primary affiliation instead" (Membership Bundle Components
  Data Source).

**Answers.**

1. Household: not documented anywhere in the help site (searches for household, family, spouse, dependent returned
   nothing). Structurally a household could only be an Account with individuals as primarily affiliated children
   (Inferred from the individual↔individual affiliation rule).
2. Any Account can hold a membership, so a household Account could hold one and flow it down if its type is Company/Both
   (Inferred; Nimble does not describe this use).
3. No documented way for an individual to cover others: Individual-category types never flow down (Verified), and an
   individual's membership only flows if the individual is a parent in a Company/Both type.
4. Family relationships would be affiliations with custom roles between person accounts, dated (Start/End), with Status
   and removal reason; they are parent→child, not symmetric; reciprocity is implied by the single record being visible
   from both sides (Verified structure, Inferred use).
5. Add/remove = create affiliation / set Inactive; staff or the person or the company manager can do it (Verified).
6. Relationship-derived: beneficiaries are the _primarily_ affiliated children of the holder; no seat count is
   documented (Verified).
7. Materialised: the standard flow-down manager writes membership fields onto each child Account (Verified).
8. Not dynamic: entitlement is read from the copied fields; a child's state is only refreshed on the listed triggers,
   and explicitly not when the parent's own information is edited (Verified).
9. Addresses flow down (billing/shipping; mailing/other between individuals) unless opted out; nothing on household
   donations or shared communication history (Verified/Unavailable).
10. Limitations: one primary affiliation per account (a person cannot be primary to both employer and family); one
    membership per account by default; materialised fields with no provenance; flow-down skipped for anyone with a
    pre-existing personal membership; behaviour on company membership lapse/cancel is not documented in the flow-down
    article; thresholds and batch jobs are needed to keep large hierarchies consistent.

**Payer vs holder.** Holder = the Account with the Membership; payment via order Bill To, "Bill Me" payment URL
shareable with "a trusted person at their company", and company managers paying company balances (Verified/Reported).
**Renewal/cancel/composition.** Parent renewal re-flows; adding/changing a primary affiliation flows the current
membership; removal of a primary affiliation without replacement is documented only for addresses ("does not occur"),
not for membership fields (Verified gap). **Entitlement explanation.** The child Account's `Member Type`/`Member Thru`
show _what_, not _from which parent_; the primary affiliation is the implicit reason (Inferred).

---

### 3. MemberSuite (GrowthZone, formerly Community Brands)

**Primary evidence.** "MemberSuite By GrowthZone — User Guide 2024 / REST API Documentation" PDF (growthzone.com,
revised March 2024), plus Higher Logic's MemberSuite integration pages. `api.membersuite.com` and `rest.membersuite.com`
Swagger UIs are JavaScript shells; `community.membersuite.com` articles were not retrievable.

**Model (Verified from the API guide).**

- CRM module "stores individuals, organizations and their contact data". Objects named in MSQL: `Individual` (fields
  include `PrimaryOrganization`, `Status`, custom `__c` fields), `Membership` (`Owner`, `Type`, `PrimaryChapter`,
  `JoinDate`, `ExpirationDate`, `ReceivesMemberBenefits`), and a `ChildEntities` search object with `Parent`,
  `IsIndividual`, `Roles` and a nested `Individual`.
- "Membership status: ReceivesMemberBenefits — This field appears on Individual, Membership and whoami responses and is
  the standard way to determine member status." Values: true = "Active membership" (Member); false = "Membership expired
  and dropped" (Non-member); null = "Never held a membership" (Non-member).
- Organization-to-individual benefit derivation is expressed as a query, not a copied flag. The guide's directory
  example:
  `FROM ChildEntities WHERE (Parent = '{organizationId}' AND IsIndividual = 1 AND Individual.PrimaryOrganization.Membership.ReceivesMemberBenefits = 1 AND Individual.Status IN (...))`.
  That is, an individual's member standing via their organization is evaluated through
  `Individual.PrimaryOrganization.Membership`.
- Memberships are owned by either record type (`Owner.Name`, "membership records matching an MSQL statement").
- Higher Logic (Reported): saved searches must include a "MemberID or Child Individual ID"; its sync treats "all Member
  Types in MemberSuite where "ReceivesMemberBenefits" is true" as member security groups. MemberSuite's CRM page says
  staff can "define and track relationships between organizations and individual records" (Reported, marketing).

**Answers.**

1. Household: Unavailable. Public material shows Individual, Organization, parent/child entities and a
   primary-organization link only.
2. Unavailable for households; organizations and individuals own memberships (Verified).
3. Unavailable (no public seat/sub-member documentation).
4. Relationship types are configurable (Reported: "set up as many custom fields, relationship types, etc. as needed");
   direction, reciprocity and dating are not publicly documented.
5. Child entities of an organization are managed through the console/portal (functional behaviour only; Unavailable in
   detail).
6. Relationship-derived through `PrimaryOrganization`; `Roles` exist on child entities (Verified fields, semantics
   Inferred).
7. Dynamic for the organization path: the documented way to find covered individuals is a join through
   `PrimaryOrganization.Membership.ReceivesMemberBenefits` (Verified). The individual's own `ReceivesMemberBenefits` is
   a stored tri-state on their own membership (Verified).
8. Yes, by query (Verified) — though only along the single primary-organization link.
9. Unavailable.
10. Limitations: a single `PrimaryOrganization` pointer is the only documented inheritance path; member status is a
    tri-state that distinguishes "dropped" from "never" but says nothing about _why_; documentation is behind customer
    logins.

**Payer vs holder / renewal / entitlement.** Not publicly documented beyond `Owner` and `ExpirationDate`. Entitlement
explanation reduces to "ReceivesMemberBenefits is true on the individual, or on their primary organization's membership"
(Inferred from the two query patterns).

---

### 4. GrowthZone (ChamberMaster lineage)

**Primary evidence.** helpdesk.growthzone.com articles (Add a New Membership Type; Contact Relationship Types; Manage
Parent/Child Relationships; Parent/Child Relationship — Considerations; Parent/Child Use Case: Child Inherits Parent
Membership; Membership Report; Change Organization Membership Billing Contact) — fetched through a reader proxy or
quoted from search excerpts; the "Managing your Memberships" and "Setting up Your Membership Types" onboarding PDFs
(text extracted locally); the GrowthZone Curated API page (`documentation.growthzoneapp.com/CuratedApi.html`).

**Model (Verified).**

- Contacts are individuals or organizations (API `SystemContactTypeId`; endpoints `/api/contacts/person/{id}` and
  `/api/contacts/OrgGeneral/{id}`).
- Membership type **Owned By**: "Business" (invoices billed to the organization) or "Individual" (invoices billed to the
  individual); the setting also controls where the type can be selected.
- Membership type **Inheritance Type**: "Full Inheritance" — "Choose Full Inheritance if staff should benefit from the
  membership"; "Owner Only" — "All individuals (including the primary contact) will be listed as non-members and would
  not benefit from things that you may offer based on membership type (i.e. event registrations)". Onboarding PDF: "If
  the membership type has been setup as Full Inheritance, all individuals associated to the contact will be activated.
  If the membership types is set to "Owner Only" only the organization will be activated." and "You can manually adjust
  which staff members inherit the membership, by selecting the Related Contacts option".
- Contact Relationship Types: Organization-to-Organization (system types **Location** and Custom), Person-to-Person
  (system type Custom only; assigned in "Related Individuals" on the individual's profile), Person-to-Organization
  (system types Account Manager, Sales Representative, Project Manager, Customer Service, Ambassador, Trainer, Mentor,
  Custom). "Only the Location system type affects membership": a child organization inherits the parent's membership as
  "one membership with multiple records attached", the parent remains owner; "memberships can only be owned by the
  parent agency, and branches inherit the membership from the parent contact".
- Considerations article: for a child with its own membership the parent may be named billing contact — "invoices will
  still "belong" to the child, but will be delivered to the parent".
- API: individual's `OrganizationsAndRoles` carries `RoleId`, `RoleStartDate`, `RoleEndDate`, `IsContact`,
  `DisplayOnWeb`; memberships carry `OwnerContactId`, `MembershipContactId`, `MembershipStatusTypeId`, `StartDate`,
  `ExpirationDate`, `GracePeriodExpirationDate`; change events expose `BillingContactId` and
  `ResponsiblePartyContactId`; status lookup values are NonMember, PendingApproval, Active, Trial, Courtesy, Dropped,
  Expired, Lead, Prospect, Suspended.
- Event discounts "validate on a person-by-person basis and will only apply to contacts with active memberships"
  (Reported excerpt of the registration-options article), i.e. an inheriting individual carries an active membership
  status of their own.

**Answers.**

1. Household: no concept. Individuals relate to organizations through roles, and to each other only through Custom
   person-to-person relationship types that carry no membership meaning (Verified).
2. No household; an organization or individual owns the membership (Verified).
3. Not documented. An "Individual"-owned type has no inheritance option described; a reviewer-style workaround (a family
   as an organization) is not in the documentation (Inferred gap). Spouse event registration is handled by letting a
   member buy a member-only registration for a guest, which GrowthZone "cannot force" otherwise (Reported).
4. Person-to-person relationships: Custom types only; the API shows dated organization roles
   (`RoleStartDate`/`RoleEndDate`) but no dates are documented for person-to-person links; direction/reciprocity not
   documented (Verified/Unavailable).
5. Individuals are related to the organization on the profile; staff "activate" or exclude them via Related Contacts
   (Verified, functional behaviour only).
6. Relationship-derived with manual override: all related individuals under Full Inheritance, or a hand-picked subset
   via Related Contacts (Verified).
7. Materialised: individuals are "activated" and carry their own membership status (Verified wording; storage not
   documented).
8. Not dynamic beyond the activation step; only the Location org-to-org type changes membership (Verified).
9. Billing contact is explicit per membership and its email comes from that contact's default email; CC addresses for
   recurring billing; nothing on shared addresses or household giving (Verified/Unavailable).
10. Limitations: "everything is labeled Contact" (Capterra reviewer); person-to-person relationships are inert;
    inheritance is a two-value switch per membership type plus manual exceptions; no family/household construct;
    parent/child inheritance requires the specific "Location" system type.

**Payer vs holder.** Owner (Business or Individual) determines who is invoiced; a separate Billing Contact per
membership; Responsible Party in the API; a parent can be billing contact for a child's own membership (Verified).
**Renewal/cancel/composition.** Drop/upgrade/downgrade are per membership; changing the billing contact updates
scheduled billings (Verified). **Entitlement explanation.** An individual is a member because they are related to a
Full-Inheritance owner (or were hand-activated); the API exposes `OwnerContactId` vs `MembershipContactId`, which is the
only trace (Verified fields, Inferred use).

---

### 5. YourMembership (Momentive/Community Brands)

**Primary evidence.** YM REST metadata page `ws.yourmembership.com/jsv/metadata?op=People` (Verified); Higher Logic's YM
REST integration guide (Verified for the status rule it applies); association-authored guides built on YM (Starkweather
"Assigning a sub-account to a master member" 2015, NWRA, NADCO, GMIS, ACUPA) and reviews (Reported). The official API
reference at `api.yourmembership.com/reference/2_30/` returns 403; the "YM AMS API Overview" PDF is image-only.

**Model.**

- People records carry `MasterProfileID` ("This key replaces the 'MasterID' key"; read-only; "This field cannot be
  updated directly through this API" — linking is done through the `PeopleBulkAttach` endpoint), `SubAccounts` (int),
  `CanDetachSubAccounts` (bool), `MembershipExpiresDate`, `MembershipEffectiveExpiresDate`, `Membership` (string),
  `MemberTypeCode`, `Approved`, `Suspended`, `EmployerName` (Verified field names; descriptions mostly absent).
- Higher Logic derives "Is Member" as `Approved && !Suspended && MembershipExpiresDate > now`; it maps YM "Parent
  Contact Id" to null and hard-codes `Is Organization = false`, i.e. the export has no organization hierarchy
  (Verified).
- Master/sub-account behaviour (Reported, consistent across four association guides): the master profile has an
  "Additional Seats" number ("If unlimited, type in 999"); staff add subs from the Master/Sub tab via member lookup, or
  members self-register through a "sub-accounts direct link"; sub-accounts may need admin approval; masters can edit,
  detach ("completely removing them from your master account"), suspend ("switch sub-accounts for that calendar year"),
  view and "sign in as" a sub-account to register them for events; a detached sub-account "will automatically default to
  "free member" status" (EETC FAQ excerpt). Starkweather frames subs as "employees, chapter members, or branches".
- Reviewer complaint (G2): "Can't pull sub-accounts of ONLY current members with their master."

**Answers.**

1. Household: none. The master/sub-account hierarchy is a one-level person-to-person (or org-to-person) ownership link
   keyed by `MasterProfileID` (Verified field; usage Reported).
2. No household entity; the master profile holds the membership and the seats (Reported).
3. Yes in effect: any master profile with "Additional Seats" can attach other people; the guides use it for
   organizations, but nothing restricts it to companies (Reported; Inferred).
4. No family relationship types are documented; the only link is master→sub, undated in the metadata
   (Unavailable/Verified absence of dates).
5. Attach by lookup, bulk attach, or invite link; detach; suspend (Reported; `PeopleBulkAttach` Verified by name only).
6. Named seats: each sub-account occupies one of the master's seats (Reported).
7. Flag/derived date: subs carry `MembershipEffectiveExpiresDate`, which differs from `MembershipExpiresDate`,
   consistent with an inherited effective expiry (Verified field; semantics Inferred).
8. Not dynamic beyond the single master link.
9. Unavailable.
10. Limitations: one master per person; sub-account reporting is weak (review); detaching silently demotes to "free
    member"; the API cannot set the master link directly; documentation is behind the vendor's login.

**Payer vs holder.** Master pays; subs do not (Reported). **Renewal/cancel/composition.** Suspend to free a seat for the
year; detach to remove (Reported). **Entitlement explanation.** "Member because approved, not suspended, and (effective)
expiry in the future" — the master link is the only provenance (Verified/Inferred).

---

### 6. Novi AMS (QuickBooks-based)

**Primary evidence.** help.noviams.com: Overview of Member Type Categories; Create a New Member Type; Member Types that
Limit Company Member Benefits to Certain Individual Members; Company Membership with Seat-Based Dues (backend admin
workflow; frontend member experience); Deep Dive: Membership Status Types; Dues Paying Member; Create a New Company or
Person; Change a Person's Parent Member; Editing Individual and Company Parent Record Settings; the Novi API reference
(Member Types object; members endpoints listed) and search excerpts of the "Member Benefits Flow" article (which 404s at
both URLs).

**Model (Verified).**

- Records are Companies or People. "Family relationships are created when a member record is assigned to a parent on the
  Details tab of the member record. A record can only have one parent." Children can be people or sub-companies;
  grandchildren exist. "A record's family tree is independent of member benefits."
- Member Type Category (API `ForCompanies` boolean): **Individual People** ("the membership belongs to a person, not a
  company"; benefits can flow to child records "such as assistants, staff, or sub-companies", unlimited or capped; dues
  fixed; dues can be billed to a related record such as a parent or grandparent; membership can depend on a related
  record); **Companies** (benefits flow to related people/companies, unlimited or limited; with a limit, "people with
  Primary, Billing, or Management Access choose recipients in Member Compass > People tab"; dues fixed regardless of how
  many inherit; billable to a parent company; can depend on a related member — e.g. a Property member needs a current
  Management Company parent and "becomes Pending if that parent expires"); **Companies with Individual Seat Dues**
  (benefits to children or grandchildren, seat restrictions with min/max, seat types, tiered per-seat dues rules,
  management access adds/removes seats in Member Compass, billable to parent/grandparent as default billable party,
  automatic seat mapping when switching seat-based types).
- Benefit flow is configured per member type on the Member Benefits tab ("Dues Paying Member's Children (People)"; a
  gear icon sets "the maximum amount of people that can inherit benefits from any member of this member type").
- Statuses: Current, Expired, Grace Period, Prospect, **Inheriting** ("a non-dues-paying record that receives benefits
  from a dues-paying member in its family tree"; the record shows "membership via"), Pending (awaiting approval, or own
  membership without a current qualifying dependent member), Non-Member. Inheriting records should have member type
  "None Designated"; "The effective member type for an inheriting record will display at the top of the member record
  stage." Novi clears an inheriting record's own member type, member since and expiration; dates come from the provider.
  "If a parent company expires, inheriting staff become Non-Members." "Dues invoice payment status does not
  automatically affect membership status." Benefits flow "from a dues-paying member in the family tree, which doesn't
  have to be a parent."
- Limited-benefit companies: admin flips the Member Benefits column (Non-Member ↔ Inheriting); backend shows total and
  unassigned seats; frontend "Assign Seat"; "Members can't remove benefits from someone while keeping that person on the
  People list. They must use the "Remove Person" link … This safeguard prevents misuse, such as moving benefits between
  people to get member pricing repeatedly." People with their own individual memberships keep them when removed from the
  family and "do not count toward the company's seats"; a Prospect with an individual type must have it removed before a
  seat can be assigned; "Grandchildren can receive a seat only if their parent company doesn't have its own membership
  with benefits flowing to children."
- Seat-based dues: seats added mid-term bill only seat rules; renewal-window purchases follow an "Additional Seats"
  setting ("Both years (proration may apply)" / "Second year only"); unassigned seats renew per seat-type setting;
  reducing total seats or removing seats from open invoices is admin-only; an expired company must have Total Seats = 0.
- Parent change: "If the person had management access with their previous parent company, that access doesn't carry over
  … The same applies to Primary Contact and Billing Contact settings." Parent Member Access setting decides whether
  individuals may change parent from Member Compass; company parents are staff-only; changes appear in the Audit Log.
  Address inheritance from parent is a separate per-child option.

**Answers.**

1. Household: not a named construct; the "family tree" is a strict single-parent tree of Company/Person records, and
   Novi's examples are employer/staff and management-company/property (Verified). A household would be a parent record
   (a company-type record or an individual) with people as children (Inferred).
2. A parent record of any type can hold the dues-paying membership and flow benefits down, so a "household" record could
   if created as a Company or Person (Inferred from Verified categories).
3. Yes: an **Individual People** member type can flow benefits to the individual's child records, capped or unlimited,
   without any household record (Verified). Novi does not label this "family", and its examples are assistants/staff.
4. Only parent→child; no relationship types, no reciprocal records, no start/end dates on the link; the Audit Log
   records parent changes (Verified).
5. Set/clear the Parent Member field (staff or, if allowed, the person in Member Compass); seats assigned/unassigned per
   person; "Remove person and unassign seat" (Verified).
6. Mixed: relationship-derived by default (all children) or named seats when limited/seat-based; seat types exist
   (Verified).
7. Computed status with materialised presentation: the child's status is "Inheriting" with an _effective_ member type
   and dates copied from the provider; Novi recomputes on parent expiry/approval (Verified; storage not documented).
8. Partly dynamic: status follows the tree and the provider's status, and a member type can be made _dependent_ on a
   qualifying related record (Pending when unmet) (Verified).
9. Addresses can be inherited from the parent per child; billable party can be parent/grandparent; nothing on household
   donations or shared communication history (Verified/Unavailable).
10. Limitations: one parent per record (no person in two families); tree-only inheritance; benefit limits mean seat
    juggling is guarded by removing the person; grandchild rules; seat reductions are staff-only; QuickBooks is the
    ledger so refunds route through QBO.

**Payer vs holder.** Holder = dues-paying record; "default billable party" may be a parent or grandparent (Verified).
**Renewal/cancel/composition.** Non-Renew & Credit drops to Expired or Prospect; parent expiry demotes inheritors to
Non-Member; seat purchases prorate inside the renewal window only (Verified). **Entitlement explanation.** Strongest in
this set: the record shows "membership via <provider>" and an effective member type (Verified).

---

### 7. Impexium

**Primary evidence.** The Impexium connector reference on learn.microsoft.com (Power Automate), which documents
operations, parameters and response schemas of Impexium's API (Verified); the ACC "Impexium Set-Up Steps for Official
Representatives" PDF (an association's member guide — Reported). Impexium's own help portal is customer-only.

**Model (Verified from the connector schemas).**

- Customers are Individuals and Organizations (`customerType`). Individuals have `primaryOrganization`; Organizations
  have `parentCompanyId`.
- Relationships: `Add Relationship to Individual` and `Save Relationship for Organization` take `relationshipName`,
  `relatedToCustomerRecordNumber`, `reciprocalRelationshipName` (all required), `isPrimary`, `isReciprocalPrimary`,
  `startDate`, `endDate`, `title`. `RelationshipData` returns `relationshipName`,
  `relatedToCustomer {id, customerType, name}`, `reciprocalRelationshipName`, `isPrimary`, `startDate`, `endDate`.
  `RelationshipTypeData` (from `List of Customer Relationships`) has `name`, `reciprocalRelationshipName`,
  `relationshipType`, `allowPrimary`, `canPurchaseForOrganization`, `canManagePAC`, `canManageOrganization`. A
  `CustomerRelationshipUpdatedPayload` webhook carries Relationship, ReciprocalRelationship, StartDate, EndDate.
- Memberships: `MembershipData` has `membershipType`, `membershipTypeId`, `code`, `joinDate`, `effectiveDate`,
  `expireDate`, `graceExpireDate`, `terminateDate`, `inheritedMembershipBenefits` (boolean), `renewalUrl`,
  `membershipUniqueId`, `termsList[startDate,endDate]`. Both `Get Individual Active Memberships` and
  `Get Organization Active Memberships` return arrays of this type; `MembershipTypeTerminatedPayload` carries
  `TerminateDate` and `TerminateReason`.
- No household operation or schema exists in the connector (Verified absence).

**Reported behaviour (ACC guide).** "In our system, a relationship type outlines who an individual is in a relationship
with their employer. For ACC purposes it's the company that is the membership holder, and benefits flow down from it to
all employees in varying degrees, determined by your relationship type." Individuals check that they are flagged
"RECEIVES MEMBERSHIP BENEFITS" on their Overview tab. Relationship types in that tenant: Official Representative (one
per company, edits the organization and other employees' profiles), Additional Contact (up to 4, more purchasable;
appears in the directory), Employee (unlimited; default). Representatives add individuals from Organization →
Individuals → "Add New Individuals"; removals go through staff.

**Answers.**

1. Household: Unavailable/absent. Relationships are typed, reciprocal, dated, primary-flagged links between customers; a
   family could be modelled as individual↔individual relationships but nothing documents benefits following them
   (Verified structure; Inferred).
2. Unavailable.
3. Unavailable (no seat or dependent construct in the API).
4. Family relationships would be relationship-type pairs (name + reciprocal name) with start/end dates and `isPrimary`
   on each side (Verified mechanism; no family types documented).
5. Via the relationship operations or the organization's Individuals tab (Verified/Reported).
6. Relationship-derived with role gradation: the relationship type determines degree of benefit (Reported) and carries
   capabilities (`canPurchaseForOrganization`, `canManageOrganization`) (Verified).
7. Materialised at the individual: an individual's membership entry with `inheritedMembershipBenefits = true` (Verified
   field; semantics consistent with the "RECEIVES MEMBERSHIP BENEFITS" flag — Inferred).
8. Not documented as dynamic; the inherited flag is stored on the individual's membership data (Inferred).
9. Addresses, phones and emails are per customer with `showInDirectory` flags; nothing household-level
   (Verified/Unavailable).
10. Limitations: benefit inheritance is tied to the single `primaryOrganization`; relationship types double as
    permission carriers (purchase/manage) which is useful but couples CRM and authorisation; no household; documentation
    is private.

**Payer vs holder / renewal / entitlement.** Holder is the organization in the ACC configuration; a relationship type
with `canPurchaseForOrganization` may pay (Verified capability). Termination has a reason code and webhook (Verified).
Entitlement explanation: the individual's membership row says `inheritedMembershipBenefits`, but not from which
organization or relationship (Verified field; gap Inferred).

---

### 8. Protech UX 365 (Microsoft Dynamics 365)

**Primary evidence.** The public UX 365 Help site (`ux365help.ptassoc.com`), a single very large page downloaded and
searched locally (Verified).

**Model (Verified).**

- Dynamics Accounts and Contacts. "Each account record can have one parent account record, multiple child sub-account
  records, and multiple child contact records. Similarly, each contact record can have one parent (contact or account
  record) and multiple child contact records." So a contact may be the parent of other contacts.
- Per-record child settings: **Use Parent Price Level** — "the child record will inherit the parent record Price Level,
  Currency, and Member flag settings. As changes to the parent's Price Level occur, these changes will automatically
  flow to the child record … Changing the value from Yes to No in the child record does not automatically update these
  fields." If the child has its own eligible primary renewal billing record (Primary = Yes, Cycle State = Current,
  Benefit Status New/Renew/Reinstate) the system updates from that instead; if the parent is removed the setting reverts
  to No and "the child record's Price Level and Member flag must be set manually". **Use Parent's Address** (primary and
  secondary, flows on save, reverts when parent removed). **Bill To Parent** — "set the parent as the default Bill To
  for all invoices created for that child record"; reverts to No when the parent is removed.
- Membership is a renewal billing record attached to an account or contact, created from a renewal benefit and rate; the
  member flag and price level drive member pricing.
- **Benefit recipients**: "A benefit recipient record is used to determine which contact or account records are
  associated with a renewal billing record to receive a particular benefit. For example, assume that a corporate
  membership rate includes five copies of a monthly publication … you can designate the five specific contacts." The
  rate's **Number Benefit Recipients** (≥1) creates that many placeholder records (defaulting to the account's primary
  contact or the contact's own name) which staff then assign; recipients are copied forward at renewal; excess
  recipients can generate an "additional benefit recipient charges" invoice line;
  `PARenewal.AdditionalBenefitRecipientsCreate` controls creation of placeholders on renewal; an optional Benefit
  Recipient Type field has no billing logic; a "Re-calculate Benefit Recipients" action exists. "Caution: There is no
  logic that prevents you from manually creating additional benefit recipient records … or deleting existing benefit
  recipient records. All updates to the benefit recipient records must be managed manually." And decisively: "Benefit
  Recipients do not confer membership status or member pricing. It is used primarily for extending member benefits, such
  as a publication, to a defined number of benefit recipients."
- No occurrence of "household", "spouse" or Dynamics "Connection" in the help text (Verified absence).

**Answers.**

1. Household: not a construct; the only grouping is the single-parent hierarchy of accounts and contacts (Verified).
2. A parent account or parent contact can hold the renewal billing record; children inherit the Member flag through Use
   Parent Price Level — so a "household" would be whichever record is made the parent (Inferred from Verified
   mechanics).
3. Yes, structurally: a contact can be parent to other contacts and pass Member flag/price level to them (Verified
   mechanics; no family example in the help).
4. Only parent→child; no relationship types, no dates (Verified).
5. Set/clear the Parent field and the three Use-Parent switches per child; benefit recipient rows are added/deleted by
   hand (Verified).
6. Two different mechanisms: relationship-derived for member status/pricing (parent link + switch) and named
   placeholders for countable benefits (benefit recipients) (Verified).
7. Materialised flags (Member flag, Price Level) that auto-flow while the switch is Yes; placeholder records for
   benefits (Verified).
8. Not dynamic: inheritance is a copied flag; switching off or removing the parent leaves stale values that "must be set
   manually" (Verified).
9. Address sharing via Use Parent's Address; Bill To Parent for invoices; galley/publication delivery via benefit
   recipients with per-recipient address override and "Opt Out Publication"; nothing on household donations or shared
   history (Verified).
10. Limitations: two disconnected notions of coverage (member flag vs benefit recipient); manual recipient maintenance
    with no guard rails; single parent; stale flags after toggles; the count of recipients is tied to the _rate_, so
    changing membership level changes entitlement counts only at renewal.

**Payer vs holder.** Holder = owner of the renewal billing record; Bill To Parent redirects invoices (Verified).
**Renewal/cancel/composition.** Recipients copy forward; excess recipients are charged at renewal or on "Recalculate";
termination is a renewal-billing process (Verified). **Entitlement explanation.** None beyond "Use Parent Price Level =
Yes"; benefit recipients are explicit rows, so publication entitlement is traceable but member pricing is not
(Verified/Inferred).

---

### 9. Naylor AMS (Timberlake Membership Software)

**Primary evidence.** "Naylor AMS API Developer Documentation" v1.0 PDF (Verified for the API surface); the Naylor AMS
demo site's membership application page (one customer's configuration — Reported); reviews (Reported).

- API: `GetBasicMemberInfo` returns name, address, phone, `MemberType`, `MemberTypeID`, `OrganizationName`,
  `OrganizationID`; `GetMemberInfo` adds `MemberGroup` entries (`GroupKey`, `GroupName`, `GroupType`, `MemberRole`,
  `SinceDate`, `StartDate`, `EndDate`); `GetAllOrganizations`, `GetOrganizationInfo`, `GetCommunityGroupMembers`. There
  is no parent, master, sub-member or relationship field anywhere in the documented responses (Verified absence).
- Demo site: firm-level membership types where "The entire law firm joins the ALFN and all employees may enjoy member
  benefits", and an associate type that requires its parent firm "must first be a member" (Reported; customer
  configuration).
- Reviews: with firm-based membership "to update the records I have to update the firm record and all the individual
  records associated with it. The system cannot do a "universal" update"; add-on renewals need manual handling
  (Reported).

**Answers.** 1–9 Unavailable except: individuals carry a `MemberType` and an `OrganizationID` (Verified), so
organization coverage is functionally a per-individual member type plus an organization pointer (Inferred); dated group
roles exist for committees/groups, not for family (Verified). 10. Limitation (Reported): no cascading update from firm
to individuals.

---

### 10. Wild Apricot bundles (Personify)

**Primary evidence.** The official Admin API OpenAPI document on SwaggerHub (v7.24.0) (Verified); help articles "Bundle
membership levels" and "Bundle coordinator guide" (Verified; the "Membership bundles" overview article 404s); the
vendor-engineer gist for creating bundles via API (Verified code); wishlist forum threads (Reported).

**Model (Verified).**

- `MembershipLevel`: `Type` enum `Individual | Bundle` ("Indicates if membership level is individual or bundle"),
  `BundleMembersLimit` ("Maximum number of bundle members. Empty for individuals."), `MembershipFee` ("Price … for
  RenewalPeriod"), `RenewalPeriod {Kind, StartFromJoinDate, Dates, AutomaticRecurringPayments}`,
  `MemberCanChangeToLevels`.
- `Bundle`: `Id`, `Email` ("Email of bundle administrator"), `Administrator` (link), `MembershipLevel` (link),
  `ParticipantsCount`, `SpacesLeft`, `Members[]`. Endpoints `GET /accounts/{accountId}/bundles?levelId=` and
  `GET …/bundles/{bundleId}`.
- `Contact`: `MembershipLevel` (absent for non-members), `MembershipEnabled`, `Status` enum
  `Active | Lapsed | PendingNew | PendingRenewal | PendingUpgrade`, `IsAccountAdministrator`, `FieldValues`. Bundle
  membership is set through system fields: `"Member role"` = "Bundle administrator" or "Bundle member", and
  `"Bundle ID"` on members (CreateContact description; gist confirms the payloads and that the Bundle ID is read back
  from the administrator's `FieldValues`).
- Help: a bundle level has one fee, "the total fee for the bundle", invoiced only to the coordinator; "All members in a
  bundle share one renewal date and policy"; the applicant becomes coordinator and the bundle is named after them;
  coordinators add only contacts whose email is not already a member, can archive but not remove members, can switch the
  whole bundle to another bundle level, and can move to an individual level only when the bundle is empty; admins can
  add existing contacts, reassign the coordinator, remove/suspend members; "Members cannot join a bundle themselves or
  switch levels while in one"; the directory links bundle members to the coordinator unless the privacy option is turned
  off; "Bundles are separate from member groups".
- Forum (Reported): staff state that administrator and members must be on the same level; users want family bundles with
  different levels per person, note "administrators end up being charged for the business they manage", and that one
  email per contact forces separate records for family members; the vendor grouped these under wishlist items (family
  membership, bundle pricing).

**Answers.**

1. Household: the bundle is a collection keyed to the administrator contact; it is not a constituent (Verified).
2. No: the administrator contact holds the level; the bundle object has no fee or status of its own (Verified).
3. Yes: any individual applying for a Bundle-type level becomes coordinator and adds people up to `BundleMembersLimit`
   (Verified).
4. No relationship types; membership roles only ("Bundle administrator"/"Bundle member"); undated (Verified).
5. Coordinator adds new/non-member contacts; archive; admin removes/suspends/reassigns (Verified).
6. Named seats: members are listed on the bundle and carry the Bundle ID (Verified).
7. Materialised: each member is a full contact with the bundle level, `MembershipEnabled`, and the shared status/renewal
   date (Verified).
8. No dynamic evaluation (Verified absence).
9. Communications are per contact; the coordinator alone is invoiced; directory linkage is a privacy setting; nothing on
   household donations (Verified).
10. Limitations: same level for all; one renewal date/status for all; members cannot self-manage or change level; one
    bundle per person; administrator pays for everything; forum pressure for family-specific behaviour has been open for
    years (Reported).

**Payer vs holder.** Coordinator is both (Verified). **Renewal/cancel/composition.** One renewal for the bundle; members
notified after it renews; coordinator change requires an admin (Verified). **Entitlement explanation.** "Member role" +
"Bundle ID" on the contact (Verified) — answerable but only one hop.

---

### 11. Glue Up

**Primary evidence.** support.glueup.com articles (blocked by Cloudflare to a non-browser client; content taken from
search excerpts — Reported), plus chamber-authored user guides (British Chamber Singapore 2020, Jersey Chamber)
(Reported). Functional behaviour only.

- Corporate membership types carry a member limit set per type; "some details (e.g., membership duration, price
  configurations, and member limits) cannot be directly edited" after creation — a new membership-type version is
  required.
- One **primary member** per corporate membership ("the main person for managing a company's corporate membership"); an
  **admin contact** is "a second contact point listed in corporate membership" with no access. Primary members
  add/delete members, transfer the primary role, resend invitations; "The number of spaces available will depend on your
  membership category"; invited people are "automatically added to your company membership where space is available";
  removed members receive an automated notification; non-primary members edit only their own profile; extra seats are
  purchased as "add-on requests" processed by staff, pro-rated; staff "member validation" reviews invited members.

**Answers.** 1. Household: none; corporate membership = company + primary member + seats (Reported). 2. No household. 3.
Not documented for individuals; corporate types only. 4. No relationship types documented. 5. Invite/remove by the
primary member; staff validation (Reported). 6. Named seats up to the category limit (Reported). 7–8. Functional
behaviour only; members under the company are shown as members in the directory (Reported). 9. Unavailable. 10. Limits
are frozen into membership-type versions; single primary member; add-on seats are a staff workflow.

---

### Patterns

**P1 — Holder is an organization-like record; coverage is a one-level parent→child link.** Nimble (primary affiliation
parent→child), Novi (single Parent Member), Protech (single parent account/contact), GrowthZone (organization roles;
Location org-to-org), MemberSuite (`PrimaryOrganization`), Impexium (`primaryOrganization`), YourMembership
(`MasterProfileID`), Wild Apricot (`Bundle ID`). Every product constrains the beneficiary side to **one**
primary/parent/master, which is exactly what breaks for a person who belongs to an employer _and_ a household.

**P2 — Named seats on the purchased item.** Fonteva Assignments (quantity × maximum, roles, `Is_Primary`, per-term),
Novi seats (seat types, per-seat dues, min/max), Protech benefit recipients (count on the rate, placeholders, overage
charges), Wild Apricot `BundleMembersLimit`/`SpacesLeft`, Glue Up spaces per category, YM "Additional Seats". Seats are
the only mechanism any product offers for "an individual covers several people" (Fonteva, Novi Individual People, Wild
Apricot, YM).

**P3 — Materialised beneficiary state with weak or no provenance.** Nimble copies Member Type/Member Thru onto the child
account; Protech copies Member flag/Price Level; GrowthZone "activates" individuals; Impexium stores
`inheritedMembershipBenefits` on the individual's membership; YM stores an effective expiry. Only Novi ("membership
via", effective member type) and Fonteva's seat path (Badge → Assignment → Subscription) let a reader see _why_.
MemberSuite is the only product whose documented pattern is a query through the relationship
(`Individual.PrimaryOrganization.Membership.ReceivesMemberBenefits`).

**P4 — Relationship records that are typed, dated and reciprocal exist (Fonteva Connections, Impexium Relationships,
Nimble Affiliations) but are not what drives coverage,** except Nimble's `IsPrimary` affiliation and Impexium's
relationship type (Reported "varying degrees"). GrowthZone's person-to-person types are explicitly inert. Impexium is
the only one whose relationship _type_ carries behaviour (`canPurchaseForOrganization`, `canManageOrganization`,
`allowPrimary`).

**P5 — Manager/role on the covering side.** Nimble Is Company Manager / Primary Contact; Fonteva Assignment Role
`Is_Primary`; Novi Primary/Billing/Management Access; Glue Up primary member; Wild Apricot coordinator; Impexium
relationship-type capabilities; GrowthZone primary and billing contacts. All are per holder, usually single.

**P6 — Payer separated from holder only as a billing pointer.** Novi default billable party (parent/grandparent),
Protech Bill To Parent, GrowthZone billing contact (invoice "belongs to the child, delivered to the parent"), Fonteva
derived billing contact, Nimble shareable "Bill Me" link. None models a payer relationship with its own lifecycle.

**P7 — Composition change semantics.** Fonteva: assignment per term, cancel inactivates all subscribers, suspend revokes
badges. Novi: parent expiry demotes inheritors; anti-abuse rule forbids freeing a seat without removing the person;
seats prorate only in the renewal window. Wild Apricot: all members share one status and renewal date; coordinator
cannot remove. Protech: recipients copy forward; overage billed at renewal. Nimble: re-flow only on listed triggers.

**P8 — Nobody has a household entity.** Fonteva's "John Doe Household" account name is the closest, and it is a label on
a one-person account. Families are approximated by seats (Fonteva, Wild Apricot, YM), by children of an individual
(Novi), or by a parent contact (Protech).

### Weaknesses worth avoiding

1. **Single-parent / single-primary coverage links** (all products). A person cannot derive benefits from an employer
   and a family at once; staff must choose which link is "primary".
2. **Copied member fields without a trigger model that covers every change.** Nimble: no re-flow when the parent's own
   information is updated; changing the flow-down class does not revert prior writes. Protech: toggling Use Parent Price
   Level off or removing the parent leaves stale values to be fixed by hand.
3. **Two entitlement systems that disagree.** Protech member flag vs benefit recipients ("do not confer membership
   status or member pricing"); Fonteva materialised assignment badges vs text-field inherited badges; GrowthZone Full
   Inheritance vs hand-picked Related Contacts with no record of which applied.
4. **Entitlement without provenance.** Nimble, Protech, GrowthZone, YM, Impexium store the result but not the source
   relationship; explaining "why is this person a member" requires reconstructing from the hierarchy.
5. **Seat models that force uniformity.** Wild Apricot: one level, one status, one renewal date for everyone in the
   bundle; the administrator is charged for all. Novi: dues fixed irrespective of inheritors unless the seat-based
   category is used.
6. **Snapshot duplication on the link record.** Fonteva Assignments copy name, email and mailing address; Nimble flows
   addresses; Protech shares addresses by switch; each drifts.
7. **Manual lists with no guard rails.** Protech benefit recipients ("no logic that prevents you…"); GrowthZone Related
   Contacts; YM sub-account attach that the API cannot set.
8. **Anti-abuse rules implemented as UI prohibitions** (Novi: cannot free a seat without removing the person) because
   the system has no concept of a seat-change history.
9. **One membership per constituent** (Nimble by default) and **one bundle per person** (Wild Apricot), which collide
   with concurrent individual + family + professional coverage.
10. **Relationship types that are purely descriptive** (GrowthZone person-to-person, Fonteva Connections) sit beside the
    coverage mechanism rather than feeding it, so the data model has two unconnected notions of "related".
11. **Status flags that distinguish "dropped" from "never" but nothing else** (MemberSuite tri-state), which is not
    enough for reinstatement rules or explanations.
12. **Reporting blind spots** reported by customers: YM "can't pull sub-accounts of ONLY current members with their
    master"; GrowthZone "everything is labeled Contact"; Naylor no cascading firm→individual updates.

### Evidence table

V = Verified, R = Reported, I = Inferred, U = Unavailable. Column numbers are the ten questions.

| Product        | 1 household entity                              | 2 household owns         | 3 individual covers others      | 4 family relationships | 5 add/remove | 6 beneficiary identification | 7 flow mechanism           | 8 dynamic eligibility | 9 donations/comms/addresses | 10 limitations |
| -------------- | ----------------------------------------------- | ------------------------ | ------------------------------- | ---------------------- | ------------ | ---------------------------- | -------------------------- | --------------------- | --------------------------- | -------------- |
| Fonteva        | V (none; "Household" is an account-name suffix) | V (Account/Contact only) | V mechanism, R family use       | V (Connections)        | V            | V (Assignments)              | V (badges; text flow-down) | V (absent)            | V (snapshots) / U           | V/R            |
| Nimble AMS     | U (I: parent account)                           | I                        | V (Individual types never flow) | V structure / I use    | V            | V                            | V                          | V (not dynamic)       | V addresses / U rest        | V              |
| MemberSuite    | U                                               | U                        | U                               | R                      | U            | V/I                          | V (query)                  | V                     | U                           | V/I            |
| GrowthZone     | V (none)                                        | V                        | U                               | V                      | V            | V                            | V (functional)             | V                     | V billing / U               | V/R            |
| YourMembership | V/R (none)                                      | R                        | R/I                             | U                      | R/V          | R                            | V field / I                | I                     | U                           | R              |
| Novi AMS       | V (tree) / I                                    | I                        | V                               | V                      | V            | V                            | V                          | V (partial)           | V / U                       | V              |
| Impexium       | V (absent)                                      | U                        | U                               | V mechanism            | V/R          | V/R                          | V field / I                | I                     | V/U                         | V/I            |
| Protech UX 365 | V (none)                                        | I                        | V mechanics                     | V (parent only)        | V            | V                            | V                          | V (not dynamic)       | V                           | V              |
| Naylor AMS     | U                                               | U                        | U                               | U                      | U            | I                            | I                          | U                     | U                           | R              |
| Wild Apricot   | V                                               | V                        | V                               | V (none)               | V            | V                            | V                          | V (absent)            | V                           | V/R            |
| Glue Up        | R                                               | R                        | R                               | U                      | R            | R                            | R                          | U                     | U                           | R              |

### Sources

Fonteva

- https://fonteva-devdocs.herokuapp.com/ — OrderApi object reference (Subscription, Subscription Line, Assignment,
  Assignment Role, Badge, Badge Type, Badge Workflow, Connection, Connection Type, Connection Type Mapping, Item, Item
  Class, Account/Contact custom fields, "Household" name extension). Downloaded and searched locally.
- https://api.fonteva.io/TechnicalDocs/ApexDocs/FDService/Subscription/ and …/Assignment/ and …/BusinessGroup/ — Apex
  wrapper property lists.
- https://support.higherlogic.com/hc/en-us/articles/360055407151-Fonteva-Integration — queries over
  `OrderApi__Subscriptions__r`, `OrderApi__Badges__r`, Account `OrderApi__Primary_Contact__c`.
- https://docs.fonteva.com/user/assignment-roles-for-subscription-item-class ,
  https://docs.fonteva.com/user/account-subscriptions , https://docs.fonteva.com/user/badge-flowdown ,
  https://docs.fonteva.com/user/accounts-and-contacts ,
  https://docs.fonteva.com/user/Creating-a-Badge-Type.1660977533.html — Reported via search excerpts (pages render a
  loading shell).
- https://fonteva.atlassian.net/wiki/spaces/OTC/pages/60686526/Assigning+Contacts+to+Subscriptions and
  …/54493298/Sales+Orders+for+Subscriptions — deprecated guide, Reported via excerpts (login required).
- https://www.ausa.org/sites/default/files/filefield_paths/Fonteva%20Users%20Guide%20for%20Chapters%20Version%201%20-%2012.15.20.pdf
  — family memberships / "donated assignments" (Reported via excerpt; 403 on fetch).

Nimble AMS

- https://help.nimbleams.com/help/live/managing-specific-affiliations — affiliation fields and semantics.
- https://help.nimbleams.com/help/live/manage-the-primary-affiliation-of-an-account — one primary rule.
- https://help.nimbleams.com/help/live/information-flow-down — membership and address flow-down rules, exclusions, async
  thresholds.
- https://help.nimbleams.com/help/live/create-a-membership-type and
  https://help.nimbleams.com/help/live/edit-a-membership-type — Category, Flowdown Manager Class, account fields.
- https://help.nimbleams.com/help/live/memberships — one membership by default; object descriptions.
- https://help.nimbleams.com/help/live/memberships-with-community-hub — company manager experience without flow-down.
- https://help.nimbleams.com/help/live/affiliations , …/affiliations-in-community-hub , …/affiliation-configuration —
  self-service affiliation management, roster.
- https://help.nimbleams.com/help/live/membership-bundle-components-data-source — company manager product selection.
- https://help.nimbleams.com/help/live/winter-19-permission-changes — `NU__Affiliation__c`, `NU__Membership__c`,
  `NU__MembershipType__c` field API names.
- https://help.nimbleams.com/help/live/order-configuration — "Bill Me" payment URL (Reported excerpt).

MemberSuite

- https://www.growthzone.com/wp-content/uploads/2024/04/MemberSuite-By-GrowthZone_User_Guide_2024___REST_API_Documentation-1.pdf
  — REST guide: `ReceivesMemberBenefits`, MSQL examples, `ChildEntities`, `PrimaryOrganization`.
- https://support.higherlogic.com/hc/en-us/articles/6047427103252-MemberSuite-Integration-Technical-Worksheet and
  https://support.higherlogic.com/hc/en-us/articles/360033048651-MemberSuite-Integration-Guide — member types by
  `ReceivesMemberBenefits`; "Child Individual ID".
- https://www.membersuite.com/software/crm — relationships between organizations and individuals (marketing).

GrowthZone

- https://helpdesk.growthzone.com/hc/en-us/articles/45934699701403-Add-a-New-Membership-Type — Owned By, Inheritance
  Type.
- https://helpdesk.growthzone.com/hc/en-us/articles/45934771664539-Contact-Relationship-Types — relationship types;
  Location rule.
- https://helpdesk.growthzone.com/hc/en-us/articles/45935029050011-Manage-Parent-Child-Relationships ,
  https://helpdesk.growthzone.com/hc/en-us/articles/45934890804891-Parent-Child-Relationship-Considerations ,
  https://helpdesk.growthzone.com/hc/en-us/articles/45935992544411-Parent-Child-Use-Case-Child-Inherits-Parent-Membership
  — parent/child inheritance and billing.
- https://growthzone.growthzoneapp.com/ap/CloudFile/Download/pBDde9kL — "Managing your Memberships" PDF: Full
  Inheritance / Owner Only activation; Related Contacts; Location prerequisite.
- https://documentation.growthzoneapp.com/CuratedApi.html — API fields for contacts, memberships, roles.
- https://helpdesk.growthzone.com/hc/en-us/articles/45934724003611-Overall-Registration-Options — per-person discount
  validation; spouse guest (Reported excerpt).
- https://growthzonesupport.happyfox.com/kb/article/1224-change-organization-membership-billing-contact/ — billing
  contact behaviour.

YourMembership

- https://ws.yourmembership.com/jsv/metadata?op=People — `MasterProfileID`, `SubAccounts`, `CanDetachSubAccounts`,
  `MembershipEffectiveExpiresDate`, `PeopleBulkAttach` reference.
- https://support.higherlogic.com/hc/en-us/articles/4410238657172-YourMembership-REST-API-Integration-Guide — Is Member
  rule; no parent/organization mapping.
- https://cdn.ymaws.com/azw.starkweather.us/resource/dynamic/blogs/20160107_115100_16439.pdf — "Additional Seats",
  Master/Sub tab (implementer, 2015).
- https://cdn.ymaws.com/www.nwrawildlife.org/resource/resmgr/how_to_pages/master_and_subaccounts.pdf ,
  https://www.nadco.org/page/subaccounts/Managing-Sub-Accounts.htm , https://acupa.org/page/add-sub-accounts ,
  https://www.gmis.org/page/addsubaccount , https://www.eetc.org/page/FAQs — association guides: invite link, detach,
  suspend, sign-in-as, approval, "free member" after detach.
- https://www.g2.com/products/yourmembership/reviews — sub-account reporting complaint.

Novi AMS

- https://help.noviams.com/en/articles/6918453-overview-of-member-type-categories — three categories, dependencies,
  billable party.
- https://help.noviams.com/en/articles/3068615-create-a-new-member-type — Member Benefits tab.
- https://help.noviams.com/en/articles/2400528-member-types-that-limit-company-member-benefits-to-certain-individual-members
  — seat assignment, anti-abuse rule, statuses.
- https://help.noviams.com/articles/7903859164-company-membership-with-seat-based-dues-admin-workflow-on-the-backend and
  https://help.noviams.com/en/articles/6625445-company-membership-with-seat-based-dues-member-experience-on-the-frontend
  — seats, proration, Member Compass.
- https://help.noviams.com/articles/5812052154-deep-dive-membership-status-types-in-novi-ams — Inheriting, "membership
  via", effective member type.
- https://help.noviams.com/en/articles/1389477-dues-paying-member — designated vs effective type.
- https://help.noviams.com/en/articles/869555-create-a-new-company-or-person ,
  https://help.noviams.com/en/articles/2711005-change-a-person-s-parent-member ,
  https://help.noviams.com/en/articles/2659806-editing-individual-and-company-parent-record-settings — parent assignment
  and access settings.
- https://help.noviams.com/articles/2597162-member-benefits-flow — family tree; one parent (Reported via excerpt; page
  404s).
- https://api-docs.noviams.com/ and https://jentic.com/apis/noviams.com/noviams.md — Member Type object
  (`ForCompanies`), members endpoints.

Impexium

- https://learn.microsoft.com/en-us/connectors/impexium/ — operations and schemas: relationships, relationship types,
  `MembershipData.inheritedMembershipBenefits`, webhooks.
- https://acconline.org/wp-content/uploads/Impexium-Set-Up-Steps-for-Official-Representatives.pdf — relationship types,
  "RECEIVES MEMBERSHIP BENEFITS" flag, adding individuals (association guide).

Protech UX 365

- https://ux365help.ptassoc.com/ — Parent and child records; Using parent price level; Sharing parent primary address;
  Billing the parent record; Benefit recipients; Number Benefit Recipients; Additional benefit recipient charges;
  `PARenewal.AdditionalBenefitRecipientsCreate`. Downloaded and searched locally.

Naylor AMS

- https://masie.membershipsoftware.org/files/Naylor%20API%20Documentation.pdf — API methods and response fields.
- https://naylor-nb.membershipsoftware.org/assoc_subscribe.asp — firm-level membership types (customer configuration).
- https://www.capterra.com/p/102408/Timberlake-AMS-Solutions/reviews/ — no cascading firm→individual updates (review).

Wild Apricot

- https://api.swaggerhub.com/apis/WildApricot/wild-apricot_public_api/7.24.0 — `MembershipLevel`, `Bundle`, `Contact`
  schemas; bundle endpoints; CreateContact bundle instructions.
- https://gethelp.wildapricot.com/en/articles/2867-bundle-membership-levels — level configuration, limits,
  coordinator/admin rights, limitations.
- https://gethelp.wildapricot.com/en/articles/514-bundle-administrator-guide — coordinator capabilities.
- https://gist.github.com/DmitriySmirnov/8b29429d75e9b66eef9c88d4a647a3e8 — API payloads ("Member role", "Bundle ID").
- https://forums.wildapricot.com/forums/308932-wishlist/suggestions/8825677-revising-bundle-membership-functionality ,
  https://forums.wildapricot.com/forums/308932-wishlist/suggestions/38069476-allow-different-membership-levels-for-bundle
  , https://forums.wildapricot.com/forums/308932/suggestions/8825986 — same-level constraint, family requests.

Glue Up

- https://support.glueup.com/hc/en-us/articles/46874625907481-What-is-the-difference-between-an-admin-contact-and-primary-member
  , https://support.glueup.com/hc/en-us/articles/43891108371353-Membership-Type-Versions ,
  https://support.glueup.com/hc/en-us/articles/49627399312281-Glue-Up-Manager-App ,
  https://support.glueup.com/hc/en-us/articles/43725842252057-Manage-your-membership-profile-from-My-Glue-app — Reported
  via excerpts (Cloudflare block).
- https://www.britcham.org.sg/sites/default/files/content-files/Glue%20Up%20User%20Guide%202020.pdf — primary member,
  spaces per category, add-on members (chamber guide; text extracted locally).
- https://jerseychamber.com/already-member — primary member permissions (chamber page).

### Pages that could not be fetched (and how the gap was handled)

- docs.fonteva.com (all pages): Salesforce Experience Cloud shell, "CSS Error / Loading"; a reader proxy returned
  "Invalid Page" — used search excerpts, marked Reported.
- fonteva.atlassian.net deprecated user guide: redirects to Atlassian login — excerpts only.
- fun.fonteva.com support articles: loading shell — not used.
- AUSA Fonteva chapter guide PDF: 403 (bot check) — excerpt only.
- help.noviams.com "Member Benefits Flow" (both URL forms): 404 — excerpts only; other Novi articles fetched fine.
- api.membersuite.com, rest.membersuite.com Swagger UIs, community.membersuite.com: JS shells / not reachable — the 2024
  REST guide PDF was used instead.
- helpdesk.growthzone.com direct fetches: 403; most articles were readable through a reader proxy or excerpts;
  documentation.growthzoneapp.com/CuratedApi.html readable only via the proxy.
- api.yourmembership.com/reference/2_30/: 403; YM API Overview PDF is image-only — relied on the ws.yourmembership.com
  metadata page.
- support.glueup.com: Cloudflare 403 — excerpts only.
- eetc.org FAQ: 403 — excerpt only.
- ux365help "UX 365 Learning Portal" PDF: exceeded the fetch size limit; not needed after the help page was downloaded
  locally.
- gethelp.wildapricot.com article 164 "Membership bundles": 404 — covered by articles 2867 and 514.
- Impexium support portal and Learning Hub: customer-only; the Microsoft connector reference stood in as the API
  documentation.

## 32. W5 — Museum, zoo, aquarium, garden and cultural-institution membership platforms

## W5 — Museum, zoo, aquarium, garden and cultural-institution membership platforms

Research brief for the Rhythm 2.0 households / constituent relationships / membership coverage architecture. Written
2026-10-21 05:10 ET. Companion to w1-blackbaud.md (Altru user guides) and the earlier CRM-vendor summaries (Neon,
Bloomerang, LGL, Virtuous, DonorPerfect, CiviCRM, Kindful, Keela).

### 0. How to read this document

**What this answers.** For each product in the admissions-and-membership segment: how a household is modelled, who can
own a membership, how the people covered by one membership are identified, how benefits reach them at the gate, and what
the vendor's own data model or API reveals about the compromises made.

**Evidence grades used throughout.**

- **Verified** — vendor documentation, a published database reference, a published REST/API reference, or vendor help
  articles read directly.
- **Reported** — integrator write-ups, partner help centres (Cuseum, InstantEncore), reviews, community posts,
  search-engine snippets of pages that could not themselves be opened.
- **Inferred** — my conclusion from Verified or Reported facts; always labelled.
- **Unavailable** — nothing public found, or the page is behind a login.

**Rule followed:** no vendor's internal database structure is stated unless a published DB or API reference documents
it. Only two vendors publish one: Blackbaud (CRM/Altru DB reference; SKY Membership API for Raiser's Edge NXT) and
Tessitura (TessituraService REST help, hosted publicly by licensees). ACME publishes an API reference, but its portal is
login-walled; ACME facts therefore come from search snippets of that portal and from Cuseum's integration article, and
are graded Reported.

**Pages that could not be fetched** are listed in §16.

---

### 1. Blackbaud Altru / Blackbaud CRM — the published database reference (DEEPER)

Altru runs on the Blackbaud CRM ("Infinity") schema. The CRM DB reference at
`webfiles-sc1.blackbaud.com/files/support/helpfiles/crm-db/content/<table>.html` is public, so the tables below are
**Verified** as the actual data model. (Altru may hide some columns in its UI; the schema is shared.)

#### 1.1 Household / group tables

**`CONSTITUENT`** — one table for every party. Relevant flags (Verified): `ISGROUP bit default 0` "Indicates whether a
record is a group"; `ISORGANIZATION bit default 0`; `ISCONSTITUENT bit default 1` "Indicates if the record is a
constituent for fundraising purposes"; `ISINACTIVE bit`; `DISPLAYNAME nvarchar(100)` "Display name for households". So a
household **is a constituent row** (ISGROUP = 1), addressable, with its own name, addresses, revenue and history. Answer
to Q1: _household = a constituent of kind "group", subtype "household"._

**`GROUPDATA`** (PK `ID` → `CONSTITUENT.LOCALID`) — "details specific to group constituents".
`GROUPTYPECODE tinyint default 0` with computed `GROUPTYPE`: **0 = Household, 1 = Custom**. Custom groups point to
`GROUPTYPEID → GROUPTYPE.ID`. `NAMEFORMATFUNCTIONID → NAMEFORMATFUNCTION.ID` is the function that generates the
household's name. `STARTDATE`, `DESCRIPTION nvarchar(300)`. Triggers include
`TR_GROUPDATA_MARKNONHOUSEHOLDASCONSTITUENT` (name only; purpose inferred: custom groups are flagged as fundraising
constituents, households are not by default) and `TR_GROUPDATA_INSERT/DELETE_HOUSEHOLD_UPDATE`.

**`GROUPMEMBER`** (PK `ID`) — "Records which constituents belong to which groups". Columns:
`GROUPID → CONSTITUENT.LOCALID` (the group), `MEMBERID → CONSTITUENT.LOCALID` (the member), `ISPRIMARY bit default 0`
("primary contact" of the group), `DATEADDED`, `DATECHANGED`, audit FKs. Indexes on GROUPID and MEMBERID. Triggers:
`TR_GROUPMEMBER_INSERT_HOUSEHOLD_UPDATE`, `TR_GROUPMEMBER_DELETE_HOUSEHOLD_UPDATE`,
`TR_GROUPMEMBER_DUPLICATEGROUPMEMBER` (prevents duplicate rows), `TR_GROUPMEMBER_UPDATEISCONSTITUENT`,
`TR_GROUPMEMBER_T2BUPDATE`. **There is no "role" or "relationship" column on GROUPMEMBER itself**; membership of a
household is a bare pairing plus a primary flag. Spouse/child semantics live in `RELATIONSHIP` (below), not in the
household link.

**`GROUPMEMBERDATERANGE`** (FK `GROUPMEMBERID → GROUPMEMBER.ID`) — "the date ranges for which constituents are members
of groups": `DATEFROM date null`, `DATETO date null`, `COMMENTS nvarchar(300)` ("the reason the member left"). Trigger
`TR_GROUPMEMBERDATERANGE_UPDATE_HOUSEHOLD_UPDATE`. So **leaving a household is end-dating, not deleting**, and a person
can have several stints.

**`GROUPMEMBERROLE`** (FK `GROUPMEMBERID`, `GROUPMEMBERROLECODEID → GROUPMEMBERROLECODE.ID`, `STARTDATE`, `ENDDATE`) —
"Role information for a group member". Roles are a configurable code table with their own date ranges; the reference
does not restrict them to households, and the query view below only exposes roles for custom groups.

**Current-member logic (Verified from `UFN_GROUP_GETCURRENTMEMBERS` and `V_QUERY_CONSTITUENTGROUPMEMBER`):** a
GROUPMEMBER row is current when it has no date range, or `DATEFROM <= today` and (`DATETO` is null or `DATETO > today`).
`V_QUERY_CONSTITUENTGROUPMEMBER` (fields `ISCURRENTMEMBER`, `ISFORMERMEMBER`, `ISPRIMARY`) filters `GROUPTYPECODE = 1`,
i.e. it is the _custom group_ query view; household membership is reported through household-specific views and the
`HOUSEHOLD_UPDATE` triggers instead.

**`HOUSEHOLDINFO`** — a one-row system table (triggers `ENSUREONEROW_INSERT/DELETE`) that is the whole household-policy
configuration:

| Column                                | Default | Meaning (verbatim)                                                                                  |
| ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `HOUSEHOLDRECOGNIZEHOUSEHOLD`         | 0       | "Indicates if a household will receive recognition credit for their own revenue."                   |
| `HOUSEHOLDRECOGNIZEMEMBERS`           | 0       | "...a household's members will receive recognition credit for the household's revenue."             |
| `MEMBERRECOGNIZEHOUSEHOLD`            | 1       | "...a household will receive recognition credit for their member's revenue."                        |
| `MEMBERRECOGNIZEMEMBER`               | 1       | "...a household member will receive recognition credit for their revenue."                          |
| `MEMBERRECOGNIZEOTHERMEMBERS`         | 0       | "...a household member will receive recognition credit for a different household member's revenue." |
| `HOUSEHOLDSCANBEDONORS`               | 1       | "Indicates whether revenue can belong to a household."                                              |
| `NAMEFORMATFUNCTIONID`                | —       | default household name generator                                                                    |
| three `*REVENUERECOGNITIONTYPECODEID` | null    | recognition type for household→member, member→member, member→household flows                        |

This answers Q9 for Altru/CRM directly: **household giving is a recognition-credit fan-out governed by six global
switches**, with the default being "member gifts roll up to the household; the household's own gifts do not roll down to
members; members do not see each other's gifts."

**`RELATIONSHIP`** — "stores relationships for constituents". Two rows per pair: `RELATIONSHIPCONSTITUENTID`,
`RECIPROCALCONSTITUENTID`, `RELATIONSHIPTYPECODEID`, `RECIPROCALTYPECODEID` (both → `RELATIONSHIPTYPECODE`),
`ISSPOUSE bit`, `ISPRIMARYBUSINESS`, `ISCONTACT`, `ISPRIMARYCONTACT`, `ISEMERGENCYCONTACT`,
`ISMATCHINGGIFTRELATIONSHIP`, `RECEIVESREPORTCARD`, `STARTDATE`, `ENDDATE`, `RELATIONSHIPSETID`. **No household column
exists on RELATIONSHIP**, and no relationship column exists on GROUPMEMBER: the two concepts are orthogonal tables
joined only through the constituent. Answer to Q4: _spouse = ISSPOUSE on a typed, reciprocal RELATIONSHIP row;
child/guardian = relationship type codes; household membership is a separate GROUPMEMBER row._

#### 1.2 Membership tables

**`MEMBERSHIPPROGRAM`** (Verified) — `PROGRAMTYPECODE` 0 Annual / 1 Recurring-Sustaining / 2 Lifetime;
`PROGRAMBASEDONCODE` 0 Dues / 1 Contributions / 2 Both; `ALLOWMULTIPLEMEMBERSHIPS bit` ("whether a constituent can hold
multiple instances of this program"); `CATEGORYCODE` (1 Museum, 2 Zoo/Aquarium, 5 Arboretum/Botanical Gardens, 3 Public
Radio, ...); expiration rules (`EXPIRESONCODE` join date / end of month / calendar year / fiscal year / specific dates,
`CUTOFFDAY`, `BACKDATEMEMBERSHIPS`); renewal window rules; **add-ons at program level: `ALLOWADDONADULT bit`,
`ADDONADULTPRICE money`, `ALLOWADDONGUEST bit`, `ADDONGUESTPRICE money`** — named adult vs unnamed guest; card printing:
`CARDFORMAT`, `LETTERTEMPLATEID`, `REPORTCATALOGID`.

**`MEMBERSHIPLEVEL`** (Verified) — `MEMBERSALLOWED smallint` "number of members allowed for this level",
`CHILDRENALLOWED smallint`, `CARDSALLOWED smallint`, `TIERCODEID`, `OBTAINLEVELCODE` 0 "Purchased with dues" / 1
"Awarded with contributions", `MEMBERSHIPTERMTYPECODE` 0 Rolling / 1 Fixed, `NONRENEWALACTIONTYPECODE` 4 Lapsed / 5
Expired, `BEFOREEXPIRATION`/`AFTEREXPIRATION` (grace windows), `FORCEMANUALDOWNGRADES`. Children are a **count on the
level, not named people** — Q6.

**`MEMBERSHIPLEVELBENEFIT`** — benefits per level with `QUANTITY`, `UNITVALUE`, `NUMBERTOOFFERCODE` **0 = "based on
member count"**, 1 = specific number; `FREQUENCYCODE` 0 every renewal / 1 initial joining.

**`MEMBERSHIPLEVELTYPE`** — N:M between a level and `MEMBERSHIPLEVELTYPECODE` (configurable "type" such as
Individual/Family; the reference does not enumerate values).

**`ADDON`** — `ADDONTYPECODE` computed `ADDONTYPE`: **0 = "Benefit", 1 = "Additional member"**. Unique name.

**`MEMBERSHIP`** (Verified) — one row per membership instance: `MEMBERSHIPPROGRAMID`, `MEMBERSHIPLEVELID`,
`MEMBERSHIPLEVELTERMID`, `MEMBERSHIPLEVELTYPECODEID`, `JOINDATE`, `EXPIRATIONDATE`, `LASTRENEWEDON`, `STATUSCODE`
(computed `STATUS`: 0 Active, 1 Cancelled, 2 Pending, 3 Grace, 4 Lapsed, 5 Expired), `NUMBEROFCHILDREN smallint`,
`NUMBEROFADDONADULTS smallint`, `NUMBEROFADDONGUESTS smallint`, `ISGIFT bit`, `GIVENBYID → CONSTITUENT.LOCALID`,
`SENDRENEWALCODE` (computed `SENDRENEWAL`: "Giver", "Primary member", "Giver and primary member"),
`AUTOMATICALLYRENEWMEMBERSHIP`, `LASTCARDNUMBER smallint` ("last number appended to a computed card number"),
`LOOKUPID`, `CANCELLATIONREASONCODEID`. **Note what is absent: there is no owner/holder constituent column on
MEMBERSHIP.** Ownership is expressed only through `MEMBER.ISPRIMARY`. Payer (gift giver) is a separate nullable FK.
Q2/Q3/Q7 follow from this.

**`MEMBER`** (Verified) — "Stores member information for a membership": `MEMBERSHIPID → MEMBERSHIP.ID`,
`CONSTITUENTID → CONSTITUENT.LOCALID`, `ISPRIMARY bit`, `ISDROPPED bit`, `ISCAREGIVER bit` ("Indicates if the member is
care giver"). Trigger `TR_MEMBER_MARKASCONSTITUENT` (name only), `TR_MEMBER_VALIDMEMBERCHECK` (Verified body summary):
rejects (a) the same constituent appearing twice, not dropped, on one membership, and (b) when the program has
`ALLOWMULTIPLEMEMBERSHIPS = 0`, a constituent holding a second active membership in the same program (cancelled
memberships ignored). `USP_DAILYSALES_MEMBERSHIPRENEWAL_GETMEMBERSANDCARDS` (snippet, Verified) reads `MEMBERSALLOWED`
and `CARDSALLOWED` from the level, counts non-dropped `MEMBER` rows, and caps card generation at `CARDSALLOWED`.

So **covered people are materialised `MEMBER` rows, each a real constituent**, with one primary, optional caregiver
flag, soft removal by `ISDROPPED`. Children are a count on MEMBERSHIP (`NUMBEROFCHILDREN`) — no MEMBER row and no age
logic appear in the schema. Unnamed guests are a count (`NUMBEROFADDONGUESTS`).

**`MEMBERSHIPCARD`** (Verified) — `MEMBERID → MEMBER.ID` (the only owner link; no direct membership or constituent FK),
`NAMEONCARD nvarchar(700)`, `CARDNUMBER nvarchar(100)`, `EXPIRATIONDATE`, `STATUSCODE` 0 Issued / 1 Printed / 2
Cancelled, `PRINTDATE`, `PRINTEDBYID`, `PRINTMEMBERSHIPCARDSPROCESSSTATUSID`. `V_QUERY_MEMBERSHIPCARD_PRINTOUTPUT` joins
MEMBERSHIP → MEMBER → MEMBERSHIPCARD → CONSTITUENT(member) with MEMBERSHIPLEVEL on the membership; the printed address
is **the member's own primary address**, not a household or primary-member address; `NAMEONCARD` is free text. Q7:
_cards hang off member rows; an unnamed guest cannot have a card row because there is no MEMBER row to attach it to._

**`MEMBERSHIPADDON`** — purchased add-on instance per membership: `ADDONID`, `QUANTITY smallint default 1`,
`EXPIRATIONDATE`, `PURCHASEPRICE`, `NUMCANCELLED`, `MEMBERSHIPTRANSACTIONID`, `REVENUESPLITID`.

**`MEMBERSHIPTRANSACTION`** (Verified) — the history ledger: `ACTIONCODE` 0 Join / 1 Renew / 2 Upgrade / 3 Downgrade / 4
Drop / 5 Rejoin / 6 Cancel; `UPGRADEMETHODCODE` 0 Not an upgrade / **1 Renewal upgrade / 2 Midterm upgrade**; `ISGIFT`,
`DONORID` (the gift payer on that transaction), `NUMBEROFCHILDREN`, `EXPIRATIONDATE`, `MEMBERSHIPLEVELID`,
`MEMBERSHIPLEVELTERMID`, `REVENUESPLITID`, `MEMBERSHIPPROMOID`. Mid-term upgrades are therefore a first-class
transaction type.

**`SALESORDERITEMMEMBER`** — members captured on a sales order (`CONSTITUENTID`, `ISPRIMARY`, `ISCAREGIVER`, `MEMBERID`
"of a member being renewed"); `SALESORDERITEMMEMBERSHIPCARD` references it. Members are therefore chosen at point of
sale and become MEMBER rows on fulfilment.

#### 1.3 Answers for Altru/CRM

1. **Household** = CONSTITUENT with ISGROUP=1 and GROUPDATA.GROUPTYPECODE=0; members via GROUPMEMBER (+ date ranges, +
   roles for custom groups). Verified.
2. **Can a household own a membership?** Schema-wise a household constituent _could_ be a MEMBER row (no constraint
   prevents it), but nothing in the membership tables references GROUPDATA; Altru user guides (w1) describe memberships
   on individuals. Inferred: not a supported pattern.
3. **Individual covering several people without a household?** Yes, Verified: MEMBER rows and NUMBEROFCHILDREN /
   NUMBEROFADDONGUESTS live on MEMBERSHIP with no household dependency.
4. **Spouse/child/guardian** = RELATIONSHIP rows (ISSPOUSE, typed reciprocal codes); household membership is a separate
   GROUPMEMBER row. Verified.
5. **Add/remove household members** = insert GROUPMEMBER; remove by GROUPMEMBERDATERANGE.DATETO (history preserved) or
   delete; triggers update household rollups. Verified.
6. **Beneficiaries** = named MEMBER rows (primary, caregiver flags) + counts (children, add-on guests) + add-on
   "Additional member" rows. Verified.
7. **Benefit flow** = materialised MEMBER rows and MEMBERSHIPCARD rows per member; gate checks a card number (Altru
   scanning, w1). Verified for structure.
8. **Dynamic eligibility via relationships?** No: nothing in MEMBER/MEMBERSHIPCARD references RELATIONSHIP or
   GROUPMEMBER. Verified by absence.
9. **Household donations/comms/addresses/history** = HOUSEHOLDINFO recognition-credit switches; household has its own
   addresses/name; HOUSEHOLD_UPDATE triggers maintain rollups. Verified.
10. **Compromises visible:** children and guests are counts, not people (no ageing-out logic); card ownership is only
    via MEMBER, so unnamed guests have no card; holder is only "whichever MEMBER has ISPRIMARY=1"; two parallel grouping
    systems (GROUPMEMBER vs RELATIONSHIP) that must be kept consistent by hand; a global, not per-program, recognition
    policy.

Extras: gift memberships = `ISGIFT` + `GIVENBYID` + `SENDRENEWALCODE` (who gets renewal notices) — payer vs holder is
explicit. Caregiver = `ISCAREGIVER` on MEMBER (a named person, not an add-on SKU). Mid-term upgrade =
`UPGRADEMETHODCODE = 2`. Reciprocal programs: not in the schema pages read (w1 covers Altru's UI).

---

### 2. Raiser's Edge NXT — SKY Membership API (new facts only)

The SKY API reference (`developer.sky.blackbaud.com/api#api=membership`) is public and renders without login. Verified
from the changelog and the `CreateMembershipV2` / `AddMembershipCardsAsync` pages (2026-07-08 LTS set).

**Shape.** `POST /membership/v1/constituents/{constituent_id}/v2/membership` creates a membership _on one constituent_
and returns a `member_junction_id`; every other route is keyed by that junction id. Fields: `category` (required; code
table, sample value "family"), `subcategory`, `program`, `dues`, `joined_date`, `expires_on_date`,
`lifetime_membership`, `given_by_id` ("constituent giving this membership as a gift"), `send_benefits_to` ∈
{PrimaryMember, Donor}, `send_notice_to` ∈ {Donor, PrimaryMember, Both}, `waive_benefits`, **`total_members` (required)
"total members allowed in this membership"**, **`total_children` (required)**, `membership_cards[]`,
`membership_benefits[]` (benefit, count, unit_cost, total_benefit_value, sent, comments), `membership_link_gift`,
`membership_fundraisers`.

**Cards = the member list.** `MembershipAddFormCardCreate` / `MembershipCardCreate`: `primary bool`, `card_member bool`,
**`joint_member bool`, `joint_member_id int`, `relationship_id int`, `relation_type` (code table, sample "Wife")**,
`card_type` (code table, sample "Child"), `name_on_card` (required), `no_of_cards`, `card_expires` ∈ {SameDates,
OneWeekAfter, ..., SpecificDates, Lifetime}, `address_to_print` ∈ {PrimaryMember, None, Constituent}, `valid_from_date`,
`valid_to_date`, `status` ∈ {Printed, NotPrinted, DoNotPrint, Lost}. Changelog: "The primary member card cannot be
deleted." Lifecycle routes: `/renew`, `/upgrade` and `/downgrade` ("during cycle"; only active or lapsed), `/drop`,
`/rejoin` ("only dropped memberships"), `/updatemembershipgift`, `/addlinktogift`.

**What this tells us (Inferred from the schema):** RE NXT models covered people as **card rows that may point at an RE
relationship** (`relationship_id`, `joint_member_id`): the joint member is a constituent reached through the holder's
relationship list, so eligibility to appear on the card is relationship-driven at entry time, but the card row is then
materialised and lives its own life (expiry offsets, Lost status, address_to_print). `total_members` / `total_children`
are caps, not rosters. There is no household entity in the Membership API; the holder is the constituent in the URL.

---

### 3. Tessitura — TessituraService REST help (DEEPER)

Source: public help at `ts-stg-appgw.calacademy.org/TessituraService/Help` (API 15.2.45). The membership resources carry
the warning that they "will change significantly in an upcoming major Tessitura release" (v16 overhauls memberships and
adds Benefits — Reported, Studio Bravo). Field lists below are Verified for v15.

#### 3.1 Constituents, households, affiliations

- **Household is a constituent.** `Constituent` has `ConstituentType → ConstituentTypeSummary`, and
  `ConstituentType.ConstituentGroup → TR_CUST_GROUP`. Notes on `Constituent.FirstName`: "Not used for organization or
  household constituents"; `MiddleName`: "Not used for household or organization constituents"; `DisplayName`:
  "Generated display name based on name values. Not updateable." Endpoints:
  `POST CRM/Constituents/{id}/ConvertToHousehold` ("Converts an individual to a household"), `ConvertToIndividual`
  ("Converts a household to an individual"), `ConvertToOrganization`,
  `GET CRM/Constituents/PrimaryHousehold?constituentId=` ("Get primary household constituent details for a provided
  constituentId"), `POST CRM/Constituents/{id}/Snapshot/SwapA1A2` ("Swaps A1/A2 names on a household and regenerates its
  salutation"). So a household record carries **two name slots (A1/A2)** and a generated display name; an individual can
  be _converted_ into a household (its record becomes the group).
- **Affiliation = the household link (and every other group link).** `Affiliation` model (POST): `GroupConstituent`
  (Entity → T_CUSTOMER), `IndividualConstituent` (Entity → T_CUSTOMER), `AffiliationType → TR_AFFILIATION_TYPE`,
  `PrimaryIndicator bool`, `NameIndicator int`, `IsIncludedInSearchResults bool`, `StartDate`, `EndDate`, `Inactive`,
  `Title`, `Salary`, `Note`, `AddressId`, `ElectronicAddressId`, `SalutationId`, `IsAllowedToTransact` (DEPRECATED
  "unused"). `AffiliationType` has `RelationshipCategory → TR_RELATIONSHIP_CATEGORY`, `UseSalary`, `UseTitle`,
  `IsIncludedInSearchResultsDefault`.
  `GET CRM/Affiliations?individualConstituentId&groupConstituentId&startActiveDate&endActiveDate&primaryOnly&showWithGroup`.
  `ConstituentType.DefaultAffiliatedConstituentTypeId` / `DefaultAffiliationTypeId` define, per constituent type, what
  group an individual is affiliated to by default (e.g. Individual → Household). `POST CRM/AffiliationInfo` creates the
  related individual if it does not exist (one-shot "add a household member"). `ConstituentSummary` in search results
  exposes `NumberOfAffiliates`, `AffiliationId`, `IsPrimaryAffiliation`, `A12Indicator`.
- **Relationships vs affiliations.** `GET CRM/Relationships` returns "all affiliations and associations" of a
  constituent; `Associations` is the separate individual-to-individual link resource. So Tessitura has _affiliations_
  (individual ↔ group: household, employer, school) and _associations_ (peer-to-peer).
- **Constituencies cascade through affiliations.** `GET CRM/Constituencies?constituentId&includeAffiliations` returns
  the constituent's constituencies "and all its visible affiliation's constituencies as well. To exclude constituencies
  of the visible affiliations pass includeAffiliations=false." Constituencies are the usual Tessitura mechanism for
  member pricing/access ("Membership constituency" — Reported, Donate2).

#### 3.2 Memberships

- `GET CRM/Memberships/Details?constituentId&includeAffiliations&membershipOrgIds&onlyShowCurrent&onlyShowDefault` —
  `includeAffiliations`: "If true, will also return details for affiliates of the constituent." `MembershipDetail`:
  `Id`, `Constituent` (holder), **`BenefitProvidingConstituentId`**, `MembershipLevel` (`MembershipOrganization`,
  `Code`, `StartAmount`, `EndAmount`, `Rank`, `ConstituencyType`), `CurrentStatus`, `IsCurrent`, `IsDefault`,
  `DeclinedBenefits`, `InitiationDate`, `InceptionDate`, `ExpirationDate`, `LapseDate`, `RenewalDate`, `MacroTrend`,
  `MicroTrend`.
- `POST CRM/Memberships/Calculate` — `CalcConstituentId`, **`CrediteeId` "A constituent id to credit the membership"**,
  `MembershipOrganizationId`, `MembershipLevelOverride` (3-char level code), `RenewUpgradeIndicator` 'R'/'U',
  `DeclineBenefits` 'Y'/'N', `CalcContributionAmount` (levels are giving-amount bands: `StartAmount`/`EndAmount`),
  `RealOrMirror`. Response includes `ParentConstituentMembershipId`, `NRRStatus`, `MembershipMode`.
- `POST CRM/Constituents/Search/ByCardNumber` — card number → constituent summaries (no membership fields in the
  response).
- **"Recipients", "Initiator", "Owner" and a MemberCard/"Cards" resource do not exist in this v15 help** (index checked;
  `MembershipLevel`/`MembershipOrganization` full models return error pages). They may be v16 terms; Unavailable here.
  Digital member cards are a v16 feature (Reported, tessitura.com Features: live in v16.0.34).

#### 3.3 Answers for Tessitura

1. Household = a constituent of a household-group constituent type; members attached by Affiliation rows (typed, dated,
   primary flag, name indicator). Verified.
2. Household owning a membership: the holder is `MembershipDetail.Constituent` — any constituent id. The
   `PrimaryHousehold` endpoint and `BenefitProvidingConstituentId` imply benefits can be provided to one constituent by
   another's membership (typically the household's or the spouse's). Inferred; the v15 docs do not spell out the rule.
3. Individual covering several people without a household: not expressible in the data shown — coverage is by
   affiliation (`includeAffiliations`). Inferred: no.
4. Spouse/child = affiliations to the household (type from TR_AFFILIATION_TYPE, RelationshipCategory) and associations
   between individuals. Verified structure, semantics configurable.
5. Add/remove = POST/PUT/DELETE Affiliations; end-date via `EndDate`/`Inactive`; `AffiliationInfo` creates the person on
   the fly. Verified.
6. Beneficiaries = affiliates of the benefit-providing constituent (dynamic), not materialised member rows. Verified by
   API shape.
7. Benefit flow = `BenefitProvidingConstituentId` + constituency inheritance across visible affiliations; pricing rules
   key off constituencies. Verified.
8. Dynamic eligibility through relationships: **yes — this is Tessitura's primary mechanism** (`includeAffiliations`,
   constituency cascade). Verified.
9. Household donations/comms/addresses: household constituent has its own address/salutation (A1/A2); `CrediteeId` lets
   a membership be credited to another constituent; `Snapshot` returns primary contact points. Verified.
10. Compromises: the household is a _converted_ constituent with hard-coded two-name (A1/A2) salutation logic; a
    community practitioner warns not to track attendance at household level because "many times not all members of a
    household will attend" (Reported, TLCC 2016 notes); memberships are giving-band levels (StartAmount/EndAmount) which
    fits arts fundraising more than admissions caps; no roster of named cardholders or child counts in v15; v16 breaks
    the API.

Extras: payer vs holder = `CrediteeId`; upgrade = `RenewUpgradeIndicator 'U'`; `DeclineBenefits` per membership; gate
scanning via `Search/ByCardNumber` plus `NScanAccessAreas` reference data (access control module) — Verified existence
only.

---

### 4. Doubleknot

Public documentation is thin; the "Managing Member Profiles" guide (PDF, Verified) and marketing/solution sheets
(Verified text, marketing grade) are what exists. The "Connect" API is described only as an "open API" for sharing data
with other systems; no public endpoint reference (Unavailable).

- **Entity model (Verified, Member Profiles guide):** every person is a _member profile_ ("The term MEMBER is a person
  registered to Doubleknot and assigned to one or more organizations", terms of service). The Member Details page lists
  sections: Member Profile, Logon Account Privileges, Elists and Groups, **Memberships** ("if your organization uses the
  Membership Management module"), Registrations/Orders/Reservations, Donations, **Member Relationships — "Displays the
  member's relationship to other member"**, Positions, Achievements, Contact Notes. Merge semantics (Verified):
  Memberships, Donations, Registrations, Elists/Groups, Positions are _added to the target profile_; **Member
  Relationships and Contact Notes are deleted** on merge.
- **Household:** no household entity appears in the guide; relationships are optional profile-to-profile links ("If your
  organization is using the Member Relationships ... features"). A Software Advice reviewer states the product has "no
  relationship or household feature" (Reported, may predate the feature). Doubleknot's own membership-card advice is
  that cards should "list other member's names" (Reported, blog) — i.e. names printed on one card, not separate
  cardholder records.
- **Membership structure (Verified, solutions sheet):** "Doubleknot can support any membership structure"; benefits
  applied automatically online; on-site "you can scan membership cards, look up memberships by name and apply their
  benefits"; print-at-home and mobile cards; gift memberships "with personal messages and options to choose the delivery
  date"; renewal reminders; member discounts by membership level.
- Registration forms support "registrant types" with per-type questions (Reported) — the likely mechanism for capturing
  additional family names at purchase (Inferred).

Answers: (1) no household entity found; relationships between profiles are the only link — Verified/Reported. (2) n/a.
(3) Yes in practice: a membership type can be a "family" type and names are captured on the registration/card —
Inferred. (4) "Member Relationships" feature, unstructured in public docs. (5) Unavailable. (6) Inferred: names on the
card/registration, not separate cardholder records. (7) Card scan or name lookup resolves the membership — Verified. (8)
No evidence. (9) Donations/registrations sit on the individual profile; duplicates are frequent enough that the guide
devotes a chapter to merging — Verified. (10) Merge deletes relationships; profile duplication by name/email; no public
API reference.

---

### 5. Versai (Explorer Systems)

Only marketing pages are public (Verified as vendor statements, not as a data model).

- "Database of members to track personal information of individuals and families" (Reported, Capterra listing of vendor
  copy); staff can see "ages of children, membership use, giving histories" (Reported, vendor site).
- Cards: "unlimited, fully customizable membership card formats with merge fields", printed at the desk; "Barcoded
  membership cards may be scanned for fast admission"; "Automatic smart scanners quickly admit your members at the
  door"; electronic member passes (Verified, versai.com).
- Gift memberships: up to "3 gift memberships in the same transaction"; buyer chooses whether each card goes to the
  giver or the recipient (Verified).
- Renewal letters "by level, date, and membership type"; kiosks prompt near-expiry members to renew; a new member is
  "instantly a member in the gift store" (Verified).
- Household entity, cardholder caps, relationships, add-ons: **Unavailable**.

Answers: (1)–(5) Unavailable; (6) Inferred: family record with children's ages tracked (so children appear to be data,
not just counts); (7) barcode card scan; (8) no evidence; (9) member history (attendance, shop, donations, camps) on the
member record — Verified; (10) no public data model or API.

---

### 6. Veevart (Salesforce)

Sources: Veevart help centre (Salesforce Experience site; two articles read in a real browser — Verified), marketing
pages (Verified text), AppExchange listing (Reported).

- **Platform:** built on Salesforce Nonprofit Cloud; "household structure" linking people and organisations (Reported,
  vendor copy) — i.e. Salesforce Account-as-household / NPC Party Relationship Groups.
- **Family membership caps are on the Membership Program (Verified, article 000002129, 2026-08-21):**
  `Number of Members Allowed` ("total number of individuals (adults and children combined)"),
  `Number of Children (Only Online)` ("must be less than the total ... each membership includes at least one adult by
  default"), `Number of Adults (Only Online)` (formula field = total − children), `Maximum Age for Children` (Veevart
  Settings; "If left blank, the default age is 17"), `Child Validation Method` ∈ {Birthday → system asks date of birth,
  Question → checkbox "is a child"}. "This feature currently applies only to sales made through the Membership Online
  app." Online flow: primary member's personal information → screen showing the limits → "Add Family Members" (with
  child check per method) → pay.
- **Selling from the Contact page (Verified, article 000002076):** the buyer's Contact → "Make a Donation" → select
  Family Membership program → Opportunity fields (`Organization` "only used for corporate memberships",
  `Primary Campaign`, `Opportunity Status` Closed Won, `Acknowledgment Status`, `Membership Start Date`) → **"Membership
  Beneficiaries" section: "If the contact already has household members, they will appear automatically as selected
  beneficiaries. You can add more beneficiaries (up to the limit allowed by the membership). To remove a beneficiary,
  click the trash icon. Removed individuals will not receive membership benefits."**
- Payments are Opportunities; recurring memberships charge a stored "Customer Card" (Reported, help article snippet).
  Digital cards target the Contact and require "the contact should have a Membership" (Reported).

Answers: (1) Salesforce household Account / NPC household — Reported. (2) Not shown; the sale is from a Contact. (3)
Yes: beneficiaries can be added beyond household members up to the cap — Verified. (4) Household members (Salesforce
relationships); child status by birthdate or checkbox — Verified. (5) Beneficiaries added/removed at sale with the trash
icon; household membership itself via Salesforce — Verified/Inferred. (6) **Named beneficiaries, pre-filled from the
household, capped by total/children/adults; children identified by age ≤ N or self-declaration** — Verified. (7)
Beneficiary records → Contact-level membership → digital card per Contact — Verified/Reported. (8) Household membership
seeds the beneficiary list but the stored coverage is the explicit selection (snapshot), not a live lookup — Inferred.
(9) Standard Salesforce household rollups — Reported. (10) Caps enforced online only ("Only Online" suffix on the
fields); ageing-out logic not documented; two sales paths with different rules.

---

### 7. ACME Ticketing

The developer portal (developers.acmeticketing.com) and support portal returned "Portal is currently not accessible" /
login in both the fetcher and a real browser. Facts below are **Reported**: search-engine snippets of those portals plus
Cuseum's integration article (which reproduces an API response).

- **Membership record:** `GET /v2/b2b/memberships` returns `membershipCategory`, `offeringName` (e.g. "Annual"),
  `membershipStanding`, `membershipStartDate`, `membershipExpirationDate`, `acmeMembershipNumber`, and a
  **`cardholders[]` array**: `cardType` ("primary"/secondary), `primaryCard`, `name`, `firstName`, `lastName`,
  `barcode`, `constituentId`, `customerId`, `acmeCustomerNumber`, `addressId`, `cardStatus`, `printStatus`,
  `expirationDate`, `ignoreUpdates`, `excludeCardOnMemberCreation` (Cuseum article; Membership Anywhere mapping uses
  `cardholders[x].primaryCard`, `cardholders[x].firstName/lastName`).
- **Cards API:** `GET v1/b2b/membership/cards` (by member id) and `/cards/{id}`; "there is currently no way to directly
  edit membership card data through the API". Membership Summary API: `membershipStanding` (active / lapsed), card-level
  standing, `primaryCard` boolean.
- **Cardholder rules:** "If the Level allows, add one or more Secondary Cardholders to the Membership. This is set in
  Back Office; Membership > Levels > configured per Level and Offering in the 'Channel Transaction Policies' tab."
  "Secondary Cardholders can come from existing Contacts, new Contacts, or can exist without having a Contact record
  (for minors, or guests)." POS: "If you check the No assigned cardholder checkbox, the default cardholder name will be
  'Guest of (Primary Cardholder)'." "A membership can have only one primary cardholder"; an API error states one primary
  cardholder must remain active through expiration. Import standing values "Active" or "Dropped" — "Dropped" when a
  cardholder is removed during a downgrade or renewal. POS is the only channel that allows
  edit/upgrade/downgrade/cancel; rejoin of lapsed memberships at POS. Back Office record shows "Named Card Holders on
  the Membership". Cardholders are specified in the B2B checkout cart; item types such as `MembershipPurchase` /
  `MembershipRenewal` set the lifecycle action.
- Household/account: none of the snippets mention a household object; cardholders carry their own `customerId` and
  `addressId`.

Answers (all Reported/Inferred): (1) No household entity; a membership is a group of cardholders, each optionally a
Customer. (2) n/a. (3) Yes — the membership itself is the group. (4) No relationship typing; secondary cardholders may
be contact-less. (5) Add/replace cardholders through lifecycle actions at POS/import; dropped cardholders get standing
"Dropped". (6) **Named cardholders (primary + secondaries), unnamed slots as "Guest of X" cards** — the clearest "slot"
model in the segment. (7) Materialised cards with barcodes; `cardStatus`/`printStatus` per card; verification by
card/member lookup. (8) No. (9) Not covered. (10) Card data read-only via API; channel-specific capabilities;
cardholder-without-customer means history cannot attach to the person.

---

### 8. Gateway Ticketing Galaxy

Configuration guides are behind the customer community (Unavailable). Vendor pages and a reseller page are Verified as
marketing statements.

- "Joint or single memberships" / "Joint membership – create one membership for multiple people" (Verified,
  gatewayticketing.com; ga.com.mx reseller).
- Pass holders: "Passholders can retrieve and display their pass from their phone"; "shoot, store and view photos in
  your Galaxy system ... photos can also be printed on membership cards"; identity verified by photo at POS/access
  control "without asking members for photo IDs" (Verified).
- Entitlements: "A membership can hold attached packages like a certain amount of guest passes, parking privileges, or
  food, beverage, or retail discounts"; these are "deferred products or entitlements" redeemable later at any POS or the
  Online Member Portal; "Benefits can be added anytime during the membership year" (Verified, Gateway blog).
- Gift memberships: buy with recipient's details billed to purchaser, or sell a ticket with an "upgrade value" and
  upgrade on site via Ticket Upgrade (Verified). Online Member Portal is "a separate section of the eGalaxy Web Store".
  "Pass Portal" assists guests with payment plans (Verified).
- Galaxy 8 adds an expanded ACS (access control) API; Galaxy Connect is the distributor API with no membership endpoints
  (Reported, blooloop/Gateway).

Answers: (1) Unavailable; the marketing vocabulary is "membership account" with pass holders. (2) Unavailable. (3) Yes —
a joint membership is one membership with several pass holders (Verified at marketing level). (4) Unavailable. (5)
Portal lets passholders "add a new membership to their account" and update demographics (Verified); adding holders not
documented. (6) **Named pass holders with photos; guest passes as counted entitlements** — Verified/Inferred. (7)
Per-holder pass media (card/phone/Apple Wallet) scanned at the gate with photo validation. (8) No evidence. (9) Member
data syncs to Raiser's Edge / a CRM (Verified). (10) Docs gated; entitlement-centric model (good for guest passes,
silent on relationships).

---

### 9. accesso Siriusware (Salesware 4.4)

The vendor's 4.4 manuals are public PDFs (portal.siriusware.com/docs/pdf/) — **Verified**. Older (v4.4), but they
document the model precisely.

- **Entities:** _guest record_ (the person), _pass_ (the membership/season pass record, with `pass_no`, `addit_no`,
  `swipe_no`/16-digit card number, start/expire/status, uses/points), _DCI_ (the item sold). "Create New Guest Record"
  item types force guest data capture at sale; "Create Pass" items create passes.
- **Families = linked guest records under a primary.** "clients who have a business practice of creating guest records
  for all family members, and then linking all members to a Primary (parent) guest record ... This allows a
  primary/parent guest record to 'own' the pass/membership, while adding in the convenience of looking up any linked
  guest record in order to locate the parent/primary guest membership and use it for validation." Linking is done "in
  Sales by using the Set Primary button on the guest record". Lookup lists show "the name of the guest who owns the
  pass" because two guests may own passes.
- **Family pass composition = modifiers + dynamic pricing.** "Family Pass Package ... The ability to restrict the number
  of individuals on a pass by type such as limit of two Adults per pass ... Unlimited quantities ... such as Youth and
  Child." Rules such as `IF(HASMOD(,,CHILD,>10),PREVENTUSE(No more than 10 children are allowed))` and
  `SETPARENTPASSVAL(a_maxqty1,2)` (Silver Family Membership: ADULT max 2, CHILD max 2, GUEST max 1). Each modifier is
  itself a Create Pass item, so **each family member gets a pass record**; the e-commerce flow "prevents the customer
  from selecting more than two adult passes (Adult+Adult, Adult+Senior or Senior+Senior)".
- **Shared value / parent pass validation (Validation guide):** "A 50-Day pass package allows an entire family to share
  fifty days' worth of use. Each family member has his own pass, but when the linked guests' passes are validated the
  uses are all deducted off of the parent pass." Configured by "Validate parent instead of this / Parent is determined
  by: Parent Guest or Master Pass No." Limitation stated: "Parent pass validation validates the first pass located for
  the parent." Linked passes display as
  `Pass: 4003001*, PEPPER SCOTT, Master Pass / Pass: 5003001, MAXIE SCOTT, Family Member Pass`.
- **Gate logic = cascading validation.** Scanning a guest number or 16-digit card "first locates the guest record, and
  then generates a list of all passes owned by the guest", ordered by the "Validation Precedence" menu; lost cards are
  voided ("Stop cascade if pass record scanned is voided"). Photos are stored on the guest record; BatchPrint can skip
  "passes for guests that do not have a photo".
- **Benefit Management:** benefits defined with Quantity ("Sum All Autosale Max Qtys ... In a Family Membership, two
  adults and two children are associated and the auto sum would be four of each item"), Timeframe
  (daily/weekly/monthly/annually/lifetime), linked DCIs; "each time a benefit is redeemed, it is recorded".
- Add-ons: "add-on vouchers or coupons attached to a membership are modeled as pass modifiers that inherit guest
  information from the parent record" (Reported snippet of the advanced guide). Gift memberships: a "JCA gift
  membership" preference exists (Verified TOC).

Answers: (1) **No household entity: a tree of guest records linked to a primary guest** — Verified. (2) n/a. (3) Yes:
one guest buys a family pass whose modifiers create passes for each named person — Verified. (4) No typed relationships;
just parent/child guest linkage and item types (Adult/Senior/Youth/Child) — Verified. (5) Set Primary on the guest
record; Master Pass link editable after sale — Verified. (6) **One pass per person** plus counted GUEST modifiers;
children are typed items, ages enforced by item eligibility — Verified. (7) Each person's pass is scanned; shared
counters via parent-pass validation; cascade finds any valid pass on the guest — Verified. (8) Partly: lookup through a
linked child finds the parent's pass (relationship used at validation time) — Verified. (9) Families "can share the same
address record" (Reported, reporting guide). (10) Composition rules are macro scripts; "first pass located for the
parent" ambiguity; the link is a single-parent pointer, not a household with roles.

---

### 10. Spektrix (arts)

Support Centre articles (read through a reader proxy; site blocks bots) and integrate.spektrix.com — **Verified**.

- **Household** = "a grouping of one or more Customer Records, used for analysis and mailings". "By default, each
  Individual is in their own single-person Household"; "Each Individual can belong to only one Household"; organisations
  cannot join. One **Primary Householder** ("the main contact for the Household"; must have an address; household adopts
  that address); shared **Addressee Name**. Join via Actions → "Join a Different Household"; leave via "Leave Household"
  (must appoint a new Primary first); logged in the Customer Audit.
- **Relationships** are separate, typed (Individual↔Individual, Individual↔Organisation, Organisation↔ Organisation),
  with roles per direction, importance 1–5, optional perpetual flag; a type enables "Can act on behalf of" → Facilitated
  Orders. "Customers do not need a Relationship to be in a Household together; however, without a Relationship they
  won't be able to make purchases on behalf of one another or soft credit their Orders."
- **Orders and giving across the household:** other members' orders show as "Household Order" with "Made by"; "Household
  Orders aren't credited as income to members who aren't the Order Owner, Facilitator, or a Soft Credit recipient"; soft
  credit requires a Relationship and never double-counts income.
- **Memberships are strictly per customer.** "A Membership is bought by a Customer, who then becomes a Member"; "Members
  can only purchase one Membership of the same type at a time"; a second person needs their own Customer Record. For a
  "joint Membership" the guidance is Soft Credit + Auto Tags; the Membership Web Component can capture a second member's
  name via Order Attributes. Benefits are not on the membership but via Offers, commissions, Priority Booking and the
  **Customer Eligibility Chooser** (criteria: Tags, "current" Memberships — renewed qualify, expired and grace-period do
  not — Subscriptions, Promotion Codes, Attributes). Nothing in the eligibility article extends a benefit to related or
  household customers.
- Gift memberships are Gift Vouchers redeemed as Account Credit on the recipient's record. API:
  `GET customer?$expand=subscriptions` → `autoRenew`, `membership.id`, `startDate`, `expiryDate`, `canRenew`; tags via
  `$expand=tags`; no household/relationship endpoints in the customer docs.
- Digital Customer Passes can verify Membership status at entry; a Customer Group can flag active Members.

Answers: (1) Household = separate grouping with one primary; relationships orthogonal — Verified. (2) No: only
individuals hold memberships — Verified. (3) No: one membership = one customer; workarounds are tags/soft credit —
Verified. (4) Typed relationships with per-direction roles; household carries no roles — Verified. (5) Join/Leave
Household actions, audited — Verified. (6) None: no cardholders; second name is an order attribute — Verified. (7)
Eligibility is evaluated per customer from tags/memberships — Verified. (8) **Not natively**; Auto Tags can be built on
household data (Reported, community blog). (9) Shared address via Primary Householder; household order visibility
without credit; soft credit for recognition — Verified. (10) The joint membership gap is explicit; aggregation needs
custom reports keyed on Household ID.

---

### 11. PatronManager (Salesforce, arts)

help.patronmanager.com articles — **Verified**.

- **Account = household.** "Think of an Account as a building, like a house or a business - it contains people who
  either live or work in it, i.e. individual Contacts." Creating a Contact auto-creates an Account; record types "Patron
  Contact" for Household/Individual Accounts, "Other Contact" for Organization/Foundation. Address, donation roll-ups
  and "Do Not Mail" live on the Account; email, "Email Status", "Birth Date", Benefit level on the Contact. Field sync
  Account↔Contact happens **only when the Account has exactly one Contact**. Multiple affiliations via Salesforce
  Related Accounts ("Add Relationship" with Roles); "Donation and Ticket Order history appears only for Contacts with a
  direct relationship to an Account". The old Affiliations app is retired (Reported).
- **Memberships are Benefit records.** Each purchase/renewal creates a Benefit linked to the buying Contact, its Account
  and the Ticket Order; "Renewed By" links successive Benefits; terms annual or monthly (30-day auto-renew with a
  Pending Renewal order 7 days before).
- **Household Memberships (Jan 2023):** set the Benefit Level's `Membership Type` to **Household** (vs Individual); then
  "its perks ... apply to an entire Account (Household), rather than just an (Individual) Contact". Fields: Account —
  `Member Status` badge, `Latest Account Membership` ("Gold | Individual"), `Latest Membership Expiration`; Contact —
  `Member Status`, `Latest Contact Membership`, `Latest Membership Expiration`, **`Member Role` = `Main Contact`
  (purchaser) or `Associated Contact`** ("covered by a Household Membership but not the buyer"). Badges/rollups update
  **overnight**; discounts and Portal access are immediate; "Portal logins are created automatically only for the Main
  Contact"; "Discount limits are per Membership and shared among covered Contacts". **"Household Memberships cannot
  limit the number of Contacts, or specify adults versus children, or track names of covered members."** Changing a
  Benefit Level's type adds or removes badges for Contacts overnight.
- Digital Membership Cards issued at purchase or sent manually; perks via Discount Codes and presale passcodes attached
  to Benefit Levels; Fair Market Value splits the deductible portion.

Answers: (1) Household = Salesforce Account — Verified. (2) Effectively yes: a Household-type Benefit covers the whole
Account, though the Benefit is still owned by the buying Contact — Verified. (3) No: coverage is "everyone in the
Account"; no named extras — Verified. (4) Membership in the Account (no typed roles beyond Related Account roles);
`Member Role` is only Main/Associated — Verified. (5) Move/split Contacts between Accounts (Salesforce); nightly job
recomputes coverage — Verified. (6) **Unbounded: every Contact in the Account**, no caps, no names — Verified. (7)
Nightly materialised badges/rollups + immediate rule-based discounts; dynamic in intent, batch in practice — Verified.
(8) Yes, via Account membership (not via typed relationships) — Verified. (9) Account rollups; Account-level Do Not
Mail; history stays on the Contact — Verified. (10) Nightly latency; no caps/ages/names; portal license per covered
person; field sync breaks at two Contacts.

---

### 12. Centaman

Vendor pages (Verified as marketing), an integrator article (Reported), API brochure (Verified, no schema).

- "Household Memberships with Individual Identity" is a listed feature; stored-value accounts "can be shared among
  household members"; "Parents can set daily spend limits for children on shared accounts" (Verified, centaman.com).
- Cards: "Physical or Digital Photo ID Membership Cards"; "with or without member photos as identity validation"; "Photo
  Validation and Pop-Up Member Notes with Each Scan"; "Decremented Access Control" vs unlimited; "Package and Guest
  Package Management" (Verified).
- API: "fully functioning read/write API" with a ReadMe-hosted Developer Hub (gated); integrator client used
  `get_member_by_email()`, member documents with `MemberCode`, `MemberNumber`, `FirstName`, `LastName`, `Email`,
  `HomeAddress`, `JoinDate`; the museum re-synced membership status on every page that checks it (Reported,
  interaction.net.au). Partner Cuseum reads tiers/passes for wallet cards (Reported).

Answers: (1) a household construct exists ("Household Memberships with Individual Identity") — Verified headline,
Unavailable detail. (2)–(5) Unavailable. (6) Inferred: individual identities (photo cards) under a household membership;
guest packages counted. (7) Photo-validated scan per person; decremented entitlements — Verified. (8) No evidence. (9)
Shared stored-value accounts across the household — Verified. (10) Docs gated.

---

### 13. "Vectra"

No membership product named **Vectra** or **Vektra** used by museums or clubs is findable (two searches, plus "Vectra
membership management software"). The only near-match is **Avectra** (netFORUM Enterprise/Team/Pro), an
association-management vendor founded 1993 and since acquired (Reported, PitchBook) — an AMS, not a museum system.
Closest comparable products for this brief: **Veevart** (Salesforce museum platform, §6) and **Versai** (§5). Treat
"Vectra" as Unavailable.

---

### 14. Patterns

**P1. Four distinct answers to "who is covered".**

| Model                               | Products                                                                                                                                                                         | How coverage is stored             |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| Materialised member/cardholder rows | Altru/CRM (`MEMBER` + `MEMBERSHIPCARD`), RE NXT (card rows with `joint_member_id`), ACME (`cardholders[]`), Siriusware (one pass per person), Veevart (Membership Beneficiaries) | explicit list, caps from the level |
| Whole-container coverage            | PatronManager (Household Benefit → every Contact in the Account)                                                                                                                 | no list; recomputed nightly        |
| Relationship-driven lookup          | Tessitura (`includeAffiliations`, `BenefitProvidingConstituentId`, constituency cascade)                                                                                         | no list; evaluated at read time    |
| Strictly individual                 | Spektrix (one customer = one membership; joint = soft credit + tags)                                                                                                             | none                               |

**P2. Counts for the nameless.** Children (Altru `NUMBEROFCHILDREN`/`CHILDRENALLOWED`, RE NXT `total_children`, Veevart
`Number of Children`), unnamed guests (Altru `NUMBEROFADDONGUESTS`, Galaxy guest-pass entitlements, Siriusware GUEST
modifier, ACME "Guest of X" cards) are modelled as _slots_, not people. Only Veevart and Siriusware carry an age/type
test at the point of sale; none of the public docs show automatic ageing-out mid-term.

**P3. Household is always a party, never a relationship.** Where a household exists it is a record with an address and a
name: a group constituent (Altru/CRM `ISGROUP`, Tessitura household constituent type with A1/A2), a Salesforce Account
(PatronManager, Veevart), or a grouping with a Primary Householder (Spektrix). Spousal, parental and guardian semantics
are _always_ in a separate relationship structure (CRM `RELATIONSHIP`, Tessitura Affiliation type / Associations,
Spektrix Relationships, Salesforce Related Accounts), and no product derives household membership from relationships
automatically.

**P4. Holder vs payer is explicit and separate from "primary".** Altru `GIVENBYID`/`ISGIFT`/`SENDRENEWALCODE`; RE NXT
`given_by_id`, `send_benefits_to`, `send_notice_to`; Tessitura `CrediteeId`; Spektrix gift voucher → recipient credit;
Versai giver/recipient card choice. Primary member is a flag on a member/card row, not a column on the membership.

**P5. The gate reads a person-level token.** Every admissions product resolves a scan to a _person's_ card or pass
(Altru card number → MEMBERSHIPCARD → MEMBER; Siriusware guest/card → cascade over the guest's passes; Galaxy/Centaman
photo-validated passes; ACME barcode per cardholder; Tessitura `Search/ByCardNumber`). Products that cover a container
(PatronManager) or evaluate relationships (Tessitura) still issue per-person cards or rely on name lookup. Siriusware's
cascade ("list all passes owned by the guest, ordered by Validation Precedence") and Altru's
`V_QUERY_MEMBERSHIPCARD_PRINTOUTPUT` are the only public specs of _how_ the entitlement is resolved; neither returns a
reason.

**P6. Lifecycle is a transaction ledger.** Altru `MEMBERSHIPTRANSACTION` (Join/Renew/Upgrade/Downgrade/Drop/
Rejoin/Cancel, mid-term vs renewal upgrade), RE NXT `/upgrade` `/downgrade` "during cycle", `/drop`, `/rejoin`;
PatronManager Benefit chain via "Renewed By"; ACME lifecycle actions with "Dropped" cardholder standing.

**P7. Benefits are quantities tied to member count.** Altru
`MEMBERSHIPLEVELBENEFIT.NUMBERTOOFFERCODE = based on member count`; Siriusware "Sum All Autosale Max Qtys";
PatronManager "Discount limits are per Membership and shared among covered Contacts"; Galaxy deferred entitlements.

**P8. Recognition credit is configured, not derived.** CRM `HOUSEHOLDINFO` six switches; Spektrix soft credit needs a
Relationship; PatronManager rolls up to the Account but keeps history on the Contact.

### 15. Weaknesses worth avoiding

1. **Children and guests as bare integers** (Altru, RE NXT, Siriusware GUEST) — no identity, no ageing-out, no audit of
   who used the slot. If Rhythm keeps unnamed slots, model them as _slot_ rows with optional person, age rule and usage
   history rather than a count column.
2. **Two parallel grouping systems with no bridge** (CRM `GROUPMEMBER` vs `RELATIONSHIP`; Spektrix Household vs
   Relationship; Tessitura Affiliation vs Association). Staff maintain both; neither can explain the other.
3. **Holder identified only by a flag on a child row** (Altru `MEMBER.ISPRIMARY`, ACME one-primary rule, RE NXT "primary
   member card cannot be deleted"). Transfers of a membership become data surgery.
4. **Whole-container coverage without caps or names** (PatronManager: "cannot limit the number of Contacts, or specify
   adults versus children, or track names"). Admissions venues cannot run a family level on it.
5. **Nightly materialisation of eligibility** (PatronManager badges/rollups) — a new household member or a lapse is
   wrong for up to a day at the door.
6. **Relationship-driven coverage with no explicit roster** (Tessitura) — hard to cap, hard to print a card list, and
   the practitioner community avoids household-level tracking because attendance is individual.
7. **Composition rules as scripts** (Siriusware dynamic-pricing macros, `PREVENTUSE(...)`) — rules live outside the data
   model and cannot be queried or explained.
8. **Cardholders without a person record** (ACME "Guest of (Primary)", secondary cardholders with no Contact) —
   convenient at the till, but history, communications and de-duplication have nowhere to attach.
9. **Global instead of per-program household policy** (CRM `HOUSEHOLDINFO` is a one-row table).
10. **Merge semantics that drop relationships** (Doubleknot deletes Member Relationships and Contact Notes on merge).
11. **Channel-dependent rules** (Veevart caps "Only Online"; ACME upgrade/downgrade/cancel "POS only").
12. **No "why admitted" output anywhere.** None of the public references return an entitlement explanation; the closest
    are Siriusware's ordered pass list and Altru's card print view. A first-class `explain(person, date)` result would
    be a genuine differentiator.
13. **Card = member** coupling (Altru `MEMBERSHIPCARD.MEMBERID` is the only link) blocks cards for unnamed slots and
    makes "one person, several memberships" awkward; RE NXT's `no_of_cards` per member row and `card_expires` offsets
    show the pressure to decouple.
14. **API instability around memberships** (Tessitura v15 → v16 "will change significantly"; RE NXT `/skyapi/` path
    break; ACME card data read-only).

### 16. Pages that could not be fetched

- developers.acmeticketing.com and support.acmeticketing.com (all articles): redirect to Freshworks login; browser shows
  "Portal is currently not accessible". Used search snippets + Cuseum only.
- webfiles-sc1.blackbaud.com `.../crm-db/tables/*.html` (wrong path, 404) — correct path is `/content/*.html`.
- `usp_dataformtemplate_add_membershipaddmember.html` — 404 (procedure name guessed).
- Tessitura `ResourceModel?modelName=MembershipLevel|MembershipOrganization`, `GET-CRM-Constituencies_...` — generic
  "Error" pages. `Help/Resources/MembershipLevels` and `MembershipOrganizations` list endpoints only.
- support.spektrix.com direct fetch — 403 (bot block); read via r.jina.ai proxy instead.
- veevart.com/memberships direct — 403; read via proxy. veevart.my.site.com via fetcher — "CSS Error"; read in the
  Browser pane instead.
- cabq.gov Siriusware DSA training PDFs — 404 (both URL encodings).
- help.pm.leapevent.tech — 301 to help.patronmanager.com (followed).
- Doubleknot DKNOT_Docs: only the Member Profiles guide exists publicly; no Membership Management guide found.
- Gateway Galaxy configuration guides, Centaman Developer Hub (ReadMe), Versai documentation — customer-only.
- developer.blackbaud.com/skyapi/apis/membership and the changelog via fetcher — JS shell ("Blackbaud"); read in the
  Browser pane.
- community.tessitura.com beyond the one TLCC PDF — login.

### 17. Evidence table

V = Verified, R = Reported, I = Inferred, U = Unavailable.

| Product                        | Q1 household entity     | Q2 household owns membership | Q3 individual covers many w/o household | Q4 relationships            | Q5 add/remove members | Q6 beneficiaries | Q7 benefit flow | Q8 dynamic via relationships | Q9 household giving/comms/history | Q10 compromises |
| ------------------------------ | ----------------------- | ---------------------------- | --------------------------------------- | --------------------------- | --------------------- | ---------------- | --------------- | ---------------------------- | --------------------------------- | --------------- |
| Altru / Blackbaud CRM (DB ref) | V                       | I (no)                       | V (yes)                                 | V                           | V                     | V                | V               | V (no)                       | V                                 | V               |
| Raiser's Edge NXT (SKY API)    | V (none in API)         | U                            | V (yes)                                 | V (relationship_id on card) | V (card routes)       | V                | V               | I                            | U                                 | I               |
| Tessitura (REST help)          | V                       | I                            | I (no)                                  | V                           | V                     | V                | V               | V (yes)                      | V                                 | V/R             |
| Doubleknot                     | V/R (none)              | U                            | I                                       | V (feature exists)          | U                     | I                | V               | U                            | V                                 | V               |
| Versai                         | U                       | U                            | U                                       | U                           | U                     | I                | V               | U                            | V                                 | I               |
| Veevart                        | R                       | U                            | V (yes)                                 | V                           | V                     | V                | V/R             | I                            | R                                 | V               |
| ACME                           | R                       | U                            | R (yes)                                 | R                           | R                     | R                | R               | R (no)                       | U                                 | R               |
| Gateway Galaxy                 | U                       | U                            | V (joint)                               | U                           | V (partial)           | V/I              | V               | U                            | V                                 | I               |
| Siriusware                     | V (none; linked guests) | U                            | V (yes)                                 | V (none typed)              | V                     | V                | V               | V (partial)                  | R                                 | V               |
| Spektrix                       | V                       | V (no)                       | V (no)                                  | V                           | V                     | V (none)         | V               | V (no)                       | V                                 | V               |
| PatronManager                  | V                       | V (yes)                      | V (no)                                  | V                           | V                     | V                | V               | V (yes)                      | V                                 | V               |
| Centaman                       | V (headline)            | U                            | U                                       | U                           | U                     | I                | V               | U                            | V                                 | I               |
| "Vectra"                       | U                       | U                            | U                                       | U                           | U                     | U                | U               | U                            | U                                 | U               |

### 18. Sources (URL → what it supports)

Blackbaud CRM / Altru DB reference (all Verified,
`https://webfiles-sc1.blackbaud.com/files/support/helpfiles/crm-db/content/`):

- `groupmember.html` → GROUPMEMBER columns, FKs, triggers, dependents.
- `groupdata.html` → GROUPTYPECODE 0 Household / 1 Custom, NAMEFORMATFUNCTIONID.
- `groupmemberdaterange.html` → DATEFROM/DATETO/COMMENTS; end-dating household membership.
- `groupmemberrole.html` → dated roles via GROUPMEMBERROLECODE.
- `householdinfo.html` → the six recognition switches, HOUSEHOLDSCANBEDONORS, ensure-one-row triggers.
- `constituent.html` → ISGROUP, ISORGANIZATION, ISCONSTITUENT, DISPLAYNAME ("Display name for households").
- `relationship.html` → ISSPOUSE, reciprocal type codes, no household column.
- `ufn_group_getcurrentmembers.html`, `v_query_constituentgroupmember.html` → current-member date logic; view filters
  GROUPTYPECODE = 1.
- `membershipprogram.html` → program types, ALLOWMULTIPLEMEMBERSHIPS, ALLOWADDONADULT/GUEST + prices, CATEGORYCODE
  (Museum, Zoo/Aquarium...), card printing fields.
- `membershiplevel.html` → MEMBERSALLOWED, CHILDRENALLOWED, CARDSALLOWED, grace windows, Lapsed/Expired, Rolling/Fixed.
- `membershiplevelbenefit.html` → NUMBERTOOFFERCODE "based on member count".
- `membershipleveltype.html`, `addon.html` → level types; ADDONTYPE Benefit / Additional member.
- `membership.html` → NUMBEROFCHILDREN, NUMBEROFADDONADULTS/GUESTS, ISGIFT, GIVENBYID, SENDRENEWAL, STATUS codes,
  LASTCARDNUMBER; no holder column.
- `member.html` → ISPRIMARY, ISDROPPED, ISCAREGIVER, FKs; `tr_member_validmembercheck.html` →
  duplicate/multiple-membership rules.
- `membershipcard.html` → MEMBERID-only ownership, NAMEONCARD, status codes; `v_query_membershipcard_printoutput.html` →
  join path and address source.
- `membershipaddon.html`, `membershiptransaction.html` → add-on instances; action codes and UPGRADEMETHOD (midterm).
- `salesorderitemmember.html` → members chosen at point of sale, MEMBERID for renewals.
- `usp_dailysales_membershiprenewal_getmembersandcards.html` (search snippet) → counts non-dropped members, caps cards
  at CARDSALLOWED.

Raiser's Edge NXT (Verified, read in browser):

- https://developer.blackbaud.com/skyapi/support/changelog/renxt → Membership API LTS route list (2026-07-08), card
  routes, "primary member card cannot be deleted", upgrade/downgrade/drop/rejoin rules.
- https://developer.sky.blackbaud.com/api#api=membership&operation=CreateMembershipV2 → MembershipCreateV2 fields
  (total_members, total_children, given_by_id, send_benefits_to, membership_cards[]).
- https://developer.sky.blackbaud.com/api#api=membership&operation=AddMembershipCardsAsync → MembershipCardCreate
  (joint_member, joint_member_id, relationship_id, relation_type, card_type, card_expires, address_to_print, status).
- https://community.blackbaud.com/blogs/69/9972 (Reported) → `/skyapi/` path removal 2025-03-10.

Tessitura (Verified unless noted, `https://ts-stg-appgw.calacademy.org/TessituraService/Help/`):

- `Help` (index) → full resource list; no MemberCards/Households/Entitlements resources.
- `Resources/Memberships`, `Api/GET-CRM-Memberships-Details_...`, `ResourceModel?modelName=MembershipDetail` →
  MembershipDetail fields incl. BenefitProvidingConstituentId, includeAffiliations; deprecation warning.
- `Api/POST-CRM-Memberships-Calculate` → CrediteeId, RenewUpgradeIndicator, DeclineBenefits, MembershipLevelOverride.
- `ResourceModel?modelName=MembershipLevelSummary` → StartAmount/EndAmount/Rank/ConstituencyType.
- `Resources/Affiliations`, `ResourceModel?modelName=Affiliation&HttpMethod=POST`, `Api/POST-CRM-AffiliationInfo`,
  `Resources/AffiliationInfo`, `ResourceModel?modelName=AffiliationType` → affiliation model and filters.
- `Resources/Constituents`, `Api/GET-CRM-Constituents-PrimaryHousehold_constituentId`,
  `ResourceModel?modelName=Constituent&HttpMethod=PUT`, `ResourceModel?modelName=ConstituentType`,
  `ResourceModel?modelName=ConstituentGroup`, `Api/POST-CRM-Constituents-Search-ByCardNumber` → household conversion
  endpoints, A1/A2 swap, name-field notes, card-number search.
- `Resources/Relationships`, `Resources/Constituencies`, `Api/GET-CRM-Internal-ConstituentMiniSnapshot-constituentId` →
  affiliations + associations; constituency cascade; snapshot affiliates.
- https://www.studiobravo.com.au/blog/tessitura-v16-key-changes-in-the-new-release/ (Reported) → v16 membership
  overhaul, Benefits types, API/reporting impact.
- https://support.cuseum.com/en/articles/8549975-tessitura-integration (Reported) → endpoints an integrator uses for
  member cards.
- https://community.tessitura.com/.../TLCC-2016-...Barcode-Hack-Notes.pdf (Reported) → "not all members of a household
  will attend"; constituent-ID barcodes.
- https://www.tessitura.com/features (Reported, via search) → digital membership cards live in v16.0.34.

Doubleknot:

- https://app.doubleknot.com/DKNOT_Docs/DKGuide_ManagingMemberProfiles.pdf (Verified) → Member Details sections, Member
  Relationships, merge semantics.
- https://app.doubleknot.com/document/solutions-for-membership-management/161143 (Verified, marketing) → scanning cards,
  name lookup, gift memberships, mobile cards.
- https://app.doubleknot.com/openrosters/ViewOrgPageLink.aspx?orgkey=580&itemkey=11376 (Verified) → Member vs Membership
  Management tabs.
- https://www.doubleknot.com/integrations-and-partnerships (Verified) → "open API", Raiser's Edge/ClearView sync, no
  schema.
- https://www.doubleknot.com/blog/6-powerful-membership-management-features-for-resiliency-in-2021 (Verified,
  aspirational) → portal, cards, pauses.
- https://www.softwareadvice.com/nonprofit/doubleknot-profile/ (Reported, via search snippet; page 403) → "no
  relationship or household feature".
- https://app.doubleknot.com/termsofservice.htm (Reported) → definition of MEMBER.

Versai: https://versai.com/feature/member-management, https://versai.com/feature/ticketing-kiosks-scanners (Verified
marketing); https://www.capterra.com/p/121532/Explorer/ (Reported) → "individuals and families".

Veevart:
https://veevart.my.site.com/support/s/article/Manage-Limits-for-adults-and-children-for-Family-Memberships-Online
(Verified) → caps, child age, validation method;
https://veevart.my.site.com/support/s/article/How-to-Sell-Family-Memberships-Programs-from-Contact-Page (Verified) →
Membership Beneficiaries pre-filled from household; https://veevart.com/memberships (Verified marketing); AppExchange
listing a0N3000000B5c24EAB (Reported) → Nonprofit Cloud household structure.

ACME (Reported):
https://support.cuseum.com/en/articles/9796436-acme-ticketing-integration-with-cuseum-for-digital-membership-cards →
`/v2/b2b/memberships` JSON with `cardholders[]`; search snippets of
https://developers.acmeticketing.com/support/solutions/articles/33000250666-membership-cards,
.../33000250672-membership-summary-api, .../33000250732-b2c-process-membership-lifecycle-actions,
.../33000248662-api-response-codes-and-error-messages,
https://support.acmeticketing.com/support/solutions/articles/33000246457-acme-salesforce-visualforce-guide,
.../33000244420-memberships-in-acme-sales-pos-, .../33000258277-viewing-membership-records,
.../33000257789-membership-import-new-memberships-and-other-lifecycle-actions,
.../33000259360-membership-anywhere-emembership-cards-mapping → secondary cardholders, Channel Transaction Policies,
"Guest of", one primary, Dropped standing, POS-only actions.

Gateway Galaxy: https://www.gatewayticketing.com/products-services/products/membership/,
https://www.gatewayticketing.co.uk/products/membership/,
https://www.gatewayticketing.com/resources/common-membership-annual-pass-needs-and-the-galaxy-solutions-to-consider/
(Verified marketing) → joint memberships, photos, entitlements, portal, gift upgrade path;
https://ga.com.mx/en/software-2/gateway-ticketing-system/ (Reported) → "one membership for multiple people";
https://supergood.ai/docs/gateway-ticketing-systems-api (Reported) → docs gated.

Siriusware (Verified): https://portal.siriusware.com/docs/pdf/MASTER_44_Memberships_and_Passes.pdf → Family Pass
Package, modifiers/dynamic pricing, primary (parent) guest ownership, Benefit Management;
https://portal.siriusware.com/docs/pdf/MASTER_44_Validation.pdf → parent pass validation, Set Primary, cascading
validation, Validation Precedence; https://portal.siriusware.com/docs/pdf/MASTER_44_Salesware_Reporting.pdf and
MASTER_44_User_Guide_for_Advanced_Users.pdf (Reported snippets) → shared address record; add-ons inherit guest info.

Spektrix (Verified): https://support.spektrix.com/hc/en-us/articles/360026521151-Setting-Up-and-Using-Households;
.../360002446552-Customers-and-Relationships; .../360028748052-Introduction-to-Memberships;
.../201668063-Customer-Eligibility; .../225508727-Gift-Memberships; https://integrate.spektrix.com/docs/customer →
customer API with `$expand=subscriptions`;
https://community.spektrix.com/public/blogs/smarter-segmentation-with-household-data-in-spektrix (Reported) → Primary
Householder in Auto Tags.

PatronManager (Verified): https://help.patronmanager.com/a/1668351 → Household Membership fields, Member Role, nightly
timing, no caps/names; https://help.patronmanager.com/a/1637935 → Jan 2023 release;
https://help.patronmanager.com/a/879634 → Benefits model, renewals, FMV; https://help.patronmanager.com/a/829684 →
Contacts vs Accounts, one-Contact sync rule; https://help.patronmanager.com/a/1064794 → Contact creation/record types;
https://help.patronmanager.com/a/940006 → Related Accounts and roles; https://help.patronmanager.com/a/829705 (Reported)
→ Affiliations app retired.

Centaman: https://www.centaman.com/membership-software-crm/ (Verified marketing) → "Household Memberships with
Individual Identity", photo validation, shared stored value; https://www.centaman.com/api-and-integrations/ and
Centaman_Connected-API.pdf (Verified) → read/write API, ReadMe hub, no schema;
https://interaction.net.au/articles/integrating-centaman-with-australian-museum-website/ (Reported) → member fields and
re-sync behaviour.

Vectra: https://pitchbook.com/profiles/company/13187-98 (Reported) → Avectra/netFORUM is the only near-match.

Reciprocal programs (context only): https://narmassociation.org/faqs/, https://exploration.org/astc/,
https://www.discoveryworld.org/wp-content/uploads/2023/01/Understanding-Your-ASTC-Travel-Passport-Program-Benefits-FAQ.pdf
(Reported) → programs are per-institution-level benefits verified by a logoed card plus ID; no vendor publishes a
reciprocal flag in its public model.

## 33. W6 — Church management and community/alumni platforms: households, relationships, giving units, guardian authorization

## W6 — Church management and community/alumni platforms: households, relationships, giving units, guardian authorization

Research memo for the Rhythm 2.0 households / constituent-relationship / membership-coverage architecture
recommendation. Written 2026-10-21 (ET). Companion to w1 (Blackbaud), w2 (netFORUM / iMIS), w3 (Salesforce, Tessitura,
others).

Evidence labels used throughout:

- **Verified** — vendor developer reference, vendor help documentation fetched directly, or open source code read
  directly.
- **Reported** — vendor help text seen only as a search excerpt, community forums, training transcripts, church-run help
  pages, third-party wrappers.
- **Inferred** — my reading of the above; stated as such.
- **Unavailable** — page could not be fetched or no public material found (listed at the end).

Internal table or column names are stated only where open code (Rock RMS) or a published API reference shows them.

---

### 0. Headline findings

1. **Every church system models the family as a first-class container with a per-member role**, not as a bag of pairwise
   relationships. The two dominant shapes are (a) _family-as-group-with-roles_ on a generic group engine (Rock RMS:
   Family is a `GroupType`; roles Adult/Child are `GroupTypeRole`s; membership is `GroupMember`) and (b)
   _household-as-dedicated-entity_ (Planning Center `Household` + `HouseholdMembership.household_role`; MinistryPlatform
   `Household` + `Household Position`; FellowshipOne `Household` + `householdMemberType`; CCB `Family` +
   `family_position`; Realm `Family Position`; Breeze `family_role`).
2. **Non-household relationships are kept in a separate structure in the mature products** — Rock "Known Relationships"
   (a second group type with reciprocal roles and automatic inverse creation), MinistryPlatform "Contact Relationships"
   (reciprocal trigger, start/end date), Realm "Relationships" (added Nov 2022), Planning Center "Trusted People / Not
   Authorized" (check-in-specific). Breeze, Elvanto, CCB and FellowshipOne have no general relationship structure; they
   overload the family roles ("Other", "Visitor", "Sibling", "Grandfather").
3. **Multiple-household membership is the dividing line.** Rock, Planning Center, MinistryPlatform ("Other Households"),
   Realm (children only), TeamSnap and CampMinder (two households per camper) allow a person in more than one household.
   Breeze ("a person can only be part of one family at a time"), CCB (scalar `familyId`), FellowshipOne (scalar
   `householdID`) and Elvanto (scalar `family_id`) do not; those systems push split custody into check-in authorizations
   or into "Visitor"/"Other" roles.
4. **Giving is never owned by the household record itself.** It is either (a) a _giving unit / giving group_ pointer on
   the person (Rock `GivingGroupId` → `GivingId` "G{groupId}" or "P{personId}"; Shelby Arena `giving_unit_id` +
   "Contribute Individually"), (b) a _statement-type flag on the donor record_ (MinistryPlatform Donor.Statement Type =
   Individual | Family; FellowshipOne "Attributed to: Household" shares a gift equally between Head and Spouse), or (c)
   a _pairwise donor link_ independent of household (Planning Center "joint donors"; Realm "shared giving" check box /
   giving number). The household is a _default_ for the giving unit, not its owner.
5. **Guardian / check-in authorization is a distinct, directional, per-child grant**, not derived from household
   membership alone: Rock "Allow Check In By" / "Can Check In" inverse roles (any Known Relationship role can be flagged
   Can Check-in); Planning Center household adults + Trusted People, with Not Authorized as a negative list; CCB
   `authorized_individuals` on the family plus an _Age Threshold_ for child-profile visibility; MinistryPlatform Other
   Households only count for check-in when the Household Type matches the check-in configuration.
6. **Life-event handling is procedural everywhere** (tools: "Move to new family", "Split Household Tool", "Separate
   Household", "Change Household", "autoadult" scripts). Only Rock computes anything automatically (AgeClassification,
   family role by age via a job, PrimaryFamilyId, GivingLeaderId), and even there the adult-child-to-own-family move is
   a data-automation setting, not a rule. Every community thread about divorce, adult children or deceased spouses is
   about the _giving unit not following the person_.
7. **Youth / sports / camp systems invert the model**: the _account_ (adult, 18+) is the root; children are login-less
   sub-profiles; guardians are invited to a child's profile with a pending state; a waiver is acknowledged by the
   account holder whose identity must match; a second household is an opt-in on the camper for mail and invoices, with
   payment rights reserved to the primary household.

---

### 1. Rock RMS (open source; code read from github.com/SparkDevNetwork/Rock, branch `develop`)

**(1) What is a family?** A **Group** of GroupType "Family"
(`Rock.SystemGuid.GroupType.GROUPTYPE_FAMILY = 790E3215-3B10-442B-AF69-616C0DCB998E`). Group's class remark: "In Rock
any collection or defined subset of people are considered a group", and a group "can be a family, small group, Bible
study, security group, etc." Roles are GroupTypeRoles `GROUPROLE_FAMILY_MEMBER_ADULT` (2639F9A5-…) and
`GROUPROLE_FAMILY_MEMBER_CHILD` (C8B1814F-…). Membership is a `GroupMember` row (GroupId, PersonId, GroupRoleId,
GroupMemberStatus Active/Inactive/Pending, IsArchived, DateTimeAdded, InactiveDateTime, GroupOrder). The GroupType has a
`GroupTerm` ("family" is given as an example) and the Group entity carries an `[Analytics("GroupTypeId","10",…)]`
attribute commented "Support Analytics Tables, but only for GroupType Family". **Verified (code).** Docs: "Every person
you add belongs to a family"; families "store a household's address and campus". **Verified (docs).**

**(2) Can the family own anything?** The family group owns **addresses** (GroupLocation: home, work, previous; one
"mapped" location — "Rock uses exactly one address for the map"), **campus** (`Group.CampusId`; `SaveNewGroup` sets
CampusId "only when the group type is the family group type"; docs: "Campus is set at the family level, so changing it
updates every member"), **record status** (marking a family Inactive "will also inactivate each member of the family"),
**family attributes**, and two computed display strings `GroupSalutation` ("Ted & Cindy Decker") and
`GroupSalutationFull` ("Ted, Cindy, Noah and Alex Decker"). It does **not** own giving: giving hangs off
`Person.GivingGroupId` (see 6). Registrations, pledges and memberships are person/alias-level entities in Rock.
**Verified (code + docs).**

**(3) One person, several families.** Yes by design. `PersonService.GetFamilies(personId)` "Gets the families sorted by
the person's GroupOrder (GroupMember.GroupOrder)". `GroupMember.GroupOrder` doc: for the family group type it "sets
which family is primary"; nulls sort last ("Use int.MaxValue in OrderBy statements for null GroupOrder values").
`Person.PrimaryFamilyId` is "computed on save, so any manual changes to this will be ignored"; `UpdatePrimaryFamily`
runs SQL that picks the first non-archived family by GroupOrder then GroupId and also syncs `PrimaryCampusId`.
**Verified (code).** Docs "Blended Families" give two sanctioned approaches: _Dual Family_ (child "a member of both
families"; "Family-based reports list two families for Brian, while person-based reports show him once"; "Mailings will
be sent to both home addresses"; check-in works with either family) and _Single Family with Relationships_ (child in one
family; other parent linked by Parent + "Can Check-in" relationships; "preferable in cases where one parent has sole
custody"). Foster children: "The best approach is to add the children to the family" and create a custom Known
Relationship type to mark them as fosters. **Verified (docs).** People who no longer live together stay related through
Known Relationships (Previous Spouse, adult Child/Parent, Sibling: "for adult siblings who are no longer in the same
immediate family"). **Verified (SystemGuid + Reported training).**

**(4) Relationship representation.** Two layers:

- _Family role_ — Adult or Child only. Spouse is **not** stored; `PersonService.GetSpouse` derives it: "Both Persons are
  adults in the same family (GroupType = Family, GroupRole = Adult, and in same Group)", both Married, optionally
  opposite gender when the "Bible Strict Spouse" setting is on, ordered by GroupOrder then closest birth date.
  **Verified (code).**
- _Known Relationships_ — a second GroupType (`GROUPTYPE_KNOWN_RELATIONSHIPS = E0C5A0E2-…`). Each person owns one hidden
  group (role `GROUPROLE_KNOWN_RELATIONSHIPS_OWNER`); every other member of that group is a related person with a role.
  Built-in roles (SystemGuid.GroupRole, comments verbatim): Can Check In ("A person that can be checked in by the
  owner…"), Allow Check In By ("A person that can check in the owner…"), Grandparent, Grandchild, Sibling, Invited,
  Invited By, Step Child, Step Parent, Child ("An adult child of the owner…"), Parent, Previous Spouse ("Role to
  identify former spouses after divorce."), Facebook Friend, Business, Business Contact. Reciprocity:
  `GroupMemberService.GetInverseRelationship` "reads the InverseRelationship attribute on the group role" and finds or
  creates the reverse record; `CreateKnownRelationship` "Creates the known relationship if it doesn't already exist" and
  then the inverse; `DeleteKnownRelationships` deletes inverses too. Self-inverse roles (Sibling, Previous Spouse) apply
  to both automatically. Custom types: add both roles, save the group type, then edit each role to set its inverse and
  "Can Check-in". **Verified (code + docs); the two-step save quirk is Reported (training).**
- _Implied / Peer Network_ — the old "Implied Relationships" group type is `GROUPTYPE_PEER_NETWORK`, marked
  `[RockObsolete("17.0")]`: "Peer Networks of this Group Type have been replaced with a formal Peer Network feature."
  **Verified (code).**
- _Dating_ — GroupMember has `DateTimeAdded` and `InactiveDateTime`, and the known-relationship record is a GroupMember,
  so a relationship can be made inactive or archived but there is no start/end-date pair on it. **Inferred from the
  model.**
- _Merging_ — `PersonAlias` keeps every former PersonId/Guid pointing at the surviving person ("the AliasPersonId is the
  previous person id that was merged into a new person record"), which is why transactions and memberships reference
  PersonAliasId. **Verified (code).**

**(5) Add / remove / life events.**

- New person: `SaveNewPerson` assigns Child under 18 else Adult, then `GroupService.SaveNewFamily` ("{LastName}
  Family"). **Verified.**
- Edit Family screen: _Add Person_ (existing or new), _Role_ Adult/Child, _Move to new family_ ("often when a child
  turns 18"), a choice to "remove them from their other families" when adding an existing person ("Keep them in both for
  joint custody"), and _Remove from family_ which "appears only when a person belongs to more than one family".
  **Verified (docs).** `RemovePersonFromOtherFamilies` "Removes the person from other families, then deletes the other
  families if nobody is left in them" and, if the removed family was the person's giving group, moves `GivingGroupId` to
  the family being kept (with a "Giving Group" history entry). **Verified (code).**
- Turning 18: `AgeClassification` is "Adult if Age >= 18 OR has a role of Adult in one or more (ANY) families", Child
  when under 18 or a child in all families; `UpdateFamilyMemberRoleByAge` sets Adult at 18+. Moving adult children into
  their own family is a "best practice" done via data automation, not automatic. **Verified (code) + Reported
  (community).**
- Divorce: no tool; practice is Move to new family + Previous Spouse relationship (role exists for exactly that).
  **Verified role; Inferred practice.**
- Death: `Person.IsDeceased`/`DeceasedDate`; salutation falls back to group name "if all members are deceased";
  giving-leader ordering now sorts living before deceased rather than excluding them (comment dated 9/25/2023, Issue
  #2848). **Verified (code).**
- Expunge: removes family memberships, deletes empty families, and first nulls any `GivingGroupId` pointing at them ("If
  theres any people that have this group as a giving group, set it to null"). **Verified (code).**
- Validation: duplicate member in same group+role blocked ("already belongs to the … and cannot be added again with the
  same role"); role `MaxCount` enforced for Active members; `MinCount` exists on the role but no enforcement was found
  in GroupMember.Logic. **Verified (code).**

**(6) Giving.** `Person.GivingGroupId` ("Combine Giving With" in the UI) names the family whose giving the person
shares; `GivingId` is computed "G{GivingGroupId}" when set, otherwise "P{PersonId}" (`UpdateGivingId`), and is the key
that statements, giving analytics and the "giving unit" use. `GivingLeaderId` is computed per GivingId: ordered by
IsDeceased, role Order, Gender, birth date. `SaveNewGroup` sets `GivingGroupId = group.Id` **only for Adult-role
members**; docs: "The field is left blank for children, so their gifts are tracked on their own." Couples wanting
separate statements leave the field blank. Because a person can be in several families, the field also picks _which_
family's giving they join. Soft credits do not exist as a concept; the giving group _is_ the sharing mechanism.
Businesses are Person records of a business record type linked through Business / Business Contact known relationships.
**Verified (code + docs).** Reported pain: no setting to default "Combine Giving With" to blank (community thread 2471);
a data view on giving totals "only lists one person from each family" (thread, fix status unclear); splitting adult
children into own families causes duplicate "The ___ Family" mailings unless deduplicated.

**(7) Communications.** `GroupSalutation` / `GroupSalutationFull` are stored on the family (computed by
`CalculateFamilySalutation`: adults sorted by gender, children by age, inactive and children optional, truncated to
250). Addresses are family-level; "Family Moved" rolls the home address to Previous. Dual-family children get mail at
both addresses. Per-person `CommunicationPreference` sits on the GroupMember. **Verified.**

**(8) Permissions / check-in.** Check-in's family screen "displays a list of family members and others with a 'Can
Check-in' relationship to the family". "Allow check in by" on the child creates the inverse on the adult automatically,
and "anyone in the family of the person you selected will be allowed to check in the child". Other relationship roles
(Grandparent, Step-parent) "do not grant check-in rights" until the role's Can Check-in is set to Yes.
`GroupTypeRole.IsCheckInAllowed` doc: "only applies during the 'already member' check during check-in" (a different
flag, about checking into the group itself). GroupTypeRole also carries `CanView`, `CanEdit`, `CanManageMembers`,
`CanTakeAttendance`, `IsLeader` as generic role permissions. `Person.AccountProtectionProfile` and
`Group.ElevatedSecurityLevel` govern how automated changes may touch a record. Portal (My Account / Family
Pre-Registration) edit rights for a parent over a child's record are configured per block; no code-level "guardian may
edit child" rule was found. **Verified (code + docs); last sentence Inferred.**

**(9) Eligibility from relationships.** None built in beyond check-in authorization and the age-derived family role;
Group Requirements can be filtered by `AppliesToAgeClassification`. **Verified.**

**(10) Visible compromises.** Dual-family membership doubles family-based report rows and mailings (docs admit it).
"Primary family" is a computed ordering (`GroupOrder`), not a user-visible flag in older versions (RockU transcript: "In
future versions you will be able to select, which one is the primary family"). Giving group reassignment on family split
is only handled when the person is removed from _other_ families; adding a person to a second family leaves
GivingGroupId untouched, so admins must edit it. Spouse is derived, so unmarried partners, same-sex couples (when Bible
Strict Spouse is on) and three adults in one family are edge cases. Known-relationship roles need a two-step save to
configure inverses. Peer-network-as-group-type was abandoned in v17 for a dedicated feature — a signal that _implied_
relationships do not belong in the same group table as _declared_ ones.

---

### 2. Planning Center People (+ Check-Ins, Giving)

**(1)** A dedicated **Household** resource: "A household links people together and can have a primary contact."
Attributes: `name`, `member_count`, `primary_contact_id`, `primary_contact_name`, `avatar`, timestamps; relationships
`primary_contact` (to-one Person), `people` (to-many). Membership is **HouseholdMembership** with `household_role` ∈
{`adult`, `child_or_dependent`, `other_adult`, `parent_guardian`} ("The role of the person within the household."),
`pending` ("False when a person's membership in a household is unverified." — note the attribute is a boolean whose
semantics the docs state inversely; treat as "unverified" flag), `person_name`. Person carries a `child` boolean.
**Verified (API reference 2026-06-04).** Help: households track "family units, such as couples or parents with young
children"; "You don't need a household for a single adult." **Verified (help).**

**(2)** The household owns a name, photo, primary contact, and nothing financial. Addresses and phones are on people;
editing a Home-type field offers "update it for all household members" (a propagation prompt, not a shared record).
**Verified (help).**

**(3)** "Just like in real life, people in Planning Center can belong to multiple households." "Manage other Households"
on the profile. Nothing links two households to each other; separated adults are simply not in the same household and
are related to the child via membership in the child's household or as Trusted People. **Verified.**

**(4)** Only the four household roles; no spouse, grandparent or sibling types. Check-in-specific lists on the child's
profile: **Trusted People** (can drop off / pick up, not in the home) and **Not Authorized** (must not pick up); "a
person cannot be both". **Verified (help).** No dated relationships. **Inferred.**

**(5)** Add: Manage → add household → search → "Join household", set roles. Remove: Edit household → remove; "they still
exist in your database." Delete household: "No profiles or activity will be deleted!" Form submissions can add someone
who "show[s] as pending … until they confirm the information in the email." No marriage/divorce/18th-birthday automation
documented. **Verified (help).**

**(6)** Giving is per donor. **Joint donors**: "Two donors can be joined, whether or not they are members of the same
household." Adult household members are _suggested_ for joining. Joining is one pair at a time ("there isn't a way to
join donors in bulk"), both are emailed and "This security setting cannot be turned off". Effects: combined history and
statements in Church Center, "Any pledges made by either donor will now be shared with their joint donor", "Gives
Jointly With" column in reports. Disjoin: "Donations return to the profile that initiated them, and pledges return to
the donor who made them." Joint print statements go to "every primary mailing address on file". **Verified (help).** No
soft credits.

**(7)** Primary contact's "contact information appears for the household in common lists and reports, such as mailing
labels". Household photo in the directory. **Verified.**

**(8)** Church Center: Parent/guardian "can edit other household members' profiles … register them for signups, check in
their children, and manage their schedules in Services if granted permission"; "Adults can add profiles, but children
cannot." Check-in: "all household adults and trusted people will be listed in the Checked In By dropdown"; default "one
security label per household"; the label "cannot be reprinted". Since Church Center 2026.4.8 all household members see
security codes for every active check-in. **Verified (help + changelog).**

**(9)** None beyond check-in authority and Church Center edit rights. **Verified by absence.**

**(10)** Giving linkage is strictly pairwise (two donors) and manual; it is not driven by the household. Households
carry no address of their own, so "household address" is a convention of copying. The `pending` attribute's documented
wording is inverted, an API wart worth noting. Trusted/Not Authorized lists are a check-in feature, not a general
relationship model.

---

### 3. Breeze ChMS (Tithe.ly)

**(1)** A **family** is a grouping of existing person profiles. API: `/api/families/create` "does NOT create new
profiles"; "a person can only be part of one family at a time." The per-person **family role** is a profile field
(`field_type` `family_role`, `role_id` 1 Unassigned, 2 Child, 3 Adult, 4 Head of Household, 5 Spouse). **Verified (API
page).**

**(2)** The family owns a profile picture and an ID ("will keep the original family ID in the database intact", which
"preserves the family's profile picture"). No giving, pledge or address ownership documented. **Verified (API) /
Inferred.**

**(3)** No. One family at a time; `families/create` with someone already in a family removes them from the old one;
`families/remove` detaches people "without deleting them"; `families/destroy` dissolves the whole family given any
member. No relationship structure exists for people outside the family. **Verified (API); absence Inferred.**

**(4)** Roles only; no spouse-to-spouse link beyond both being in the family with roles Head/Spouse. Help (Reported,
excerpt): roles "are primarily tools for sorting and organizing outputs (like giving statements and mailing labels), not
descriptive positions"; "Head of Household is the first person listed on directories, mailing labels, and other exports.
The Spouse is listed second."

**(5)** Help (Reported): add from the profile's family section, "Add to Family", assign role; "Removing someone from a
family removes the individual from the family unit, allowing for a stand-alone profile or connection to a different
family; it does not delete their profile." Adult children wanting their own tax statement "should be moved to their own
family unit".

**(6)** Statements are produced per family unit, sorted by role; separate statements require separate families
(Reported). No giving-unit flag independent of family found. **Reported / Inferred.**

**(7)** Mailing labels and directories key off Head of Household order (Reported).

**(8)** No public material on member-portal family editing found. **Unavailable.**

**(9)** None found.

**(10)** Single-family rule forces split custody into duplicate profiles or "Unassigned" roles; no non-family
relationships; statement grouping is coupled to family membership so "separate statement" means "separate family" (the
same compromise Rock's community recommends, but here compulsory). Support pages at support.breezechms.com and
support.tithe.ly returned 403, so (5)–(7) rest on search excerpts.

---

### 4. ACS Technologies Realm

**(1)** A **Family** record with per-member **Family Position**. Defaults "Head, Spouse, Child, and Other"; admins add
custom positions (grandparent, godparent, guardian) under one of three predefined classes _Primary_, _Child_, _Other_
("Predefined 'primary', 'child', and 'other' positions can't be deleted, but they can be edited"). **Verified (help).**

**(2)** The family carries the family phone number and address ("A primary family member can … update the family phone
number and address"). Giving is per individual with optional sharing (see 6). **Verified.**

**(3)** "Only children can be in multiple families." Divorce scenario: "Select Separate Household"; "For dual custody,
you can check both families, but then select a Primary Family." A child in multiple families who becomes an adult:
change position Child → Other and on save the individual is "linked only with the primary (custodial) family and deleted
from all others." "Remove from this family" appears only for the child's non-primary families. **Verified (help).**
Non-cohabiting relatives are linked through the **Relationships** feature (Nov 2022 release note, Reported via excerpt):
tracks "connections beyond a person's family record, such as grandparents, cousins, coworkers, and close friends"; "Each
person added as a relationship must already have a Realm profile"; adding one "updates both profiles at once"; visible
"only to Realm users with permission"; usable in profile reports and custom queries; emergency-contact use case. Legacy
ACS "Other Relationships" land in Notes on migration.

**(4)** Family Position (classed primary/child/other) plus the separate reciprocal Relationships list. No dating
documented. **Verified / Inferred.**

**(5)** Life-change tools: _Get Married_ ("Use this process when an adult congregant marries or remarries"), _Separate
Household_, _Change Household_ ("Family Member Leaves for Other Reasons"), child-to-adult position change with automatic
pruning of non-primary families. Death not covered on the scenarios page. **Verified (help).**

**(6)** Giving is individual; "Only primaries can share giving." With Giving Numbers off: after merging profiles "select
the shared giving check box on the Giving tab"; clearing it later keeps gifts "on the respective profiles they were
originally assigned to". With Giving Numbers on: different numbers → "Giving is not shared, and each partner maintains
own giving number/pledges"; same number or only one has a number → "Giving is shared. Giving number 60 is assigned to
both." Pledges print at the bottom of statements. Search excerpt (Reported): "When you add a head and spouse record to
the same family, they automatically have a shared giving record." **Verified (help) + Reported.**

**(7)** "When a child is emailed, the parents are copied on the email"; if the child is in multiple families "parents in
every family receive them"; shared email between child and parent → one email. **Verified.**

**(8)** Portal: "Primary family members, such as 'Head' or 'Spouse,' can edit their family members' profile information.
Children and 'other' members can't." Privacy settings: "only primary members of a family can change settings for
others"; changes notify the affected person by email. Removing a family member's profile "contact your church
administrator". Staff need _Edit Individual_ to change a position. Go Method sync assigns an Adult Family Position to
adults and a Child Family Position to under-18s. **Verified (help).**

**(9)** Position class drives rights (edit others, share giving, change privacy) — an eligibility-by-role pattern.
**Verified.**

**(10)** Adults cannot be in two families (blended families with adult dependents, elderly parents in two homes are
unsupported). Sharing giving is a merge-time manual step with a surprising dependence on whether Giving Numbers are
enabled. Relationships were bolted on in 2022 after years of "put it in Notes". The child-to-adult transition silently
deletes non-primary family links.

---

### 5. Pushpay ChMS / Church Community Builder (CCB)

**(1)** A **Family** resource (`FamilyProfileSerializer`): `id`, `last_name` (no `name` field), `address`, `images`,
`message` (check-in message), `members[]`, `authorized_individuals` ("individuals authorized for pickup or check-in").
Individuals carry `family_id` ("A valid Family ID is needed to create a new individual.") and `family_position` ∈
{`PRIMARY_CONTACT`, `SPOUSE`, `CHILD`, `OTHER`} ("The position of the individual in the family"). v1 API used codes
h/s/c/o ("head of household", spouse, child, other). Glossary: Primary Contact is "The individual in a family who
primarily receives communication to the family (not necessarily the head of household). Each family is required to have
one and only one Primary Contact." "Family ID: The number used to represent a family within your site, found at the end
of the URL for a family's profile." **Verified (API v2 + glossary PDF).**

**(2)** Family owns last name, address, campus, phone, check-in message and the authorized-pickup list. Giving is
individual (glossary and API say nothing about family giving). **Verified / Inferred.**

**(3)** `family_id` is a scalar on the individual → one family per person. **Inferred from API.** Split custody is
handled by `authorized_individuals` on the family for check-in (Verified field) and, per the KidCheck integration
mapping (Reported), Primary Contact = primary guardian, Spouse = authorized guardian, Other = ignored.

**(4)** Positions only; no relationship types. **Verified by absence.**

**(5)** `POST /families` with `individual_id` "will create a new family with this individual as the primary contact (all
other fields ignored)"; "POST /individuals does not reliably support family creation in the same call." Rollout guide
(Reported): "Set as Child" etc. via an ellipsis menu. No divorce/18 tooling found. **Verified (API) / Reported.**

**(6)** No family giving construct found in public API/glossary. **Unavailable.**

**(7)** Primary Contact "primarily receives communication to the family". **Verified (glossary).**

**(8)** **Age Threshold**: "The age at which children's profile information becomes visible to individuals in your site.
Profile information of children below this age will be visible only to the Primary Contact and Spouse in their families,
People administrators, and individuals with active Child Work Approved" status. Rollout guide (Reported): primary
contacts and spouses can add and edit family members' profiles. Check-in authorization = `authorized_individuals`.
**Verified (glossary + API).**

**(9)** Child Work Approved status gates visibility of under-threshold children — a relationship-plus-clearance rule.
**Verified.**

**(10)** Exactly-one-primary-contact and one-family-per-person are rigid; "Other" is the only escape hatch; no
reciprocal relationships; the family lacks a name field (uses `last_name`), which blended families break.

---

### 6. ACS Technologies MinistryPlatform

**(1)** Separate **Household** record (address, home phone, congregation, Household Source, Care Log) and **Contact**
record with a **Household Position** lookup. Positions are fixed: 1 Head of Household, 2 Minor Child, 3 Other Adult, 4
Adult Child, 5 Guest Child, 6 Company — "Do not add to, edit, or delete these values. No modifications are supported."
Minor Child and Guest Child "support Parent tokens in Check-In Suite". "A contact must belong to one household (their
'primary family')"; "To record a home phone, address, and primary Congregation, a Contact record must belong to a
Household record." Check-In and the Add/Edit Family Tool "do not display Contacts unless they are associated with a
Household record." Households "should have no more than two Contacts" in Head of Household. **Verified (help).**
Participant and Donor are optional sibling records of Contact. Data dictionary: "for a data dictionary of all database
tables in HTML format, email Support" — **Unavailable** publicly.

**(2)** Household owns address/phone/congregation and the Household Care Log; one name for the household even when
members' last names differ ("the Household only has one name"). Giving lives on the Donor record, not the household.
**Verified.**

**(3)** Primary household is single, but **Other Households** (tab on Contact) relates a Contact to additional
households with a Household Position, Household Type and an **End Date** ("If this situation is temporary or no longer
is valid, add an End Date"). Uses: "relate non-custodial parents in a different Household to the child's Household",
grandparents who check children in, college students, seasonal homes. Separated spouses: "Split the Household into two
Households, where each is at their own actual address", keep Marital Status Married, link through Other Households.
**Verified (help).**

**(4)** **Contact Relationships** is a separate structure: fields Relationship type ("The type of relationship the two
contacts have in common"), Related contact, Start Date, End Date ("Leave blank if the relationship is current"), Notes,
read-only Triggered By ("the system uses to trigger updates on the reciprocal record"). "When a Relationship is created
on one Contact record, the reciprocal relationship is automatically created on the related Contact record (if a
reciprocal relationship exists)" — a database trigger. Types are admin-configurable under System Lookups >
Relationships; guidance to use gender-neutral labels ("Sibling Of" over "Brother Of/Sister Of"); reading direction "Jane
Dow is the 'Parent Of' Jimmy Dow". Ending a relationship requires entering the End Date on both sides ("Repeat for the
other Contact"). **Verified (help).**

**(5)** Split Household Tool "when a young adult comes of age or when couples divorce"; moving one Contact to a new
address = move to a new Household; foster child leaving = Inactivate Tool or split; elderly parent living with adult
children = Other Adult position with Individual statement type, or own Household at the same address. **Verified
(help).**

**(6)** Donor record **Statement Type**: "Individual: Donors with this statement type get a statement exclusively
containing qualifying donations that were credited to their Donor record. Family: Donors with this statement type get a
single statement including qualifying donations from any household members with a family statement." "If a couple wants
separate giving statements, you don't have to split the Household … change both Donor records to Individual statements."
Statement Donors records link each contributing household member (including a child) to the household statement.
Statement Method = delivery preference. **Verified (help).** No soft credits.

**(7)** Household has one name and one address; Other Households give a second address for mail. **Verified.**

**(8)** Other Households show at check-in only when "the Household Type must match the Household Type you have set for
CIM, ShowsInCheckinHHTypeID". Parent tokens in Check-In Suite resolve from Minor/Guest Child positions. Portal
permissions not covered in fetched pages. **Verified (partial).**

**(9)** Statement eligibility is a per-donor flag combined with household membership; check-in eligibility is Other
Household + Household Type. **Verified.**

**(10)** Fixed position list (no Spouse — two Heads instead); Other Households are not reciprocal (the docs are silent);
Household Type doubling as a check-in switch is a configuration trap; performance warning against an "orphan" household
("greatly impacts system performance"). The schema is not public.

---

### 7. FellowshipOne (Ministry Brands)

**(1)** **Household** resource: "A household is a resource that contains people, addresses, and communications."
Attributes `householdName`, `householdSortName`, `householdFirstName`, `lastSecurityAuthorization`; no delete method.
Person has required `householdID` and `householdMemberType` (HouseholdMemberTypes: 1 Head, 2 Spouse, 3 Child, 4 Other,
101 Visitor; the list "cannot be updated via the API"). "Fellowship One defines a household as the combination of the
heads of household (the individuals with the Head and Spouse household member type)." **Verified (developer docs +
help).**

**(2)** Household owns addresses and communications, and **can be the attributed party of a contribution**: "The
Household choice means that the contribution is shared between the heads of household equally." Statements: "you can
either send individual statements or household roll-up statements." **Verified (help).**

**(3)** `householdID` is scalar → one household per person. The **Visitor** member type (shown in a separate "Household
visitors" section) is how a person is attached to a second household without membership; **Other (extended family)**
sits in the main listing. `oldHouseholdID` attribute exists on the Person payload (undocumented). **Verified fields;
one-household rule Inferred.**

**(4)** Member types only; no relationship table found. **Inferred.**

**(5)** Split Individual from Household (Reported excerpt): People > Data Integrity > Split Household; choose per data
item "copied or moved"; "a new household gets created"; partial moves use Move Individual. Merge offers "Do not move
Hsld giving", "Move Hsld giving to Individual", "Move Hsld giving to Household"; "We cannot leave the household giving
with a household that has no individuals"; "Individual giving is not a consideration in merge cases." Advice for
newlyweds: attribute the wife's prior giving to her as an individual because household attribution means "manually
re-attributing her contributions if she later wants a separate statement." **Verified (merge page) / Reported (split
page 403).**

**(6)** Gift "Attributed to" = individual or Household; "Contributions cannot be split between households or between a
household and an organization" (Reported). A household-attributed gift is a genuine household asset shared 50/50 between
Head and Spouse. **Verified + Reported.**

**(7)** Household name / sort name / first-name fields exist for addressing ("Nick and Rebecca Floyd"). **Verified.**

**(8)** `lastSecurityAuthorization` on household (undescribed). Adding people through Check-in "requires no special
security right". **Verified field; semantics Unavailable.**

**(9)** None found.

**(10)** Household-attributed giving is elegant for couples but hostile to divorce (gifts cannot be split across
households; re-attribution is manual). Visitor/Other are structural hacks for non-residents. Developer docs still
describe v1 (2010s).

---

### 8. Shelby Arena and ShelbyNext Membership

**Arena.** **(1)** Family record with family roles; help defines roles only as "adult vs. child" and lets admins add
roles (forum: "Adult Child"). **(4/5)** Head of Household is computed: sort by role (adult before child) then gender
(male before female); "A single adult's record is always the HOH." Children → adult conversion is done with community
"autoadult" stored procedures run yearly or at 18. **(6)** Statements go "to the head of household, which is the male";
"If one spouse is marked Contribute Individually, then you must also mark the other spouse." Forum: the autoadult script
"does not change the giving_unit_id", leaving a converted child in the parents' giving unit unless
`contribute_individually` is set — fixed with a SQL update. "Combine Families" mailing option "combines the mailer for
all individuals living at the same address"; Move Agent rolls the address to Previous for all family members. **(10)**
Custom roles break stock tools ("It does not reconize Adult Child and will not return the parents"). **Verified (help
pages) + Reported (forums).** Giving unit column name is Reported, not Verified.

**ShelbyNext Membership.** Public material is thin: a Shelby tips PDF (Reported) shows a Family page where one member is
set _Primary_ and others get relationship labels such as _Husband_; the product overview says members can "add family
members" and view/print giving history; church help pages (Reported) state that family-recorded giving is visible to
both spouses while individual giving is visible only to the giver, and that switching between family and individual
accounting is a finance-office action. A reviewer of the predecessor (v5) complained the system "doesn't default to one
report for the entire family (or couple) so contributions have to be transferred, or 2 reports are created." No
developer documentation found. **Reported / Unavailable.**

---

### 9. Elvanto / Tithe.ly ChMS

**(1)** Family is identified by `family_id` on the person; `family_relationship` ∈ "Primary Contact, Spouse, Partner,
Child, Sibling, Grandfather, Grandmother, Other". `family_id` = "new" creates a family; an existing id joins it.
**Verified (API).** Help (Reported excerpt): "supports immediate family structures best"; "the Sibling relationship only
applies to the primary contact, not to children"; the spouse or primary contact can be of either gender.

**(2)** Not documented beyond grouping. **Unavailable.**

**(3)** Scalar `family_id` → one family. **Inferred.**

**(4)** Relationship labels are _relative to the primary contact_ (Grandfather, Sibling "of the primary contact"), a
single-hub model. **Verified values / Reported semantics.**

**(5)** Add from the profile's Family tab (search defaults to same last name); "Making a New Couple a Family" article
exists (not fetched). **Reported.**

**(6)** Admins choose whether a profile shows family members' giving; statements are admin-generated; member "My Giving"
shows own records by default. **Reported.**

**(8)** Admins can enable member editing of family profiles ("My Family" setting) and can block members from editing
others or adding family members; "profile pictures cannot be uploaded for family members". **Reported.**

**(10)** Hub-and-spoke relationship labels cannot express child-to-child or grandparent-to-specific-child; all help
pages returned 403 so (5)–(8) are excerpts.

---

### 10. Hivebrite (alumni / community)

No household, spouse, partner, dependent or guardian concept appears in the public Partner API index (sections: Users,
User data fields, Memberships, Network, Roles, Organizations, Groups, Events, Donations, Mentoring) or marketing pages;
profiles are individual with admin-defined custom fields and per-field visibility. A "household" would be a custom field
or a group. **Inferred from absence; the SPA docs page did not render for field-level reading — Unavailable for
detail.**

### 11. Graduway (Gravyty)

No public documentation of spouse/household modelling found; the product positions itself as an engagement layer syncing
to Raiser's Edge NXT / Salesforce, so household logic lives in the CRM of record. **Unavailable.**

### 12. Anthology Encompass (iModules)

Public help documents member imports keyed by constituent ID (incremental files only), a Constituent Management view,
non-member/guest merges by Member ID, and event-form _guest roles_ that "capture the guest's connection to the
registrant (spouse, child/teen)" for pricing — i.e., the relationship is captured at registration, not as a linked
member record. No spouse-link or household field is documented publicly; the Encompass schema is viewable only inside a
site ("Data > Import Member Data > Review Encompass Schema"). Alumni couples wanting a joint gift counted for both is a
recurring request on a competitor's board (Almabase) answered with Raiser's Edge soft credits. **Reported /
Unavailable.**

### 13. Blackbaud Raiser's Edge NXT — alumni usage (public community material only; see w1 for the model)

Higher-ed practice (Reported, community): one constituent record per spouse, gifts automatically soft-credited to the
other, Head of Household flagged on "the one with the greater level of engagement or higher level constituency code",
HoH mail processing for one mailing per couple; alum/alum couples flagged with a repurposed attribute. Product note: HoH
is now editable in web view, but "if one spouse isn't a constituent, you can't mark either of them as head of
household." A HoH query filter "doesn't work unless EVERYONE has a spouse" (thread title). **Reported.**

---

### 14. Youth / sports / camp systems (family account, guardian consent, age eligibility)

**TeamSnap.** Household (beta): "Anyone can create a household"; creator = Household Organizer "with privileges to
invite or remove members"; "Household members are not required to have a TeamSnap account"; "You can also be a part of
multiple households, but can only create a single household"; Remove "permanently deletes that member's profile from
your household". Roster profiles: "All contacts added under a member profile will have access to the same information
the member has" and "can perform the same actions"; adding contacts to an admin profile "will grant the family or
contact access to the team or organization with administrator permissions" (strongly discouraged). 2026 release: primary
contacts "can now invite additional parents or guardians to a player's profile"; "Admins and primary contacts can now
promote other guardians to a 'Primary' role"; "Contacts can now remove themselves from a player's profile (provided
another primary contact exists)"; "Built-in safeguards prevent parents from accidentally duplicating their own account
profile as a child"; adults in high-school/adult programs can be registered "through parent/guardian workflows" with
guardians required to "actively confirm they are registering an adult participant". Waivers: per participant
("acknowledged once for each participant being registered") or per registration; "the entry will be required to match
the account holder information"; the product calls it "technically more of an 'acknowledgement' than an electronic
signature" and recommends an uploaded signed document for a true signature. **Verified (help + release notes).** API v3
exposes `members` only; no household resource documented. **Verified by absence.**

**SportsEngine (NBC Sports Next).** "A family will only have one account"; "the primary account holder must be 18+ to
sign electronic documents and purchase memberships"; "A player profile will not have a login to the account" and "can
only be created during registration or when added to a roster"; an Account Owner Profile "will have a login", "can
manage other sub-profiles", cannot itself have guardians but "can serve as a guardian for a Player Profile". Guardians:
"To add a Guardian, you must be the Primary Account Owner"; "The invited guardian will appear as Pending until their
invitation has been accepted"; access level chosen at invite; "The household account owner cannot be removed. Guardians
cannot remove profiles on another account." Memberships (and the waivers inside them) "must be purchased by an account
owner or parent/guardian", not from the player profile. **Verified (help).**

**CampMinder.** Unified Person Record: "Children are associated with a Primary Childhood Household, while staff and
adults are linked to a Principal Household." Camper application: "Campminder requires you collect info for one parent";
a second parent/household can "display by default or sit behind a link"; camps decide "whether Household 2 receives camp
mail and invoices"; "Only members of the Primary Childhood Household can make online payments, regardless of invoice
settings"; "capturing marital status is recommended for managing household relationships over time"; all parents in
primary and alternate households get Campanion app access on enrolment; archiving a record "can prevent specific
contacts, like a divorced parent, from receiving communications while keeping their relationship to the camper intact".
**Verified (help/onboarding pages).** E-signature/authorized-pickup specifics not found. **Unavailable.**

**Jumbula.** Families "containing parents, guardians, and authorized pickups"; a family account is "set up by a
parent/legal guardian" (church/camp instructions, Reported); Parents tab = "primary contacts"; participants = "students
or children enrolled", with age and grade on the record; admins can create a family manually by email; waivers completed
at registration or sent afterwards with admin visibility of agreed/signed/"Not set"; a family Authentication Code drives
check-in/check-out. **Verified (KB) + Reported.**

**Sawyer.** Parent account with Profiles (children and additional adults); provider-side Family tab "View family members
associated with the customer's account" with switching between "the primary adult, each child, or other adults";
caregiver and secondary-contact info under personal information; waivers are provider-defined form fields signed
digitally by the parent. Account-level articles returned 404. **Verified (business help) / Reported / partly
Unavailable.**

Common pattern across all five: adult account is the root identity; children are sub-profiles without credentials;
guardians are _granted_ to a child's profile (invite → pending → accepted; removal constrained so a child is never
orphaned); consent is captured per participant or per registration by the authenticated adult; age gates sit on the
adult (18+ to sign) and on the program (age/grade eligibility), not on the child record's permissions.

---

### 15. Patterns

**P1 — Family as group with roles (Rock), household as entity with membership role (PCO, MP, F1, CCB, Realm, Breeze).**
Both store _membership + role_; the group-engine variant gains reuse (same validation, history, attributes, security,
syncs, salutation) and allows arbitrarily many "family-like" group types; the dedicated-entity variant gains clearer
ownership of address/name/primary contact. In both, the role vocabulary is small and _position-in-household_
(adult/child, head/spouse/child/other, parent_guardian/other_adult/child_or_dependent), never kinship.

**P2 — Known relationships as a separate, reciprocal, directional structure.** Rock (owner-group per person, role +
InverseRelationship attribute, automatic inverse create/delete), MinistryPlatform (Contact Relationships with Start/End
Date, trigger-maintained reciprocal, admin-defined types), Realm (Relationships, both profiles updated, permissioned).
The distinguishing features worth copying: reciprocity enforced by the system; types configurable with an inverse (or
self-inverse); kept apart from household membership; dated (MP only); permissioned separately from the profile (Realm).

**P3 — Primary household as a computed or chosen pointer.** Rock computes `PrimaryFamilyId` from member order; Realm
forces a choice ("select a Primary Family") and prunes on adulthood; MP has one primary and dated "Other Households";
CampMinder distinguishes primary childhood household (pays) from alternate (mail/invoices optional). Reports, addresses
and payments key off the primary.

**P4 — Giving unit is a pointer on the person, defaulted by household.** Rock `GivingGroupId`/`GivingId`; Arena
`giving_unit_id` + `contribute_individually`; MP Donor.Statement Type; F1 "Attributed to Household" (the only true
household-owned gift); PCO joint donors and Realm shared giving (pairwise, household-independent). Every system lets a
member opt out of the household unit and every system's forums show the unit failing to follow the person through
divorce, adulthood or death.

**P5 — Guardian / check-in authorization is an explicit grant list, directional, with negative entries.** Rock Allow
Check In By ↔ Can Check In; PCO household adults + Trusted People − Not Authorized; CCB `authorized_individuals`; MP
Other Households + Household Type gate; sports systems' invited guardians with pending state and promotion to Primary.
Household membership supplies a _default_ set of authorized adults; the grant list extends or restricts it.

**P6 — Age-derived classification feeding role and rights.** Rock `AgeClassification` (18) and family role by age; CCB
Age Threshold for child-profile visibility; Realm Child positions with parents copied on email; SportsEngine 18+ to
sign. The age rule is centralised, the consequences (role, visibility, who may sign) are per feature.

**P7 — Life events as procedures.** Move to new family / Split Household / Separate Household / Change Household / Get
Married are wizards that re-point membership, address and (sometimes) giving unit. None of the surveyed systems records
the event itself as data; Rock writes Person History entries.

**P8 — Youth/camp model: account → sub-profiles → granted guardians → per-participant consent.** See §14.

---

### 16. Weaknesses worth avoiding

1. **One-family-per-person hard limits** (Breeze, CCB, F1, Elvanto) force duplicate people, "Visitor"/"Other" hacks and
   lost custody context. Allow multiple households with one primary (Rock/PCO/MP/Realm), but keep the primary explicit
   and dated rather than computed from sort order (Rock's `GroupOrder` is opaque and the UI lagged the model).
2. **Deriving spouse instead of recording it** (Rock `GetSpouse` from Married + Adult + same family + gender) fails for
   partners, same-sex couples under strict settings, and multi-adult households. Record the couple relationship.
3. **Letting the household own giving** (F1 household-attributed gifts) blocks later separation ("Contributions cannot
   be split between households"). Keep gifts on persons with a _statement/giving-unit_ grouping that can be re-pointed,
   and keep history of the grouping.
4. **Giving unit not following the person** on 18th birthday, divorce or death (Arena `giving_unit_id` scripts, Rock
   community threads, ShelbyNext reviewer). Make the unit recomputation part of the household-change operation, with
   explicit per-person opt-out (Rock's blank "Combine Giving With", Arena "Contribute Individually" which must be set on
   _both_ spouses — avoid that asymmetry).
5. **Fixed role lists without a class** (MP's six positions, CCB four) versus Realm's _class + label_
   (Primary/Child/Other classes with custom labels) — the latter lets rights hang off the class while labels stay local.
6. **Relationships as roles relative to a hub** (Elvanto "Grandfather of the primary contact") cannot express who is
   related to whom. Store pairwise, directional, reciprocal, typed, dated.
7. **Reciprocal inverses maintained by convention or triggers that need a second save** (Rock's role-attribute two-step;
   MP's "repeat on the other contact" to end a relationship). Make the pair one record with two reading directions, or
   guarantee the inverse in the same transaction.
8. **Household Type doubling as a feature switch** (MP `ShowsInCheckinHHTypeID`). Authorization should be an explicit
   grant, not a side effect of a classification.
9. **Family-level cascades without guards**: Rock's "marking the family Inactive will also inactivate each member" and
   "campus is set at the family level" surprise multi-family people; cascade only to members whose _primary_ household
   it is.
10. **Dual-household reporting duplication** (Rock docs: two rows in family reports, mail to both addresses) — give
    reports a "count once by primary household" mode from day one.
11. **Pending/unverified memberships with inverted or vague semantics** (PCO `pending` doc wording). Model verification
    state explicitly (requested / confirmed / declined) with who confirmed.
12. **Treating a checkbox as consent** (TeamSnap "more of an 'acknowledgement' than an electronic signature"). Capture
    signer identity, relationship to the minor, timestamp and document version.
13. **Implied/affinity relationships in the same store as declared ones** — Rock retired the Implied Relationships group
    type (v17) for a dedicated Peer Network feature. Keep inferred connections out of the constituent-relationship
    table.
14. **Portal rights by family position without a child-safety floor** — CCB's Age Threshold and Realm's "children cannot
    change privacy settings for others" are the floor to copy; TeamSnap's "contacts inherit the member's access"
    (including admin) is the anti-pattern.

---

### 17. Evidence table

Legend: V = Verified, R = Reported, I = Inferred, U = Unavailable. Questions: 1 entity shape · 2 household ownership · 3
multi-household / estranged links · 4 relationship representation · 5 add/remove & life events · 6 giving · 7
communications · 8 permissions/check-in · 9 eligibility from relationships · 10 limitations

| Product                | 1            | 2   | 3   | 4                  | 5         | 6            | 7   | 8              | 9           | 10  |
| ---------------------- | ------------ | --- | --- | ------------------ | --------- | ------------ | --- | -------------- | ----------- | --- |
| Rock RMS               | V            | V   | V   | V (dating I)       | V         | V            | V   | V (portal I)   | V           | V+R |
| Planning Center        | V            | V   | V   | V                  | V         | V            | V   | V              | V (absence) | V+I |
| Breeze                 | V            | V/I | V   | V+R                | R         | R/I          | R   | U              | U           | V+R |
| Realm                  | V            | V   | V   | V+R                | V         | V+R          | V   | V              | V           | V+I |
| CCB / Pushpay          | V            | V/I | I   | V (absence)        | V+R       | U            | V   | V+R            | V           | V+I |
| MinistryPlatform       | V            | V   | V   | V                  | V         | V            | V   | V (partial)    | V           | V+I |
| FellowshipOne          | V            | V   | V+I | I                  | V+R       | V+R          | V   | V (field only) | U           | V+R |
| Shelby Arena           | V+R          | R   | R   | R                  | R         | V+R          | V   | U              | U           | R   |
| ShelbyNext             | R            | U   | U   | R                  | R         | R            | U   | R              | U           | R   |
| Elvanto                | V            | U   | I   | V+R                | R         | R            | U   | R              | U           | R   |
| Hivebrite              | I (none)     | U   | U   | I (none)           | U         | U            | U   | U              | U           | I   |
| Graduway               | U            | U   | U   | U                  | U         | U            | U   | U              | U           | U   |
| Encompass              | R/U          | U   | U   | R                  | U         | R            | U   | U              | U           | R   |
| Blackbaud NXT (alumni) | R            | R   | R   | R                  | U         | R            | R   | U              | U           | R   |
| TeamSnap               | V            | V   | V   | V                  | V         | n/a          | V   | V              | V           | V   |
| SportsEngine           | V            | V   | V   | V                  | V         | n/a          | U   | V              | V           | V   |
| CampMinder             | V            | V   | V   | V (marital status) | V         | V (payments) | V   | R              | V           | U   |
| Jumbula                | V            | V   | U   | V (pickups)        | V (admin) | n/a          | V   | V              | U           | U   |
| Sawyer                 | V (business) | U   | U   | R                  | R         | n/a          | U   | R              | U           | U   |

---

### 18. Pages that could not be fetched

- community.rockrms.com/documentation/church-management/people/families (index page rendered "Error"; sub-pages fetched
  instead).
- developer.planning.center/docs/#/apps/people/... (SPA shell only; the equivalent
  api.planningcenteronline.com/docs/apps/people/versions/2026-06-04/vertices/* pages were used).
- help.planningcenter.com/en/households.html (404; 138577-households.html used).
- support.breezechms.com/hc/en-us/articles/360001160753-Using-Family-Roles and
  support.tithe.ly/hc/en-us/articles/30949469292311 (403) — content taken from search excerpts.
- hexdocs.pm/breezex (redirect followed; wrapper documents functions but not endpoints — official app.breezechms.com/api
  used).
- help.elvanto.com (all articles 403: Working With Families, How a Member can Edit Family Profiles).
- help.acst.com/realm/whats-new/2022/november-2-2022 (404 with and without /en/) — excerpt only.
- support.fellowshipone.com Split Individual from Household (403) — excerpt only; help.fellowshipone.com mirror used for
  Merging & Household Giving.
- kb.ministryplatform.com relationships (redirects to help.acst.com root); help.acst.com MP contact-relationships page
  fetched instead. MinistryPlatform data dictionary: by request to Support only.
- docs.hivebrite.com/partner/admin (SPA; field lists not rendered).
- help.anthology.com/encompass import page truncated; no spouse/household fields in the readable part.
- discover.sportsengineplay.com account article (body not rendered); help.sportsengine.com /
  mobile-help.sportsengine.com articles used instead.
- help.campminder.com "Unified Person Record" (401) — search excerpt used; onboarding.campminder.com pages fetched.
- support.hisawyer.com account articles (404) — help.hisawyer.com business article fetched.
- community.blackbaud.com "How do other organizations handle Households?" (body not rendered) — thread excerpts used.
- CCB glossary PDF: fetched as binary and text-extracted locally (pypdf/pdftotext) — treated as Verified.

---

### 19. Sources (URL → what it supports)

Rock RMS (code, branch develop)

- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/Group/Group.cs → Group entity,
  salutations, IsArchived filter, campus, analytics-for-family attribute.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/GroupMember/GroupMember.cs →
  GroupMember fields, GroupOrder-as-primary-family, archive filter.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/GroupMember/GroupMember.Logic.cs →
  duplicate-member, MaxCount, capacity, requirements validation.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/GroupTypeRole/GroupTypeRole.cs → role
  flags (IsLeader, Min/MaxCount, CanView/Edit/ManageMembers, IsCheckInAllowed).
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/GroupType/GroupType.cs → GroupTerm,
  DefaultGroupRole, AttendanceRule, inheritance.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/CRM/Person/Person.cs →
  GivingId/GivingLeaderId/PrimaryFamilyId/AgeClassification computed-on-save, IsDeceased, AccountProtectionProfile.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/CRM/Person/PersonService.cs (3 segments) →
  GetFamilies, GetSpouse, GetHeadOfHousehold, GetFamilyRole, SaveNewPerson, RemovePersonFromOtherFamilies,
  UpdatePrimaryFamily, UpdateGivingId, UpdateGivingLeaderId, UpdateFamilyMemberRoleByAge, UpdatePersonAgeClassification,
  ExpungePerson, AddContactToBusiness.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/Group/GroupService.cs →
  SaveNewFamily/SaveNewGroup (GivingGroupId for adults only), CalculateFamilySalutation, AddNewGroupAddress.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/Group/GroupMember/GroupMemberService.cs →
  CreateKnownRelationship, GetInverseRelationship (InverseRelationship attribute), DeleteKnownRelationships, owner role.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/Model/CRM/PersonAlias/PersonAlias.cs → merge
  aliasing.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/SystemGuid/GroupType.cs → Family / Known
  Relationships / Peer Network (obsolete 17.0) GUIDs.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/SystemGuid/GroupRole.cs → Adult/Child and all
  known-relationship roles with comments.
- https://raw.githubusercontent.com/SparkDevNetwork/Rock/develop/Rock/SystemGuid/Attribute.cs → giving-related person
  attributes (no inverse-relationship constant there).

Rock RMS (docs / community)

- https://community.rockrms.com/documentation/church-management/people/families/intro-to-families → every person in a
  family; multiple families.
- https://community.rockrms.com/documentation/church-management/people/families/blended-families → dual-family vs
  single-family-with-relationships, foster families, reporting/mailing consequences.
- https://community.rockrms.com/documentation/church-management/people/families/edit-a-family → add/move/remove members,
  campus cascade, inactive cascade, addresses.
- https://community.rockrms.com/documentation/church-management/finance/track-giving/family-giving and
  https://chiptraining.rockrms.com/ask-chip-training/core-docs/article/3026 → Combine Giving With behaviour.
- https://community.rockrms.com/documentation/church-management/check-in/check-in-fundamentals/check-in-relationships →
  Allow check in by / can check in; enabling Can Check-in on other roles.
- https://community.rockrms.com/documentation/church-management/check-in/check-in-fundamentals/individual-vs-family-check-in
  → family screen shows family + Can Check-in people.
- https://community.rockrms.com/documentation/church-management/people/person-profile-page/use-known-relationships →
  stored as groups, inverse configuration, Can Check-in per type.
- https://chiptraining.rockrms.com/ask-chip-training/rocku/193 (Reported) → known-relationship type list, two-step
  inverse setup, sibling/previous-spouse self-inverse.
- https://chiptraining.rockrms.com/ask-chip-training/rocku/92 (Reported) → blended-family approaches; primary family not
  yet selectable at time of recording.
- https://community.rockrms.com/ask/using/2471 (Reported) → no default for Combine Giving With; adult children to own
  family; duplicate mailings.
- https://community.rockrms.com/ask/using/377, /1991 (Reported, via search) → giving units and statement reporting gaps.

Planning Center

- https://api.planningcenteronline.com/docs/apps/people/versions/2026-06-04/vertices/household → Household
  attributes/edges/actions.
- https://api.planningcenteronline.com/docs/apps/people/versions/2026-06-04/vertices/household_membership →
  household_role values, pending, create/update/delete.
- https://api.planningcenteronline.com/docs/apps/people/versions/2026-06-04/vertices/person → `child` attribute,
  household edges.
- https://help.planningcenter.com/en/138577-households.html → roles, multiple households, pending, Church Center rights,
  address propagation.
- https://help.planningcenter.com/en/138190-security.html → Trusted People, Not Authorized, Checked In By, one label per
  household.
- https://help.planningcenter.com/en/138371-joint-donors.html → joint donors behaviour, disjoin, statements.
- https://www.planningcenter.com/changelog/check-ins/improved-all-security-codes-in-church-center (via search) →
  household members see all security codes (2026.4.8).

Breeze

- https://app.breezechms.com/api → families endpoints, one-family rule, family_role ids.
- https://breezex.hexdocs.pm/Breezex.Families.html (Reported) → wrapper semantics.
- https://support.tithe.ly/hc/en-us/articles/30949469292311-Using-Family-Roles (search excerpt, Reported) → roles as
  sorting tools, adult children separate family, removal semantics.

Realm

- https://help.acst.com/realm/guides/getting-started/system-wide-settings/family-positions → positions, classes,
  primaries share giving, children in multiple families, parent email copy.
- https://help.acst.com/realm/how-to/profiles/manage-your-information/your-family → portal edit and privacy rights by
  position.
- https://help.acst.com/realm/how-to/profiles/life-changes/scenarios → marriage, separation, child to adult, change
  household.
- https://help.acst.com/realm/how-to/profiles/life-changes/scenarios-for-givingpledges-and-marriage → shared giving
  check box, giving numbers.
- https://help.acst.com/en/realm/whats-new/2022/november-2-2022 (search excerpt, Reported) → Relationships feature.
- https://help.acst.com/realm/how-to/profiles/profile-basics/change-a-family-members-position (via search) → Edit
  Individual permission.
- https://help.acst.com/en/go-method/how-to/integrations/church-management-software/realm/edit-realm-family-position-settings
  (via search) → adult/child default positions on sync.

CCB / Pushpay

- https://docs.pushpay.io/chms-v2/reference/createindividual → family_id required, family_position enum.
- https://docs.pushpay.io/chms-v2/reference/createfamily → Family resource fields incl. authorized_individuals, members,
  individual_id-as-primary-contact.
- https://docs.pushpay.io/chms-v1/reference/update_individual (via search) → v1 h/s/c/o codes.
- http://designccb.s3.amazonaws.com/helpdesk/files/official_docs/glossaryofterms.pdf → Primary Contact (exactly one),
  Family ID, Age Threshold, Child Work Approved.
- https://www.kidcheck.com/tutorial/church-community-builder-ccb-integration/ (via search, Reported) →
  position-to-guardian mapping.
- https://cornerstonenj.org/wp-content/uploads/2026/05/CCB-Rollout-Booklet-Rev2.pdf (via search, Reported) → primary
  contact/spouse edit family profiles.

MinistryPlatform

- https://help.acst.com/en/ministryplatform/help-topics/platform/contacts-and-related-records/households-or-families/households-faq
  → one name per household, splits, separated spouses, elderly parents, orphan households.
- https://help.acst.com/ministryplatform/help-topics/platform/contacts--related-records/households-or-families/split-households
  → split rationale, Individual statements alternative, Other Households.
- https://help.acst.com/ministryplatform/help-topics/platform/contacts-and-related-records/households-or-families/related-records
  → Other Households, Household Type check-in match, one primary household rule, Care Log.
- https://help.acst.com/ministryplatform/help-topics/platform/contacts-and-related-records/contacts/contact-relationships
  → Contact Relationships fields, reciprocal trigger, labels.
- https://help.acst.com/ministryplatform/help-topics/platform/customizations/extending-the-platform/system-lookups →
  Household Positions 1–6.
- https://help.acst.com/ministryplatform/help-topics/platform/contributions/donors/donor-statements/standard-statements/statement-donors
  and .../donor-statements (via search) → Statement Type Individual/Family, Statement Donors, Statement Method.
- https://www.ministryplatform.com/kb/ministryplatform/households (via search) → ≤2 heads of household.
- https://help.acst.com/ministryplatform/help-topics/platform/customizations/extending-the-platform/sql--databases/database-relationships
  (via search) → data dictionary by request.

FellowshipOne

- https://developer.fellowshipone.com/docs/v1/People/HouseholdMemberTypes.help → member types 1–4, 101.
- https://developer.fellowshipone.com/docs/v1/People/Households.help → Household resource.
- https://developer.fellowshipone.com/docs/v1/People/People.help → householdID / householdMemberType required,
  oldHouseholdID.
- https://help.fellowshipone.com/Content/People%20Management/Adding%20Household%20Members.htm → positions incl. Visitor
  and Other (extended family).
- https://help.fellowshipone.com/Content/People%20Management/Data%20Integrity/Merging%20and%20Household%20Giving.htm →
  household = Head+Spouse, household-attributed giving shared equally, merge options, roll-up statements.
- https://support.fellowshipone.com/hc/en-us/articles/19345068720923-Split-Individual-from-Household (search excerpt,
  Reported) → split procedure.
- https://help.fellowshipone.com/Content/Contributions%20Management/Contributions%20Application/Splitting%20a%20Contribution.htm
  (via search, Reported) → contributions cannot be split between households.

Shelby

- https://help.shelbyinc.com/arena/topics/idh-topic1580.htm → HOH computation, merge/address notes.
- https://help.shelbyinc.com/Arena/topics/idh-topic1620.htm → statement to HOH "which is the male", Contribute
  Individually on both spouses, Combine Families, Move Agent.
- https://community.shelbysystems.com/arena/f/arena-public-forum/14169/changing-family-role-from-child-to-adult
  (Reported) → autoadult scripts, giving_unit_id not updated.
- https://community.shelbysystems.com/arena/f/arena-public-forum/6314/parent-list-with-another-family-role (Reported) →
  Adult Child role breaks Parent List.
- http://isccon.shelbysystems.com/wp-content/uploads/2018/06/SNMPowerTipsM250.pdf,
  https://www.shelbysystems.com/info/shelbynext/ShelbyNext-Membership-Overview19.pdf,
  https://www.lcr.church/faq-for-shelbynext-giving/, https://www.softwareadvice.com/church/shelby-church-profile/ (all
  via search, Reported) → ShelbyNext family page, member family editing, family vs individual giving visibility, v5
  statement complaint.

Elvanto

- https://www.elvanto.com/api/people/create/ → family_id, family_relationship values.
- https://www.elvanto.com/api/people/getInfo/ → family fields in responses.
- https://help.elvanto.com/hc/en-us/articles/7481201498903 and /23921436664855 (search excerpts, Reported) →
  relationships list, sibling limitation, member family editing settings.

Alumni / community

- https://docs.hivebrite.com/partner/admin and https://hivebrite.io/features/member-directory/ → API sections, per-field
  visibility; no household construct.
- https://help.anthology.com/encompass/en/data/import-member-data.html,
  https://help.anthology.com/encompass/en/tips---tricks/expert-advice.html,
  https://support.imodules.com/hc/en-us/articles/218799267-Import-Non-Member-Merge-File (Reported) → import by
  constituent ID, guest roles capture spouse/child, non-member merge.
- https://feedback.almabase.com/giving/p/allowing-joint-gifts-from-alumni-couples-which-can-be-seen-on-leaderboards
  (Reported) → alumni-couple joint gift request answered with soft credits.
- https://community.blackbaud.com/discussion/comment/242085, /205659, /266375, /229415 and
  https://community.blackbaud.com/blogs/13/6889 (Reported) → spouse records, HoH practice and limits in RE NXT.
- https://gravyty.com/products/online-community-platform (marketing) → Graduway positioning; no data-model material.

Youth / sports / camp

- https://helpme.teamsnap.com/article/732-understanding-households → Household Organizer, multiple households, members
  without accounts.
- https://helpme.teamsnap.com/article/108-add-family-or-contacts-to-a-profile → contacts inherit member access; admin
  warning.
- https://helpme.teamsnap.com/article/1312-add-waivers-and-documents → per participant/registration, account-holder
  match, acknowledgement vs signature.
- https://teamsnapone.launchnotes.io/announcements/parents-can-now-manage-contacts-in-the-mobile-app and
  /ann_HryJedIq9Mfwp → guardian invites, Primary promotion, self-removal rule, adult-participant confirmation.
- https://help.sportsengine.com/en/articles/6645096-usa-volleyball-create-a-family-account → one account per family, 18+
  to sign.
- https://mobile-help.sportsengine.com/en/articles/8262121-what-is-the-difference-between-a-player-and-an-account-owner-profile
  → player profile has no login; owner manages sub-profiles.
- https://mobile-help.sportsengine.com/en/articles/8262157-how-to-add-a-guardian → Primary Account Owner adds guardians;
  pending.
- https://help.sportsengine.com/en/articles/6311072-how-to-remove-athlete-profiles and /10509528-add-a-person-in-myse
  (via search) → removal limits, Household tab.
- https://onboarding.campminder.com/en/articles/6598164-camper-application-configure-what-families-fill-out → Household
  2 mail/invoices, primary household pays, marital status.
- https://help.campminder.com/en/articles/8639225-get-to-know-the-unified-person-record (search excerpt, Reported) →
  Primary Childhood vs Principal Household.
- https://help.campminder.com/en/articles/9346030-… and /10068613-archive-individual-records (via search) → app access
  for both households; archive a divorced parent.
- https://kb.jumbula.com/portal/en/kb/articles/families-and-participants-14-5-2023 → families, parents tab,
  participants, authentication code.
- https://kb.jumbula.com/portal/en/kb/articles/how-can-i-add-a-follow-up-form-or-waiver-to-multiple-orders-14-5-2023 and
  https://spi.jumbula.com/registration-payment-tips (via search, Reported) → waiver states; account by parent/legal
  guardian.
- https://help.hisawyer.com/en/articles/11105600-customer-profiles-on-sawyer-for-business → Family tab, children and
  other adults on an account.
- https://support.hisawyer.com/en/articles/11326160-can-i-add-additional-family-members-to-my-account and
  https://help.hisawyer.com/en/articles/11105799-using-form-fields-for-policies-and-waivers (search excerpts, Reported)
  → Profiles, caregiver info, provider waivers.
