Primitive spec

Mannix

The authentication primitive. Mannix owns every credential, every token, and every auth flow on the platform — for customers, operators, and machines, across every tenant. Nothing else mints, stores, or validates a credential; every other primitive proves who is calling by verifying a Mannix-issued token.

Status: Live. Phases 0–7 are built — global Principal + password (Phase 0), RS256 access tokens + JWKS (0.5), the cross-tenant shared pool + per-tenant isolation toggle (1), operators absorbed as a Mannix relying party (2, with magic-link retired), TOTP MFA + recovery codes (3), passkeys / WebAuthn (4), social sign-in via Google (5), Mannix as an OIDC provider (6), and machine API keys + service-token exchange (7). Operator and customer authentication now run on Mannix in production, which cut over to the latticekit.app apex. It superseded the prior Demerzel operator auth and the day-old Terminus customer auth in a clean rebuild, salvaging only reusable plumbing. The June 10–11 hardening sweep added a per-account brute-force lockout on password credentials and put real rate limits on every credential endpoint.

What it owns

Mannix owns authentication — proving you are who you claim to be — and the credentials, sessions, and tokens behind it. It is the one hardened enforcer: it issues short-lived signed tokens, publishes a JWKS so any primitive can verify them offline, and recognises a fact the old model didn't — one human is a customer of many businesses. A global principal carries the credential, so a customer reuses one login across businesses without any business learning about another, and without ever seeing "LatticeKit".

Mannix is not identity-as-CRM — the tenant-scoped profile (people, groups, consents, preferences) stays in Terminus. It is not the authorization engine — it asserts a verified principal, node, staff status, and role/scope claims, and the primitive of record (whichever owns the resource being acted on) decides what may actually be done. And it does not send messages: verification and reset flows are Mannix-owned but Speaker-delivered, consent-checked, so the comms choke point holds.

Concepts

Principal
The global identity — one prnc_ per real human or machine, deliberately not tenant-scoped, because its cross-tenant nature is the whole point. kind is person or service; it holds the credentials and the identity key (email, lower-cased), and carries security state (status, failed attempts, lockout, MFA requirement). Email is unique per poolScope: a fixed GLOBAL sentinel for the shared pool, or a tenant id for a tenant that opted out of sharing (a separate credential silo).
Credential
One cred_ per proof attached to a Principal — type is password (argon2id/bcrypt), passkey (WebAuthn public key + counter), oauth (provider + subject), totp, or recovery_code. One Principal has many. Secret material is encrypted at rest behind the same Foundation credential-master-key boundary used for Speaker and Payments provider secrets.
TenantLink & the two axes
A tlnk_ joins (principalId, tenantId) → terminusPersonId with a role — it resolves "the global Jane" down to "Acme's customer Jane" per request. Authentication spans two axes that never bleed into each other: customers are flat and global (one Principal, N customer links); operators sit in a recursive tenancy tree (a tenant gains an optional parentTenantId), where authority is a node plus its subtree — LatticeKit staff govern everything, a reseller's staff govern all their locations, a location's staff govern just their location.
The cross-tenant pool
Shared credentials are default on, per-tenant toggleable: Jane signs in on Acme's own branded page with her email + password, Mannix matches the global credential, finds-or-creates her Acme TenantLink and a fresh Acme-scoped Terminus Person, and issues an Acme-scoped token. Acme learns nothing about her other businesses; she never makes a new password. A tenant can flip its pool off for an isolated identity silo. Verified-email portability is on; broader profile sharing is off and consent-gated. No business-to-business visibility, ever — enforced with tests.
Session & tokens
A sess_ is the revocable grant: a long-lived opaque refresh token (hashed, device-bound) used to mint short-lived RS256 JWT access tokens. Claims carry sub (global principal), tnt (active tenant), psn (tenant-scoped Terminus Person), roles/scopes, and amr (methods used, including MFA). Access tokens are verified offline by any primitive against the JWKS — no hot-path call back to Mannix. The no-bleed invariant: a token is always scoped to exactly one (node, role) context, so a human who is both an operator and a customer keeps the two surfaces completely isolated.
MFA & passkeys
Step-up security on a Principal. MfaFactor backs TOTP plus single-use recovery codes; a tenant (or an ancestor node) can require it. Passkey + WebAuthnCeremony back passwordless, phishing-resistant WebAuthn — enrolled as "this computer" (platform authenticator) or "phone / security key" (roaming). Passkeys are origin-bound, so password + verified email stays the cross-business reuse mechanism while passkeys reuse within a brand's domains.
Social & federation
"Continue with Google" links a SocialIdentity to an existing Principal by verified email (the provider list is pluggable). For redirect SSO without the LatticeKit reveal, Mannix is itself an OIDC providerOidcClient + OidcAuthCode back a standard authorize / token / userinfo flow and an OpenID discovery document, so third-party apps and tenants on a branded auth domain can "Login with LatticeKit".
Service principals & API keys
Machine callers are Principals with kind=service; an akey_ API key (tenant-issued) or a short-lived service token exchange authenticates them. This ends the deferred header-trust posture — internal callers (render proxy → API, worker → API) and webhooks carry a real, verifiable Mannix token rather than a trusted X-Foundation-Tenant header.
TenantAuthConfig & base policy
A tacf_ holds per-tenant auth config — the shared-pool toggle (default on), enabled methods, a password-policy override, social/federation providers, and the branded auth domain. Mannix owns the base security policy (min password strength, MFA requirement, session TTLs, lockout thresholds, rate limits); every node in the tenancy tree may tighten it but never weaken below an ancestor's floor — so a reseller can mandate MFA across all its locations and a location can't undo it.
Signing keys & JWKS
A rotating set of RS256 SigningKeys signs every token; the public set is published at the JWKS endpoint, with current + recent keys so in-flight tokens stay verifiable across a rotation. This is what makes offline verification by every other primitive possible.
Authorization lives elsewhere
Mannix authenticates and asserts; it does not decide permissions. There is no central role→permission catalog. Each primitive of record reads the Mannix token's role / scope / node claims and maps them onto its own permissions for its own records — Mallow decides who may void an invoice, Seldon who may edit a schedule, Hardin who may change the menu.

