Skip to content

Topic claims

A topic claim is an in-process assertion of authority over a subject prefix. The TopicClaimRegistry tracks which actor owns which prefix and prevents two actors from concurrently claiming the same one. It is distinct from the wire-level NATS ACL — topic claims are runtime-side discipline that catches collisions before they reach the bus.

Source: projects/matrix-3/packages/core/src/core/security/ITopicClaim.ts and TopicClaimRegistry.ts. The doc comments cite Joe Armstrong, Mark Miller, and Butler Lampson on namespace security.

The shape

ITopicClaim (verbatim from ITopicClaim.ts:25):

typescript
export interface ITopicClaim {
  prefix: string;            // 'flowpad.tree' claims all flowpad.tree.*
  owner: string;             // 'flowpad/tree' — owning component mount
  delegatedBy?: string;      // parent who authorized; absent for root claims
  claimedAt: number;
  // ... plus rights, expiresAt
}

A claim covers the prefix and everything under it. flowpad.tree claims:

SubjectCovered?
flowpad.treeyes (exact match)
flowpad.tree.childyes (descendant)
flowpad.tree.$inboxyes
flowpad.tree.accepts.*yes
flowpadNO — claim is on flowpad.tree, parent prefix is unclaimed
flowpad.canvasNO — different sibling under flowpad

Default rights

When an actor claims a prefix, it gets a default capability bundle (verbatim from TopicClaimRegistry.ts:17):

typescript
const DEFAULT_CAPABILITIES: CapabilityRight[] = ['read', 'write', 'invoke', 'emit'];

Most actors don't need to specify rights manually — claiming the prefix gives them everything needed to publish, subscribe, and receive ops.

Why claiming exists

Without claim tracking, two actors could subscribe to {root}.flowpad.tree.$inbox and both receive every message. The first to handle it wins; the other's reply is dropped. Symptoms: random intermittent failures depending on which actor handles the request.

The claim registry detects this at registration time:

typescript
// runtime-side flow
const result = topicClaimRegistry.claim({
  prefix: 'flowpad.tree',
  owner: 'flowpad/tree',
  delegatedBy: 'flowpad',
});

if (!result.granted) {
  throw new Error(`Topic claim denied: ${result.reason}`);
}

If flowpad/canvas already owns flowpad.tree (or a parent prefix that covers it), the claim is denied.

Prefix validation

The registry validates prefix strings before accepting a claim (TopicClaimRegistry.ts:50+):

RejectedReason
empty / non-stringinvalid input
.. (path traversal)namespace escape attempt
> or * (NATS wildcards)injection / over-claim
null bytesinjection
leading or trailing dotsmalformed prefix

Slashes (/) are NOT blocked, because owner mount paths use them (flowpad/tree). The prefix itself uses dots; the owner can use either.

Delegation chains

A child actor can claim a prefix under its parent's claim. The child's claim record carries delegatedBy: <parent-mount>. The registry verifies the parent's claim covers the child's prefix:

parent claim:  'flowpad'           owner='flowpad/root'
child claim:   'flowpad.tree'      owner='flowpad/tree'   delegatedBy='flowpad/root'

Without delegatedBy, only the parent's mount can claim the child prefix. With delegatedBy, the child claims authority granted by the parent.

Topic claims vs system.registry mount claims

Two distinct registries:

RegistryScopeRecords
TopicClaimRegistry (in-process)this runtime's view of which actor owns which subject prefixITopicClaim (prefix, owner, rights)
system.registry (cross-process)the bus-level "who serves what" claimIRegistryEntry (logicalMount, providerRuntimeId, runtimeWireRoot)

The runtime-side registry catches local conflicts (two actors in the same process). The bus-level registry catches cross-process conflicts (two runtimes both claim chat.conversation). Both are necessary.

Disabled by default

MatrixRuntime exposes the registry through topicClaimRegistry but defaults enableTopicClaiming: false (projects/matrix-3/packages/core/src/runtime/MatrixRuntime.ts:135):

typescript
this._enableTopicClaiming = config.enableTopicClaiming ?? false;
this._topicClaimRegistry = config.topicClaimRegistry ?? new TopicClaimRegistry();

Claiming is opt-in — packages that need strict prefix discipline turn it on. The runtime exposes the registry as an object regardless, so callers can inspect what's claimed even when enforcement is off.

Status: target state, partial. The registry is shipped; enforcement across all packages (auto-claim on actor mount, reject conflicting claims) is not yet on by default. The intent is to make it default-on once package boundaries are stable enough.

Bus-level enforcement

Topic claim discipline does not replace NATS ACL — it complements it. The wire-level boundary is still enforced by:

  • NATS account permissions per authority root (subject-prefix isolation).
  • Per-account import/export rules for cross-root federation.
  • Browser users get a scoped JWT that pins them to their root.

Topic claims catch collisions inside a single trust domain. NATS ACL catches cross-domain attempts.

See also