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.
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.kindispersonorservice; it holds the credentials and the identity key (email, lower-cased), and carries security state (status, failed attempts, lockout, MFA requirement). Email is unique perpoolScope: a fixedGLOBALsentinel 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 —typeispassword(argon2id/bcrypt),passkey(WebAuthn public key + counter),oauth(provider + subject),totp, orrecovery_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) → terminusPersonIdwith arole— 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 optionalparentTenantId), 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
TenantLinkand 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 carrysub(global principal),tnt(active tenant),psn(tenant-scoped Terminus Person), roles/scopes, andamr(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.
MfaFactorbacks TOTP plus single-use recovery codes; a tenant (or an ancestor node) can require it.Passkey+WebAuthnCeremonyback 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
SocialIdentityto an existing Principal by verified email (the provider list is pluggable). For redirect SSO without the LatticeKit reveal, Mannix is itself an OIDC provider —OidcClient+OidcAuthCodeback 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; anakey_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 trustedX-Foundation-Tenantheader. - 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.
| Method | Path | Purpose |
|---|---|---|
| POST | /mannix/v1/customer/register | Register a global Principal (email + password) and link it to the active tenant. |
| POST | /mannix/v1/customer/login | Authenticate against the global credential; set the LK_* cookie and issue tokens. |
| POST | /mannix/v1/customer/logout | Revoke the Session behind the refresh token. |
| GET | /mannix/v1/customer/me | The signed-in Principal plus its tenant-scoped Terminus Person. |
| POST | /mannix/v1/customer/token | Mint / 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-keys | Mint or list machine API keys for the tenant. |
| DELETE | /mannix/v1/admin/api-keys/{id} | Revoke an API key. |
| POST | /mannix/v1/service/token | Service-to-service token exchange for internal callers (kills header-trust). |
| GET | /mannix/v1/jwks.json | Public signing keys — how every primitive verifies a token offline. |
| GET | /.well-known/openid-configuration | OpenID 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.