Appearance
Identity model
There are several distinct kinds of identity in the system. They look similar in places. They are not interchangeable.
The identity stack
External IdP (Google, GitHub, …)
| (iss, sub)
v
Principal (HiveCast) p_<sha20>
|
v
Space (HiveCast) spc_<hex>
|
v
authorityRoot e.g. space.spc-7f3a9b2c
|
+-- Host Link / Device hostlink_<hex> / host_<hex>
|
+-- Public namespace claim space.<spacePath>External identity → principal
The cloud system.auth actor maintains an externalIdentities[] table mapping (issuer, subject) from each IdP to a stable principalId. HostPrincipalStore.findOrCreate() in host-auth.ts:324-358:
ts
const id = stablePrincipalId(issuer, subject);
// stablePrincipalId(): SHA-256(`${issuer}\n${subject}`).slice(0, 20) → 'p_<hex>'The principalId is therefore stable: even if the principal's display name or email changes, the id derived from (issuer, subject) does not. Re-signing-in always lands the same principal record.
Principal → Space → authorityRoot
A principal owns one or more Spaces. A Space has:
spaceId— durable id,spc_<hex>or legacyspace_<20 hex>.authorityRoot— the bus / security / runtime root used by everything inside this Space. For new Spaces, derived fromspaceId(e.g.space.spc-7f3a9b2c). Legacy upper-caseSPACE.<UPPERCASE-ROUTEKEY>values remain readable.- Optional
publicNamespace— a typed namespace claim likespace.alt.stories.ghost-stories.funny. - Optional
routeKey— v1 alias forspacePath.
Per WORKSTREAMS/core-and-packaging/MATRIX-AUTHORITY-MODEL.md § "Public Space identity stack":
| Concept | Field | Example |
|---|---|---|
| User-facing public path | spacePath | alt.stories.ghost-stories.funny |
| Typed namespace claim | publicNamespace | space.alt.stories.ghost-stories.funny |
| Durable Space identity | spaceId | spc_7f3a9b2c |
| Internal authority root | authorityRoot | space.spc-7f3a9b2c |
spacePath and publicNamespace are user-facing / API names. New URLs should use spacePath. New code uses the canonical publicNamespace internally.
The phrase "route key" appears in routeKey fields and the CLI flag --route-key. It is a v1 compatibility alias. Treat it as "Space path" in user-facing copy.
Default Space rule
A principal's principalDefaultSpaces[] table records which Space is that principal's default. HostSpaceStore.getDefaultForPrincipal() is the lookup. When a Host Link is created without explicit Space hints, the cloud defaults to the principal's default Space (creating one if none exists yet).
Email-derived authority root (legacy / local)
For local-owner Hosts and historical pairings, the authority root is derived from the principal's email:
richard.santomauro@nimbletec.com → COM.NIMBLETEC.RICHARD-SANTOMAUROEmail TLD reversed, capitalised, hyphens preserved. This pattern is the v1 default. New Spaces use space.<spaceId> instead, derived from the durable spaceId. Legacy email-derived roots remain readable to preserve continuity.
Device identity
A Device has its own identity stack, separate from the principal:
| Concept | Field | Lifetime |
|---|---|---|
| Install identity | installId (install_<20 hex>) | per-install, never re-keyed |
| Link identity | hostId (host_<20 hex>) | per-pairing; reused across re-pairings of the same install |
| Link record | hostLinkId (hostlink_<24 hex>) | per-pairing; new id on each pair |
| Mutable label | hostName | mutable user-facing label |
| Stable projection key | deviceSlug | per pairing within a principal+Space |
See docs-devices/overview/device-model.md for the full Device identity table. The displayName field is forbidden as a Device label — displayName is principal/user identity only.
Identity from transport metadata
Per CLAUDE.md Rule 5:
Identity from transport metadata, NEVER from payload. Transport metadata = authenticated identity. Payload = untrusted. Never trust
payload.principalId.
Implications:
- HTTP requests: identity is the validated session JWT (cookie or Bearer header), not the body's
principalIdfield. - NATS request-reply: identity is the authenticated NATS user (the
authorized_userclaim), not a body field. - Bearer-token APIs (e.g. heartbeat): identity is whatever
verifyHeartbeatToken()returns from the bearer, not body fields.
SystemAuthActor.onAuthIdentityResolve() is the canonical "given these HTTP headers, what identity do they prove?" call. Other actors that need identity should call into it via auth.identity.resolve rather than re-implementing.
Sessions
HostSessionService (in host-auth.ts:196-313) issues two flavors of JWT:
| Token | TTL | Use |
|---|---|---|
mx_session cookie | 7 days (SESSION_DURATION_MS) | browser session |
| Bus token | 5 minutes (BUS_TOKEN_DURATION_MS) | scoped bus authentication |
Both are HS256-signed by a 32+ byte secret (auth.sessionSecret). The service rejects sessions with bad signature, wrong issuer, expired exp, or revoked jti. Revoked-session JTIs are kept in revokedSessions[] until their exp (then garbage collected).
See also
- Authentication / HiveCast account login — how the principal session is minted.
- Authentication / OAuth providers — provider-specific detail.
- Authorization / Namespace ownership — Space and namespace claim authority.
WORKSTREAMS/core-and-packaging/MATRIX-AUTHORITY-MODEL.md§ "Public Space identity stack".
Source:
projects/matrix-3/packages/system-auth/src/host-auth.ts(HostPrincipalStore,HostSpaceStore,HostNamespaceStore,HostSessionService);projects/matrix-3/packages/system-auth/src/google-oidc.ts(stablePrincipalIdconsumers).