API surface

All endpoints are versioned under /mannix/v1/ (plus the well-known OIDC routes) and return RFC 7807 problem details on error. Customer flows are embedded / same-origin — the login UI lives on the tenant's own page and posts server-side through the render proxy, so there is no redirect and nothing to reveal. Operator sign-in, passkey, and MFA enrollment are driven from the Demerzel console as a Mannix relying party.

Mannix endpoints in the Foundation API reference OpenAPI 3.1 schema for Mannix with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST/mannix/v1/customer/registerRegister a global Principal (email + password) and link it to the active tenant.
POST/mannix/v1/customer/loginAuthenticate against the global credential; set the LK_* cookie and issue tokens.
POST/mannix/v1/customer/logoutRevoke the Session behind the refresh token.
GET/mannix/v1/customer/meThe signed-in Principal plus its tenant-scoped Terminus Person.
POST/mannix/v1/customer/tokenMint / refresh a short-lived access token from the Session.
GET / PUT/mannix/v1/admin/tenant-auth-config/{tenantId}Read or update a tenant's auth config — pool toggle, enabled methods, password policy.
POST / GET/mannix/v1/admin/api-keysMint or list machine API keys for the tenant.
DELETE/mannix/v1/admin/api-keys/{id}Revoke an API key.
POST/mannix/v1/service/tokenService-to-service token exchange for internal callers (kills header-trust).
GET/mannix/v1/jwks.jsonPublic signing keys — how every primitive verifies a token offline.
GET/.well-known/openid-configurationOpenID discovery document (Mannix as an OIDC provider).
GET / POST / GET/mannix/v1/oidc/{authorize,token,userinfo}The OIDC provider flow for "Login with LatticeKit" and branded-domain SSO.

Example: embedded customer login

Jane signs in on Acme's own page. The render proxy posts same-origin to Mannix; no redirect, no LatticeKit reveal:

POST /mannix/v1/customer/login
Content-Type: application/json
Host: book.acme.com

{
  "email":    "jane@example.com",
  "password": "<secret>"
}

→ 200 OK
   Set-Cookie: LK_SESSION=<opaque-refresh>; HttpOnly; Secure; SameSite=Lax
   {
     "principalId": "prnc_01JABZ…",
     "tenant":      "t_acme",
     "personId":    "per_01JAC1…"
   }

The access token any primitive then verifies offline against the JWKS — decoded claims:

{
  "iss": "https://id.latticekit.app",
  "sub": "prnc_01JABZ…",          // global principal
  "tnt": "t_acme",                // active tenant context
  "psn": "per_01JAC1…",           // tenant-scoped Terminus Person
  "roles": ["customer"],
  "amr": ["pwd"],                 // methods used (add "otp" / "webauthn" with MFA)
  "exp": 1779999999
}

How it fits with the rest

flowchart LR
  Cust[Customer site] -- embedded login --> Mx(Mannix)
  Op[Operator console] -- relying party --> Mx
  Mx -. verify / reset .-> Sp[Speaker]
  Mx <-. principal link .-> Te[Terminus]
  Mx -- publishes --> J[(JWKS)]
  J -. offline verify .-> P{{Every primitive}}
            

Every primitive verifies a Mannix token offline against the JWKS — that is the contract. Terminus keeps the tenant-scoped profile; each Person carries a mannixPrincipalId backlink, and "is this email verified?" is a Mannix fact Terminus reads but never sets. Speaker delivers the verification, reset, and magic-link messages Mannix initiates, still consent-checked. Demerzel stops owning operator auth and becomes a relying party — operators are principals with a staff role on a node of the tenancy tree, and act-as is a Mannix-issued scoped token. Authorization stays with whichever primitive owns the resource: Mannix says who, the owner says what they may do.