# People + Organizations

**Status:** partial — the schema + creation choke point shipped
(v0.10.0); the People/Organizations management UI and the satellite
registry API are the next R2 slices.
**Lives at:** `app/models/party.py` (`Party`, `Organization`, `Person`,
`PartyAffiliation`), `app/services/entities/parties.py` (creation
helpers), `alembic/versions/0006_people_organizations.py`,
`app/services/migrate/transforms.py` (`_insert_party` mirror).
**Spec:** `/srv/apps/work/docs/rebuild/02-domain-model.md` §1 (identity),
as refined by ADR #012/#013. Cross-app contract:
[../SATELLITE-CONTRACT.md](../SATELLITE-CONTRACT.md).

## Summary

An actor in the system is a **party** — the shared identity that every
relationship, invoice, project, and suite satellite points at. A party is
one of two **kinds**: a **Person** or an **Organization**. Those are the
first-class objects the app works in; "party" is the invisible plumbing
underneath that lets any reference point at "either a person or a
company" with one clean id.

## Why

Dated record of the ask (2026-07-10, rian). Standing up
`hosting.bowden.works` forced the question "does a hosted site reference a
client, an organization, or a party?" — and underneath it rian's
long-standing discomfort that a person and a company were stored as one
undifferentiated `parties` row with a `kind` flag. Three cases the flag
modelled badly:

- **Heather Bowden** — a person who is *also* her own one-person company.
  One row with one `kind` can't be both; she should be two linked records.
- **A login user who belongs to no organization** — a person must be able
  to stand alone, not only as an employee sub-record of a company.
- **A vendor** (Google, Airtable) — not a new type, just an organization
  playing the vendor *role* in a relationship. Confirmed that People and
  Organizations are the objects users think in (rian's Airtable
  Contacts/Companies), and roles live on relationships, not on the party.

## Behavior

- **`parties`** is the identity anchor and carries the identity common to
  every actor: `kind` (person|org), `name`, `slug`, contact
  (`email`/`phone`/`address`), `notes`, `is_active`. Unchanged by this
  work — every existing read still works.
- **`organizations`** (1:1 with an org party) holds what only a company
  has: `legal_name`, `currency`, and the reference facts satellites read
  (`drive_folder_url`, `mosiah_folder`).
- **`people`** (1:1 with a person party) is the first-class home for
  person-only fields as they arrive (job title, avatar, app-account
  linkage). Thin today — name/contact are common and live on the party.
- **`party_affiliations`** links a person party to an org party with a
  **role** (`owner`, `employee`, `contractor`, `contact`,
  `billing_contact`, `cc_recipient`, `member`, `other`), append-only
  (`started_on`/`ended_on`), at most one active per (person, org, role).
  This is the Heather link (person + org joined by an `owner` affiliation)
  and the home for employees-of-clients, cc-recipients, etc.
- **Three reference levels.** A relationship points at the level it means:
  either-kind → `parties.id` (invoice from/to, engagement party, a
  satellite's `owner_party_id`); an organization specifically →
  `organizations`/an org party; a person specifically → `people`/a person
  party (`users.person_party_id`).

## Constraints & edge cases

- **Additive by design.** No column moved off `parties`; the diff harness
  new-side emitter is unchanged, so parity holds by construction
  (`test_compare_green_after_clean_run` green with zero harness edits).
  This is the deliberate refinement of ADR #012 recorded in ADR #013.
- **1:1 invariant.** Every org party has exactly one `organizations` row,
  every person party exactly one `people` row — enforced at ONE creation
  choke point: `services/entities/parties.create_org_party` /
  `create_person_party` (ORM) and the transform `_insert_party` (raw SQL).
  Never construct `Party(...)` directly; use the helpers.
- **Cascade.** Detail + affiliation FKs are `ON DELETE CASCADE`, so the
  rebuild-from-legacy `DELETE FROM parties` clears them (they are NOT in
  the transform's `TRUNCATE_TABLES`).
- **Backfill.** Migration 0006 backfills one detail row per existing
  party by kind; affiliations start empty (no legacy source).
- **Naming.** `public.organizations` (this new subtype) ≠
  `legacy.organizations` (the old multi-org table, now `tenants`).
- **Kind-correctness of affiliations** (person_party_id is a person,
  org_party_id an org) is app-enforced at the creation path, not a DB
  CHECK (cross-table). A `person_party_id <> org_party_id` CHECK is in
  place.

## Permissions

No new capability. Reading/writing People, Organizations, and affiliations
will reuse the existing authz grid when the management UI lands (owner /
super-admin for cross-tenant entity edits, matching entity-manage today).
The satellite registry API will authenticate server-to-server (a service
credential), separate from end-user sessions — decided when that slice is
built.

## Open considerations

- **Satellite registry API** (`/api/registry/*`): resolve a `party_id` →
  `{kind, name, is_active}`, search/typeahead, create-organization (the
  one write; dedup via `near_matches`), org reference facts. The contract
  is frozen in [../SATELLITE-CONTRACT.md](../SATELLITE-CONTRACT.md); the
  implementation + its auth mechanism are the next slice.
- **People / Organizations management pages** — the first-class list + edit
  surfaces (org currency/folders, person fields, affiliations incl. the
  Heather owner-link). Today entities are still created only via the
  import resolver / team admin; a dedicated CRUD surface is R2 UI work.
- **Kind-guarantee via composite FK** (`UNIQUE (id, kind)` on parties +
  composite FKs) was considered and deferred — the creation choke point
  already guarantees correctness; revisit if a non-choke-point writer ever
  appears.

## Tests

- `tests/migrate/test_subtypes.py` — tables exist, `organizations`
  columns, the both-direction 1:1 invariant after a clean transform run,
  and cascade-keeps-rebuilds-clean.
- `tests/migrate/test_harness_parity.py::test_compare_green_after_clean_run`
  — unchanged and green: proves the refactor preserved all reporting
  behavior (the additive-change guarantee).
- Existing resolver (`tests/imports/test_resolver.py`) and team-admin
  (`tests/team_admin/`) tests exercise the creation helpers transparently.

## Changelog

- 2026-07-10 — v0.10.0. Shipped the schema + creation choke point:
  `organizations` / `people` / `party_affiliations` (alembic 0006),
  the `services/entities/parties` helpers, the transform mirror, and the
  1:1 + cascade tests. Foundation for the satellite registry API. ADR
  #012/#013.
