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) andGroupMember(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 oneGroupMember(status=active)with optionalvalidFrom/validUntilbounds. 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).
assertConsentresolves 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.
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /terminus/v1/persons | Create or list Persons. |
| GET / PATCH / DELETE | /terminus/v1/persons/{id} | Fetch, modify, or soft-delete a Person. |
| POST | /terminus/v1/persons/resolve | Resolve or create a Person by handle (email, phone, external id). |
| POST / GET | /terminus/v1/accounts | Manage auth + entitlement accounts on a Person. |
| POST / GET | /terminus/v1/groups | Create or list Groups (any kind). |
| POST / GET / DELETE | /terminus/v1/groups/{id}/members | Manage GroupMember rows on a Group. |
| POST | /terminus/v1/entitlement-checks | Evaluate a tier or group entitlement for a Person. |
| POST / GET | /terminus/v1/consents | Record or query consent state for a Person on a scope. |
| POST | /terminus/v1/consents/assert | Server-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.