Skip to content

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
CategoryExampleWho claimsVerification
space.*space.alt.stories.ghost-stories.funnyany signed-in principalself (no proof beyond authentication)
domain.*domain.example.comprincipal who owns the DNSDNS TXT record proof
system.*system.gateway.httpplatform itselfplatform-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 readable

Per the authority model:

Authority root provenance for new Spaces. New Space authorityRoot values MUST be derived from durable spaceId, 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:

  1. Suspend the Space owning the claim (sets status: 'suspended').
  2. 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 pool

Today 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 for spaceId-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).