Primitive spec

Terminus

The identity primitive. Terminus owns people, accounts, groups, memberships, and consents — the "who" and "what relationship" behind every booking, message, and ledger entry. It is the consent and entitlement choke point other primitives call before acting on a customer.

What it owns

Terminus is the source of truth for any non-physical entity that has identity and relationship: customers, families, businesses, teams, care relationships, tier programs. It owns persons, accounts (auth + entitlement), groups, memberships, consents, preferences, and tags. A stylist's name and consents live here; her current location lives in Trantor.

Two service methods do most of the cross-primitive work. checkEntitlement resolves tier-membership gates — Seldon calls it against every BookingAccessRule attached to an Offer's TimeScheme. assertConsent is the opt-in choke point that Speaker calls before any outbound communication. There is no path to bypass either.

Concepts

Person
A human. Single canonical row across the platform. Carries name, contact handles (email, phone), preferences, and tags. Other primitives reference Persons by opaque id (per_*); they do not duplicate Person attributes.
Account
An auth + entitlement surface for a Person. Separating Person from Account lets one human have multiple roles (operator account at one tenant, customer account at another) without duplicating identity.
RankedGroup
One generalised model for every "Person belongs to a thing" relationship: household, family, corporate, team, joint_account, care, tier, ad_hoc. Two tables: Group (kind + name + metadata) and GroupMember (person + role + rank + validity window + status). Rank gives ordered traversal (head of household first, primary signer first).
Tier membership
A specialisation of RankedGroup, not a separate entity. A Gold program is Group(kind=tier, name="Gold"); each enrolled customer is one GroupMember(status=active) with optional validFrom/validUntil bounds. Tier-gated booking access works through the same entitlement check as any other group membership.
Consent
A Person's recorded opt-in or opt-out for a specific scope (marketing email, transactional SMS, third-party data sharing). assertConsent resolves the live consent state — Speaker calls it on every outbound, with no override. Consent changes are append-only; the history is the audit trail.
TerminusDirectory
The service injected by Seldon and Speaker. Three methods: resolveOrCreatePerson (find by handle or create), checkEntitlement (does this person have an active GroupMember row in the named tier or group?), assertConsent (is the named scope currently consented for this person?).

API surface

All endpoints are versioned under /terminus/v1/. Identity writes are typically driven by application flows (signup, booking, consent capture), but the full CRUD surface is exposed for admin tooling and import.

MethodPathPurpose
POST / GET/terminus/v1/personsCreate or list Persons.
GET / PATCH / DELETE/terminus/v1/persons/{id}Fetch, modify, or soft-delete a Person.
POST/terminus/v1/persons/resolveResolve or create a Person by handle (email, phone, external id).
POST / GET/terminus/v1/accountsManage auth + entitlement accounts on a Person.
POST / GET/terminus/v1/groupsCreate or list Groups (any kind).
POST / GET / DELETE/terminus/v1/groups/{id}/membersManage GroupMember rows on a Group.
POST/terminus/v1/entitlement-checksEvaluate a tier or group entitlement for a Person.
POST / GET/terminus/v1/consentsRecord or query consent state for a Person on a scope.
POST/terminus/v1/consents/assertServer-to-server: is this scope currently consented? Returns boolean + version.

Example: a Gold tier and a member

Create the tier program as a Group:

POST /terminus/v1/groups
Content-Type: application/json
Authorization: Bearer <token>

{
  "kind": "tier",
  "name": "Gold",
  "metadata": { "displayName": "Gold Member" }
}

Enrol a Person with a 12-month window:

POST /terminus/v1/groups/grp_01JAB1…/members
Content-Type: application/json

{
  "personId":   "per_01JAB7…",
  "role":       "member",
  "rank":       0,
  "validFrom":  "2026-01-01T00:00:00Z",
  "validUntil": "2027-01-01T00:00:00Z",
  "status":     "active"
}

Seldon's Booking flow checks tier entitlement at quote time:

POST /terminus/v1/entitlement-checks

{
  "personId": "per_01JAB7…",
  "groupId":  "grp_01JAB1…"
}

→ { "entitled": true, "memberStatus": "active", "validUntil": "2027-01-01T00:00:00Z" }

How it fits with the rest

flowchart LR
  S[Seldon] -- checkEntitlement --> Te(Terminus)
  Sp[Speaker] -- assertConsent --> Te
  Pa[Payments] -. owner .-> Te
  M[Mallow] -. customer / employee .-> Te
  T[Trantor] -. identity for humans .-> Te
            

Seldon stores Person and Account refs on every Booking and calls checkEntitlement against tier-gated BookingAccessRules. Trantor Resources representing humans (stylists, baristas) carry terminusPersonId and only the physical-state delta. Speaker calls assertConsent on every outbound dispatch — the gate is in Speaker, not the caller, so no primitive can route around it. Payments PaymentMethods are owned by terminusPersonId. Mallow references Persons for customer and employee identity on ledger entries.