Appearance
Namespace ownership
A "namespace" is a typed Space-path claim like space.alt.stories. The cloud system.auth actor's HostNamespaceStore owns these claims: who has them, what type each is, and how they're verified.
This page documents the three namespace categories, the verification levels, and the claim/release/transfer rules.
Three categories
space.<path> <- user-claimed, self-verified
domain.<path> <- domain-claimed, DNS TXT verified
system.<path> <- reserved, platform-managed| Category | Example | Who claims | Verification |
|---|---|---|---|
space.* | space.alt.stories.ghost-stories.funny | any signed-in principal | self (no proof beyond authentication) |
domain.* | domain.example.com | principal who owns the DNS | DNS TXT record proof |
system.* | system.gateway.http | platform itself | platform-managed; not user-claimable |
The namespace pattern (PUBLIC_NAMESPACE_PATTERN) is in host-auth.ts:1801. It enforces lowercase, kebab-allowed, dot-separated, with at most 63-char labels.
Claim rules (HostNamespaceStore.claim())
Source: host-auth.ts:616-720. Important rules:
Principal authentication required
claim rejects with Principal not found: <id> if the calling principal does not exist. Per CLAUDE.md Rule 5, the calling principalId is derived from transport metadata, not from a body field.
Categories restricted
ts
// projects/matrix-3/packages/system-auth/src/host-auth.ts:639-646
if (!publicNamespace.startsWith('space.')) {
return {
ok: false,
error: publicNamespace.startsWith('domain.')
? 'domain.* namespaces require DNS verification'
: 'system.* namespaces are reserved',
};
}auth.namespace.claim only accepts space.* claims today. domain.* claims need DNS verification (target state); system.* are reserved.
Already-claimed check
ts
if (this.resolve(publicNamespace)) {
return { ok: false, error: `Public namespace already claimed: ${publicNamespace}` };
}A namespace can have only one active claim at a time. Releasing returns it to the pool (target state for an explicit release op; today the only path is suspending a Space).
Parent namespace ownership
For nested claims (space.alt.stories.ghost-stories), the parent (space.alt.stories) MUST be owned by the same principal:
ts
// projects/matrix-3/packages/system-auth/src/host-auth.ts:654-661
if (parentNamespace) {
parentResolution = this.resolve(parentNamespace);
if (!parentResolution || parentResolution.space.ownerPrincipalId !== principal.id) {
return { ok: false, error: `Parent namespace is not delegated to ${principal.id}` };
}
}This means: a principal who owns space.alt.stories can sub-claim within that hierarchy. A different principal cannot sub-claim under someone else's parent.
Reserved route keys
A separate RESERVED_ROUTE_KEYS set (host-auth.ts:1803) holds HTTP-path reserved words: api, auth, login, logout, signup, apps, assets, static, system, admin, well-known, etc. A routeKey matching one of these is rejected by normalizeRouteKey.
DNS-like TLD rejection
isDnsLikeRouteKey (referenced in host-auth.ts:1834-1854) rejects multi-label keys whose last label looks like a real TLD (com, org, io, etc.). This prevents users from claiming space.example.com through the space.* path; that has to be claimed as domain.example.com with DNS verification.
Authority root derivation
When a claim succeeds, the resulting Space's authorityRoot is derived from the namespace:
text
space.alt.stories.ghost-stories.funny <- publicNamespace
spc_<hex> <- spaceId (durable)
space.spc-<hex> <- authorityRoot (canonical, new Spaces)
SPACE.<UPPERCASE-ROUTEKEY> <- legacy authorityRoot, still readablePer the authority model:
Authority root provenance for new Spaces. New Space
authorityRootvalues MUST be derived from durablespaceId, never from mutable public path text.
Legacy email-derived roots (COM.NIMBLETEC.RICHARD-SANTOMAURO) and legacy SPACE-prefix roots remain readable. New code derives from spaceId.
Transfer (target state)
Today, namespace ownership transfer is not a single op. The path is:
- Suspend the Space owning the claim (sets
status: 'suspended'). - Re-claim the namespace from the new principal (now-vacant claim slot).
Future state would have auth.namespace.transfer({ publicNamespace, fromPrincipalId, toPrincipalId }).
Resolution
HostNamespaceStore.resolve(publicNamespace) returns the active claim:
ts
{
claim: IHostPublicNamespaceClaim,
space: IHostSpaceRecord,
principal: IHostPrincipalRecord,
}…or null if not claimed. resolveRouteKey() does the same for routeKey-style queries.
Lifecycle states
IHostPublicNamespaceClaim.status (declared at host-auth.ts:59):
'active' -- live claim
'reserved' -- claimed but not yet active (target state)
'suspended' -- the owning Space is suspended
'released' -- explicitly released; namespace returns to the poolToday only 'active' and (rarely) 'suspended' are used. The 'released' state requires the explicit-release op which is target state.
Why this matters for security
Namespace ownership decides who controls a public Space-path. A compromised namespace claim means:
- The new owner's content appears at the legitimate URL.
- The previous owner's existing content moves to revoked URLs.
- Users following links may land on the new owner's content without noticing.
The defenses are:
- Claim mutation is gated by principal authentication.
- Cross-principal claim under someone else's parent is rejected.
- DNS-verified domains (target state) prove control of
domain.*claims. - Suspending a Space is a separate op from releasing the namespace, preventing accidental transfer during admin investigations.
See also
- Identity model — Space and authorityRoot.
- Reference / Claim schema — wire shape of
IHostPublicNamespaceClaim. WORKSTREAMS/core-and-packaging/MATRIX-AUTHORITY-MODEL.md§ "Public Space identity stack".WORKSTREAMS/loose-ends/items/P1.23f-public-spacepath-authority-root-v2.md— current spec forspaceId-derived authority roots.
Source:
projects/matrix-3/packages/system-auth/src/host-auth.ts(HostNamespaceStore.claim()at lines 616-720,normalizePublicNamespace,normalizeRouteKey,RESERVED_ROUTE_KEYS,DNS_LIKE_ROUTE_KEY_TLDS).