Primitive spec

Trantor

The physical-world primitive. Trantor owns resources that have identity and location — rooms, courts, lanes, tables, chairs, stylists, kitchen stations, POS terminals — and the atomic allocation of those resources to other primitives that need to hold them.

Latest (July 2026): Trantor now owns physical access control — doors are Resources and QR self-check-in is live end to end: a fail-closed policy engine (membership, hours, bookings, waivers, staff roles, staff-on-shift), single-use member QR codes, fix-it deep links on denials, an explain debugger, and driver seams for cloud door controllers. It also gained ResourceAvailabilityWindow: published Riose shifts project staffing windows that gate discrete allocation, so an unstaffed hour is simply not bookable.

What it owns

Trantor models physical things. It does not own people (that is Terminus) — a stylist Resource in Trantor carries a terminusPersonId pointer and only the physical-state delta (current location, current status, calendar) that Terminus does not need to know about. One Person row in Terminus, one Resource row in Trantor that references it. No identity duplication.

Trantor also owns the act of allocation: the all-or-nothing hold that Seldon needs when it confirms a Booking and the inventory consume that Hober performs when it closes an Order. Allocation is pessimistic; conflict surfaces as a structured 409.

Concepts

ResourceType
A category of interchangeable physical things — styling_chair, standard_king_room, treatment_room, stylist, pos_terminal, kitchen_station, table. Allocation works against the type, not the instance.
Resource
A concrete instance of a ResourceType. Carries optional terminusPersonId for human resources, location, state (open / closed / out-of-service), and a calendar reference for availability windows.
ResourceHold
An atomic lock on N instances of a ResourceType for a specific consumer (typically a Seldon Booking). All-or-nothing: a request for 4 stylists at a moment that only has 3 free returns a structured conflict rather than holding 3. Expires if not consumed; an expired-hold sweeper releases them.
TrantorAllocator
The service Seldon and Hober inject directly today. Three methods: queryAvailableTypes (what is free in this window), holdResources (atomic hold, throws AllocationConflictException with structured shortfall on failure), releaseHolds (cancel a hold without consuming).
Discrete resource state
Some Resources have a long-lived state that is not a Hold. A dine-in table is held "until the tab closes" rather than "for a 90-minute window" — modelled as a state flag on the Resource (with the Order id in linkRef), not a ResourceHold row. Different physics, different storage.
Calendar-aware allocation
Resources can carry an availabilityTimeSchemeId pointing at a Seldon TimeScheme. The allocator respects open hours, maintenance windows, and per-resource calendars; a blocked window returns a structured closed_by_calendar shortfall in the 409 body so the caller can distinguish "not enough capacity" from "closed for the window."
ResourceAvailabilityWindow
Staffing-derived availability. Published Riose shifts project windows onto the staff member's Resource; a Resource with no windows is always available, one with windows is allocatable only inside a covering window. Discrete allocation subtracts window-excluded resources, and a reverse guard blocks canceling a shift that already has booking holds inside it.
Access control (doors are Resources)
An AccessPointBinding turns a Resource into a door; AccessPolicy rows (priority-ordered, allow/deny, optional schedule) gate it with rule kinds membership_tier, open_hours, has_booking (with zone or direct booked-resource matching), form_signed (waiver at the door), staff_role, and staff_on_shift. The decision core is fail-closed — no matching policy denies — and every decision writes an append-only AccessEvent plus an outbox row for reports and live feeds. Denials carry fix-it deep links; a staff-only explain debugger traces the policy walk without polluting the visit log.
Member QR + kiosk check-in
Rotating HMAC-signed member codes (60-second buckets, single-use via an embedded nonce and a replay ledger) plus never-expiring printed static passes. An allowed scan with a matching booking checks the booking in; a velocity flag raises a health signal when one person is admitted at two doors under five minutes apart.
Cloud ACS provider drivers
The same policies compile out to cloud door controllers through AccessProviderDriverInterface (Mock today; a Kisi driver is built but not yet validated against live hardware): grant sync pushes who-may-pass, signature-verified webhook ingest folds provider events into the same visit log, and a reconcile sweep repairs drift.

API surface

All endpoints are versioned under /trantor/v1/. Seldon and Hober typically call TrantorAllocator directly via dependency injection rather than through HTTP; the endpoints below are for admin tooling, external integrations, and the AI-driven authoring surface.

Trantor endpoints in the Foundation API reference OpenAPI 3.1 schema for Trantor with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST / GET/trantor/v1/resource-typesCreate or list ResourceTypes for the tenant.
GET / PUT / DELETE/trantor/v1/resource-types/{id}Fetch, replace, or soft-delete a ResourceType.
POST / GET/trantor/v1/resourcesCreate or list concrete Resources of a given type.
GET / PATCH / DELETE/trantor/v1/resources/{id}Fetch, modify (state, location, calendar), or soft-delete a Resource.
POST/trantor/v1/holdsPlace an atomic ResourceHold. Body specifies the type, count, and window. 409 on shortfall with structured detail.
DELETE/trantor/v1/holds/{id}Release a ResourceHold without consuming.
GET/trantor/v1/availabilityQuery free counts per type for a window.
POST/trantor/v1/access/checkinKiosk decision: QR token or person id against a door's policies. Allow may check a booking in; staff override supported.
POST/trantor/v1/access/qr/mintMint a rotating member QR (self) or a static printed pass (staff-only).
POST / GET/trantor/v1/access/policiesAuthor and list door policies (rule kinds above, priority + schedule).
POST / GET/trantor/v1/access/pointsCreate and list access points (door Resources + bindings).
GET/trantor/v1/access/eventsThe append-only visit log (decisions, reasons, methods).
POST/trantor/v1/access/debugExplain a decision: per-policy trace, no side effects. Staff-only.
POST/trantor/v1/access/webhooks/{providerConfigurationId}Signature-verified cloud-ACS event ingest (provider drivers).

Example: hold two stylists

Seldon's Booking flow calls this internally; the equivalent HTTP shape:

POST /trantor/v1/holds
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7a4b8c1f-…

{
  "resourceTypeId": "rt_stylist",
  "count": 2,
  "window": {
    "startsAt": "2026-06-12T09:00:00Z",
    "endsAt":   "2026-06-12T13:00:00Z"
  },
  "linkRef": {
    "kind": "seldon_booking",
    "id":   "bk_01JAZB…"
  }
}

If only one stylist is free, the response is a structured 409:

{
  "type":   "https://latticekit.tech/problems/allocation-conflict",
  "title":  "Allocation conflict",
  "status": 409,
  "detail": "Insufficient resources of type rt_stylist in window",
  "resourceTypeId": "rt_stylist",
  "requested": 2,
  "available": 1,
  "conflictingHoldIds": ["rh_01JAZC…"]
}

How it fits with the rest

flowchart LR
  S[Seldon Booking] -- hold --> T(Trantor)
  Ho[Hober close] -- consume --> T
  T -. identity for humans .-> Te[Terminus]
  Pa[Payments POS terminals] -. device .-> T
            

Seldon calls TrantorAllocator during Booking confirm to hold the resources the Offer requires. Hober calls it on Order close to consume Hardin recipe ingredients out of Trantor inventory. Terminus is referenced for the identity behind any human Resource. Payments POS Terminals are Trantor Resources of type pos_terminal, not a Payments-owned entity. Kitchen stations, tables, and printers all live here too — one physical-asset choke point.