# Parties

> Companies and people an organization works with, with lifecycle stages, owners, who works where with a role and a primary contact, dedupe by domain and email, merges and keyset-paged search.

Source: https://bettersupabase.com/docs/blocks/parties

`parties` keeps the companies and people an organization sells to, supports or hires: the counterparty that deals, quotes, invoices, tickets and applications point at. A company has a name, a domain, a website, a VAT id, an address, an owner and a lifecycle stage. A person has a name, an email, a phone, a title, an owner, a lifecycle stage and optionally the user they sign in as. `company_people` records who works where, with a role and one primary contact per company.

Reading needs `parties.read`, creating, changing, linking and archiving needs `parties.write`, and merging needs `parties.manage`. Admins get every permission, and members get `parties.read` and `parties.write`.

```bash
better-supabase sql add parties
```

The module needs `tenant` and `access`.

```ts
import { createParties } from "better-supabase/blocks/parties";

const parties = createParties({ transport });
const acme = await parties
  .saveCompany(organizationId, {
    name: "Acme",
    domain: "acme.com",
    ownerId: userId,
  })
  .orThrow();
const ada = await parties
  .savePerson(organizationId, {
    name: "Ada Lovelace",
    email: "ada@acme.com",
    title: "CTO",
  })
  .orThrow();
await parties
  .linkPerson(acme.id, ada.id, { role: "Champion", primary: true })
  .orThrow();
```

## Companies and people [#companies-and-people]

`saveCompany` and `savePerson` create a record when the input has no `id`, and change the keys the input names otherwise: a key left out keeps its value, and `null` clears it. A new record needs a `name` (`PARTY_NAME_REQUIRED`). Domains and emails are stored in lower case, and an active record can't share its domain or email with another in the organization (`PARTY_DOMAIN_TAKEN`, `PARTY_EMAIL_TAKEN`), so an import finds the existing record instead of a duplicate.

A lifecycle stage is one of `options.companyLifecycles` (default `lead`, `prospect`, `customer`, `churned`) or `options.personLifecycles` (default `lead`, `mql`, `sql`, `opportunity`, `customer`); a new record starts at the first (`PARTY_LIFECYCLE_INVALID` for another value). `archiveCompany(id)` and `archivePerson(id)` hide a record from the lists, and `archived: false` restores it.

`linkPerson(companyId, personId, { role, primary })` links a person to a company or changes the link; a primary link replaces the company's previous primary contact. `peopleOf(companyId)` returns the company's people, primary contact first, and `unlinkPerson` removes a link.

## Search and pages [#search-and-pages]

`listCompanies` and `listPeople` return the active records by name, 50 per page (at most 200). `search` matches the name and the domain or email, and `lifecycle` and `ownerId` filter. Pass the page's `next` as `cursor` for the next page:

```ts
const page = await parties
  .listPeople(organizationId, { search: "acme", limit: 25 })
  .orThrow();
const more = page.next
  ? await parties
      .listPeople(organizationId, { search: "acme", cursor: page.next })
      .orThrow()
  : undefined;
```

The lists, `peopleOf` and the tables all run as the caller: a member reads the organization's records through the `parties.read` policy, and PostgREST reads the tables directly.

## Merge duplicates [#merge-duplicates]

`mergeCompanies(keep, drop)` and `mergePeople(keep, drop)` fill the kept record's empty fields from the dropped one, move the dropped record's links to it, and archive the dropped record with `merged_into` set. Merging needs `parties.manage` (`PARTY_MERGE_INVALID` for records of two organizations, one record twice, or one already merged). The `company.merged` and `person.merged` events carry `mergedId`, so a consumer moves the app's own references, such as deals or comments, to the kept record. A merge is written to the audit log.

## Use them as subjects [#use-them-as-subjects]

Register the tables in `config.subjects` with `partiesSubjects()`, so [comments](/docs/blocks/comments), [tags](/docs/blocks/tags), [attachments](/docs/blocks/attachments), [pipelines](/docs/blocks/pipelines) and the activity feed attach to companies and people:

```ts title="better-supabase.config.ts"
import { partiesSubjects } from "better-supabase/blocks/parties";

const { company, person } = partiesSubjects({
  url: { company: "/companies/{id}", person: "/people/{id}" },
});

export default defineConfig({
  subjects: { company, person },
});
```

With the [notifications](/docs/blocks/notifications) module installed, the owner watches a company or person when they become its owner, and a level they already chose stays.

## Extra columns [#extra-columns]

`options.extraColumns` adds columns to `companies`, such as a segment or an industry. `saveCompany` writes them from `fields`, and every company it returns carries them in `fields`:

```ts title="better-supabase.config.ts"
sql: {
  modules: {
    parties: { options: { extraColumns: { segment: "text", employees: "integer" } } },
  },
},
```

People take app fields through [custom fields](/docs/blocks/custom-fields) with the `person` subject.

## Events [#events]

| Event                                                    | Data                                              |
| -------------------------------------------------------- | ------------------------------------------------- |
| `company.created`, `company.updated`, `company.archived` | `organizationId`, `companyId`, `name`             |
| `company.merged`                                         | `organizationId`, `companyId`, `name`, `mergedId` |
| `company.person_linked`, `company.person_unlinked`       | `organizationId`, `companyId`, `personId`, `role` |
| `person.created`, `person.updated`, `person.archived`    | `organizationId`, `personId`, `name`              |
| `person.merged`                                          | `organizationId`, `personId`, `name`, `mergedId`  |

## Hooks [#hooks]

The module calls `before_company_save(tenant, company uuid, attrs jsonb)`, `after_company_save(company uuid, created boolean)`, `before_person_save` and `after_person_save` around its saves. A `before_` hook raises to refuse the write. The TypeScript client takes `hooks` for its methods (see [extending blocks](/docs/extending/blocks)).

## Wire it to ReUI [#wire-it-to-reui]

The ReUI CRM Contacts Directory reads `listPeople` for its grid, with the Filters `search` and lifecycle values passed through, and its bulk owner action calls `savePerson` with `ownerId` per selected row (or the [bulk](/docs/blocks/bulk) block for many at once). Its CSV import maps rows to `savePerson`, and an email that is taken points at the existing record.

## Errors [#errors]

| Hint                      | Cause                                                             |
| ------------------------- | ----------------------------------------------------------------- |
| `PARTY_FORBIDDEN`         | the caller lacks `parties.write`, or `parties.manage` for a merge |
| `PARTY_ATTRS_INVALID`     | the attributes aren't an object                                   |
| `PARTY_NAME_REQUIRED`     | a new record has no name                                          |
| `PARTY_LIFECYCLE_INVALID` | not a configured lifecycle stage                                  |
| `PARTY_DOMAIN_TAKEN`      | another active company has the domain                             |
| `PARTY_EMAIL_TAKEN`       | another active person has the email                               |
| `PARTY_NOT_FOUND`         | no such record                                                    |
| `PARTY_MERGE_INVALID`     | the records can't be merged                                       |