EXTENSION-RELAY

Version: 1.4 Status: Active Depends: ENTITY-CORE-PROTOCOL.md (v7.40+) — the only prerequisite; a relay peer is just a peer running system/relay, and the origin's capability chain passes through unchanged (§1). Related: EXTENSION-ROUTE.md (consulted for a next hop only when a forward-request carries no source route — one of three sources, §3.1.1); EXTENSION-INBOX.md, EXTENSION-CONTINUATION.md, EXTENSION-NETWORK.md, EXTENSION-REGISTRY.md, EXTENSION-DISCOVERY.md (composition surfaces named in §1); EXTENSION-ENCRYPTION.md (peer-mode payloads carried opaquely) Tier: Operational — Tier 2b (network), per SYSTEM-ARCHITECTURE.md §13.1. Authors: Architecture team.

Two prior arch rulings are load-bearing and unchanged by this landing:

Landscape grounding: the relay-and-aggregator pattern study (Nostr / AT Protocol / ActivityPub / libp2p circuit-relay / SMTP-NNTP → four canonical modes).


§1 Concept

A relay is an intermediary that carries opaque, signed, capability-bearing envelopes between two endpoints. The origin's capability chain passes through unchanged; the intermediary is transport. Four canonical modes are exposed by the single system/relay handler:

A relay peer is just a peer running system/relay. No special infrastructure role; same substrate as everything else. Operators install whichever modes their deployment supports and advertise them.

v1 ships Mode F (forward) + Mode S (store-and-poll). Mode A and Mode C entity types and operations are named for forward-compatibility; their normative spec text is deferred to follow-on proposals.

This spec defines the v1 modes, the entity types per mode, the handler operation surface, the capability model (per the ANALYSIS ruling), and composition with the landed INBOX, CONTINUATION, NETWORK, REGISTRY, and DISCOVERY extensions plus the in-flight STORAGE-SUBSTITUTE-HTTP / BRIDGE-HTTP / ENCRYPTION proposals.


§2 Modes table

ModePush/PullPersistent?Multi-author?RoutingStateful?
F (Forward)pushtransientsingle-per-enveloperouted-to-destinationtransparent
S (Store-and-poll)put-then-pollpersistentsingle-per-envelopeaddressed-by-namespacetransparent
A (Aggregate, deferred)publish-then-subscribepersistentmulti-publisherbroadcast or filteredmay add stream ordering
C (Circuit, deferred)bidirectionaltransientsingle-per-circuitrouted via reservationtransparent

Operators select modes per deployment; the spec exposes the mechanism, not the policy.


§3 Entity types

§3.0 Representation conventions

§3.1 Mode F — Forward

type: "system/relay/forward-request"
data: {
  destination:     <peer_id>,            ; terminal recipient (Base58, §3.0)
  route:           [<peer_id>] | null,   ; v1.1 — the remaining relay hops, in order, ending
                                         ;   at `destination`; the originator's SOURCE ROUTE.
                                         ;   null/absent → single-hop (use `next_hop`) or
                                         ;   table-routed (no route + no next_hop → §3.1.1).
                                         ;   omitempty: a v1.0 single-hop request encodes
                                         ;   byte-identically (the field is absent when null).
  next_hop:        <peer_id | null>,     ; single-hop shorthand / route-table output (Base58).
                                         ;   When `route` is present and non-empty, `next_hop`
                                         ;   is advisory and MUST equal route[0] if both set
                                         ;   (else invalid_request/400, pre-dispatch).
  ttl_hops:        u32,                  ; relay-transport hop budget; decremented per hop
  expires_at:      <timestamp | null>,   ; v1.3 — ms since epoch. The ORIGINATOR's deadline for
                                         ;   this envelope, in the outer request where a relay
                                         ;   may read it. Optional; omitempty, so a request
                                         ;   without one encodes byte-identically to v1.2.
                                         ;   On the §6.2.1 store fallback it becomes the
                                         ;   store-entry's `expires_at`, clamped by §8.1.
  envelope_inner:  <bstr>                ; the content hash (bare system/hash form: format
                                         ;   byte + digest, LENGTH FOLLOWS THE FORMAT BYTE --
                                         ;   33 B under SHA-256, 49 B under SHA-384; never
                                         ;   fixed, SPECIFICATION-FORMAT.md 8.4.5) of the
                                         ;   carried inner envelope; the inner entity travels
                                         ;   as opaque raw bytes in the envelope's `included`
}

route is the forward path (v1.1). It lists the relay hops after the immediate dispatch target, ending at destination. The originator sends the forward-request EXECUTE to the first relay; that relay is not in route. route is the inverse of V2.0's path: [peer_id] (where you've been) — it is where you are going. route and next_hop are both absent for the table-routed case (§3.1.1 reads the local route table per EXTENSION-ROUTE.md); precedence is source route > route table > direct (§3.1.1).

envelope_inner is a data field carrying the raw content-hash bytes — not a refs entry (cohort convergence R6/R7, 3-impl). The load-bearing property is that it is raw and unwrapped, not that it is 33 bytes: its length follows its own format byte (SPECIFICATION-FORMAT.md §8.4.5), and a CBOR bstr is self-delimiting, so a relay reads it without knowing the originator's home content_hash_format — which it must not need to, since it never decodes the inner entity. The relay reads the hash from data to address the carried entity and forwards the inner entity's raw bytes verbatim from the included set; it never decodes them. Content-store dedup is preserved (the inner is still content-addressed by the hash).

The carried inner envelope is a full materialized system/envelope{root, included} — not a bare root (§2.3 ruling; the inner must carry its own signatures/caps so the terminal hop delivers a self-contained, independently-verifiable message; a bare root would arrive unverifiable). The relay treats it as opaque bytes regardless (§9).

The inner envelope MAY be encrypted (per the ENCRYPTION proposal); the relay sees only an opaque content-addressed blob it never decodes (§9). The relay MUST decrement ttl_hops on each forward and MUST reject (fail-closed) if it is 0 on receipt.

ttl_hops vs V7 bounds.ttl. ttl_hops bounds the relay transport path length and lives on the relay envelope. It is distinct from V7 bounds.ttl, which bounds the computation/dispatch chain (with await_stack cycle detection) and travels inside the inner envelope — which the relay cannot read. A single logical dispatch may cross N relay hops. The relay decrements only ttl_hops and never touches the inner envelope's bounds.

expires_at is the same move for time, and for the same reason ttl_hops exists [v1.3]. The originator's deadline for a message is bounds.ttl_absolute, which travels inside the inner envelope where the relay is forbidden to look (§9). So without an outer field the relay picks the expiry for a message it is holding on someone else's behalf — on the §6.2.1 store fallback it constructs the store-entry itself, and the only party who knows how long the message is worth holding cannot say so. A bound whose enforcing party cannot read it is not a bound. expires_at on the outer request carries the deadline where the relay may read it, mirroring store-entry.expires_at (§3.2), and it is the same outer-copy pattern ttl_hops already establishes for hop count.

Precedence, stated so two conformant relays agree. On the §6.2.1 fallback the constructed store-entry.expires_at is the request's expires_at, clamped by §8.1's ceiling; where the request carries none, the ceiling applies as if it were null. A relay MUST NOT extend a deadline the originator set. expires_at bounds the envelope's storage lifetime and has no effect on a live forward, which either succeeds or falls back within one operation.

Async response correlation is not a relay field — it is carried inside the opaque inner envelope (INBOX deliver_to / deliver_token); see §6.2.

§3.1.1 Per-hop next-hop determination + intermediate vs terminal hop (the dispatch shape)

On receiving a forward-request, a relay first determines its next hop from exactly three sources, consulted in this precedence order (the relay is the forwarding plane; the choice of path is the routing planeEXTENSION-ROUTE.md §3):

  1. Source route (route, §3.1) — if route is present and non-empty, next = route[0]. The originator dictated the path. (If next_hop is also set it MUST equal route[0]; a mismatch is invalid_request/400 pre-dispatch, before any forward.)
  2. next_hop shorthand — else if next_hop is set, next = next_hop (the degenerate single-hop / single-element route; v1.0 behavior).
  3. Route table — else (both absent) consult the local route table per EXTENSION-ROUTE.md §3 (exact match or "*" default, lowest metric, expiry-skipped). The table yields Deliver (terminal), Forward(via) (next = via), or no match → no_route/502. A peer with no route table installed at all applies the trivial direct-or-no_route default (deliver iff destination is directly reachable, else no_route/502) — exactly v1.0 behavior.

A relay is at the terminal hop when next == destination (the destination is the chosen next hop / directly reachable from this relay). Otherwise it is at an intermediate hop.

Worked example A→B→C→D (D = destination): A → B {destination: D, route: [C, D], ttl_hops: 8}; B sees next = C ≠ D → intermediate, forwards B → C {destination: D, route: [D], next_hop: D, ttl_hops: 7}; C sees next = D == destination → terminal, delivers the bare inner envelope to D. D verifies the inner signature exactly as on a direct connection.

TTL is the loop bound. ttl_hops MUST be ≥ the route length for the route to complete; on receipt with ttl_hops == 0 the relay rejects ttl_exhausted/400 (fail-closed, no partial delivery). An explicit finite path + TTL is self-bounding — no separate loop-detection state is required (distinct from the inner envelope's bounds.await_stack, which detects request/response cycles a relay cannot see — §3.1).

Raw-frame (not decode-then-redispatch) is required to deliver true byte-identity-to-direct: a decode/re-encode round-trip can be byte-identical for ECF-canonical inputs but is not guaranteed to be, and §3.1.1's promise is exactness. This resolves the Rust (raw-frame) vs Python (decode-then-redispatch) divergence in favor of raw-frame; an impl whose dispatch hookpoint takes an inner entity whose .Data is raw bytes writes .Data verbatim rather than decoding it.

This is required by the cap-chain ruling (§5.1): the destination verifies the inner envelope's signature + capability chain exactly as on a direct connection, and therefore MUST NOT need the RELAY extension installed merely to receive a forwarded message. The terminal relay delivering its own relay-typed wrapper, or a re-encoded inner, would break the transparency guarantee and the byte-identical-to-direct property (§9). For a browser/NAT'd destination the "dispatch" is the push down the held outbound session (§6.2.1); the verbatim inner-envelope bytes are what is pushed.

§3.2 Mode S — Store-and-poll

type: "system/relay/store-entry"
data: {
  namespace:       <path>,               ; where the receiver polls
  expires_at:      <timestamp | null>,   ; ms since epoch; standard cap-style expiry
  put_by:          <peer_id>,            ; the authenticated session/connection peer (Base58, §3.0)
  envelope_inner:  <bstr>                ; content hash of the carried inner envelope
                                         ;   (data field, raw bstr, length per format byte
                                         ;    — as §3.1)
}

Both the store-entry and the inner envelope are tree-bound (the relay indexes everything it serves in the tree, so the standard tree-handler cap check governs per-namespace access):

:put (and the §6.2.1 :fallback) MUST establish both bindings. The receiver polls system/relay:poll against namespace; the relay returns enumerated store-entry hashes (§4.2), which the receiver fetches via tree:get on the namespace-scoped paths — system/content is NOT a receive-side dependency (§4.2). STORAGE-SUBSTITUTE-HTTP is the first deployed Mode S — a static-CDN-hosted peer's tree IS a Mode S relay namespace (§6.5).

put_by is placement-identity, not authorship. put_by MUST equal the authenticated session/connection peer — the peer that placed this entry over this connection (§2.2 ruling). The relay sets/verifies it from the authenticated connection identity and rejects a mismatch (put_by_mismatch/400, §4.3). Using the session/connection peer (not the wire-EXECUTE author) is what makes put_by mean placement: on a cross-peer dispatch the two differ, and placement is the connecting peer. This makes put_by trustworthy for abuse attribution, rate-limiting, and GC. It is NOT an authorship claim: the actual author is whoever signed the inner envelope (V7 §5.2), which the relay cannot read. A poller MUST derive authorship from the inner envelope's signature, never from put_by. (In the §6.2.1 fallback path the forwarding relay places the entry on the origin's behalf, so put_by = the relay's session identity, while authorship remains the origin's inner-envelope signature — exactly why the two must stay distinct.)

§3.3 Mode A — Aggregate (deferred from v1)

Entity type named for forward-compatibility; not normatively specified (Mode A deferred — §11.1a):

type: "system/relay/aggregate-subscription"
data: {
  source_peers:    [<peer_id>],          ; publishers being aggregated (Base58)
  source_subtrees: [<path_pattern>],     ; per-publisher subtree filter
  filter:          <filter expression | null>,   ; per-event filter (opaque to relay)
  emit_to:         <namespace>           ; where the aggregated stream surfaces
}

§3.4 Mode C — Circuit (deferred to follow-on)

Named for forward-compatibility; not normatively specified:

type: "system/relay/circuit-reservation"  ; reservation slot
type: "system/relay/circuit-dial"         ; connect through reserved slot

§3.5 The inbox-relay declaration — the MX-equivalent (closes Q2)

A peer publishes a signed declaration of where its mail is stored when it is unreachable — the DNS-MX analog. This is what gives the §6.2.1 Mode-S fallback a resolvable, self-certifying target, closing the v1.0 Q2 gap (which-relay-holds-my-mail).

type: "system/peer/inbox-relay"
data: {
  relays: [
    { relay:     <peer_id>,            ; the relay holding my mail (Base58, §3.0)
      namespace: <path>,               ; where to put it (default: my own peer_id)
      priority:  u32 }                 ; lower = preferred (MX-priority; backups higher)
  ],
  expires_at:  <timestamp | null>      ; ms since epoch; null = until superseded
}
; SIGNED by the declaring peer per V7 §5.2; signature reachable at
; system/signature/{hex(content_hash)}. No refs: block.

Authored by the peer; served by an always-on holder. The peer authors and signs the declaration (it is the source of truth, self-certifying). But it MUST be served for resolution by a holder that is up when the peer is down — the MX record lives in the directory, not on the mail server. Authority travels with the signature, not the host (V7 §1.4 universal namespace), so any holder serves it authoritatively without being trusted: it can serve or withhold the signed bytes, but cannot forge or redirect.

Resolution (always-on first): REGISTRY is the primary home — a name resolution returns the inbox-relay declaration alongside the name→peer binding (the A+MX-in-one-zone pattern; the registry serves a synced copy referenced by the binding's current head). Secondary holders: the relay named in the declaration (itself always-on), and a local/relationship cache. The peer's own tree is the authoritative origin but not the fallback-path source (it's offline exactly when needed). A resolver MUST verify the declaration's signature against the resolved peer_id and reject (fail-closed) on mismatch, and MUST try relays in ascending priority order. A peer with no registry presence and no prior relationship is reachable by a stranger only via a cached copy — honest parity with a mail server that has no DNS entry.

(Full design + security audit + the 6 cross-impl test vectors incl. the end-to-end INBOX-RELAY-FALLBACK-1: proposals/implemented/PROPOSAL-PEER-INBOX-RELAY-MX-EQUIVALENT.md.)


§4 Handler operations

system/relay:forward             ; Mode F — accept envelope; forward to next hop
system/relay:put                 ; Mode S — accept envelope; store at namespace
system/relay:poll                ; Mode S — receiver polls a namespace for entries
system/relay:advertise           ; All — relay announces supported modes + limits

Deferred — Mode A: system/relay:subscribe / :unsubscribe. Mode C: system/relay:reserve-circuit / :dial-circuit.

§4.1 system/relay:advertise (all modes)

A relay announces its capabilities via an entity at system/relay/advertise/{relay_peer_id}:

type: "system/relay/advertise"
data: {
  modes:           [<mode_id: "F"|"S">],             ; v1 modes; "A"/"C" when they land
  endpoints:       [<dial-able endpoint per NETWORK §6.5>],
  limits: {
    max_envelope_size:    u64 (optional),
    max_storage_bytes:    u64 (optional, Mode S; the metric is defined in §8.2),
    max_retention_ms:     u64 (optional, Mode S; the retention CEILING, §8),
    forward_rate_limit:   u32 (optional, Mode F; envelopes/sec)
  },
  caps_required:   [<cap_path>],                     ; what cap is needed to use this relay
  expires_at:      <timestamp | null>                ; ms since epoch
}

The advertise entity MUST be signed by relay_peer_id per V7 §5.2; its signature is reachable at system/signature/{hex(advertise.content_hash)} (no refs: block — §3.0). Consumers query this entity (standard tree fetch) to see what's available and what cap they need. Finding the relay's peer_id is DISCOVERY's / REGISTRY's job — §6.7.

max_retention_ms is how long, where the rest of limits is how big [MUST when present, v1.3]. A Mode S relay that enforces a retention ceiling (§8) MUST publish it here. Duration is the number that decides whether store-and-forward is usable: a sender choosing a relay, and a peer choosing which relays to name in its own system/peer/inbox-relay declaration (§3.5), both depend on how long an entry will be held, and neither can otherwise learn it. A ceiling that is enforced but unpublished configures behaviour no counterparty can observe before depending on it. Absent means the relay declares no ceiling; it does not mean unbounded — see §8.

An advertised limit is a promise a relay can break, exactly like every other limit in this block. §5.1's threat model already bounds what an intermediary can do — drop or delay — so publishing a retention ceiling discloses nothing new and creates no new failure class. It gives a counterparty something to plan against.

§4.2 Operation wire schemas (Mode F + Mode S)

Each op is an EXECUTE against the system/relay handler carrying a request entity; the relay returns a result entity or a V7 ERROR (§4.3). The carried inner envelope stays opaque throughout (§9) — these schemas describe only the relay-envelope surface.

system/relay:forward (Mode F). Request IS a system/relay/forward-request (§3.1). Result:

type: "system/relay/forward-result"
data: {
  status:          "forwarded" | "queued-fallback" | "rejected",
  next_hop:        <peer_id | null>,         ; hop actually used, if forwarded
  stored_at:       <path | null>             ; namespace, if queued-fallback (§6.2.1)
}

ttl_hops is decremented before forward; ttl_exhausted if 0 on receipt. If the destination has no live session, the relay falls back to Mode S and returns queued-fallback with stored_at set (§6.2.1). Async response correlation, if any, rides inside the inner envelope (§6.2) — the relay-level result is only the immediate ack.

system/relay:put (Mode S). Request IS a system/relay/store-entry (§3.2). Result:

type: "system/relay/put-result"
data: {
  status:      "stored",
  stored_at:   <path>,                       ; system/relay/store/{namespace}/{hash}
  entry_hash:  <system/hash>,                ; hash of the stored store-entry
  expires_at:  <timestamp | null>            ; ms since epoch
}

system/relay:poll (Mode S). Poll is a relay-owned operation over the namespace subtree system/relay/store/{namespace}/*; the cursor and entry ordering are the relay's (NOT INBOX's — §6.1):

type: "system/relay/poll-request"
data: {
  namespace:   <path>,
  since:       <cursor | null>,              ; relay-owned cursor; null = from start
  limit:       <uint | null>                 ; null = backend default
}

type: "system/relay/poll-result"
data: {
  entries:     [<system/hash>],              ; hashes of store-entry entities
  cursor:      <opaque>,                      ; pass back as `since` to continue
  has_more:    bool
}

poll returns store-entry hashes (pointers), not inline bytes. The receiver then does two tree:gets on the namespace-scoped relay paths (§3.2): it fetches the store-entry at system/relay/store/{namespace}/{entry_hash_hex}, reads its envelope_inner field (the inner hash), and fetches the inner envelope at system/relay/store/{namespace}/inner/{inner_hash_hex}. Both are content-addressed and preserve content-store dedup.

system/content is NOT a relay receive-side dependency. A receiver consumes Mode-S traffic with tree + relay only. The "content-addressed two-hop discipline (NETWORK §6.5.3.1)" is path→hash→bytes, realized two ways by transport — neither the system/content extension: over a live session a single materializing tree:get (V7 §1.7) on the relay-tree path returns the entity bytes directly; over a static CDN (STORAGE-SUBSTITUTE-HTTP, §6.5) TREE_GET returns the bound system/hash pointer (Amendment 6) and the consumer second-hops CONTENT_GET /content/{hex(H)}. CONTENT_GET (a transport route / core hash-fetch) is distinct from system/content:get (an extension handler op); the inner bytes are reached by tree resolution plus a core hash-fetch, never by installing system/content. This is also why the static-CDN Mode-S deployment — which has no live :poll handler — consumes through the same tree paths: the surface is uniform across live and static Mode S.

Cursor ordering is impl-defined; the contract is resumability, not a specific order. A relay MAY order entries FIFO/insertion-order, lexically by hash, or otherwise — all conformant (cohort: Go uses insertion-order, Python lexical-by-hash; both pass). The only requirement is that passing cursor back as since makes forward progress and eventually drains the namespace without loss or unbounded repetition. Conformance tests MUST assert resumability (cursor round-trips and drains), NOT a FIFO order.

Empty namespace is not an error. A poll against a namespace the caller is authorized to read that currently holds no entries MUST return {entries: [], has_more: false} at status 200 — not namespace_not_found. This is the normal steady state for a freshly-created inbox (e.g. a reconnecting destination polling its own peer-id namespace before anything is queued, §6.2.1). namespace_not_found (§4.3) is reserved for deployments that require explicitly-provisioned namespaces, polled against one that was never provisioned.

An expired entry MUST NOT surface on :poll. A store-entry whose expires_at is at or before the relay's current time is not returned in entries, is not counted toward limit, and does not stop the cursor making forward progress — expiry is honored on read, whether or not the entry has been physically reclaimed (§8). A relay MAY reclaim lazily; a poller MUST NOT be able to observe the difference.

This is what makes expires_at mean anything to the receiver rather than only to the relay's GC: without it, whether a dead entry is visible depends on when a sweep happened to run, which is a cross-peer-observable difference between two conformant relays with identical stored state. The same reasoning as the empty-namespace rule above — the poll result is a statement about live entries, not about storage. expires_at remains a creation-side rejection at :put (expired_on_arrival/400, §4.3); this is the read-side half of the same field.

§4.3 Error taxonomy

All relay ops fail via the standard V7 ERROR shape (status centralized per V7 §3.3; codes domain-scoped). Errors are fail-closed: on any error the op performs no partial effect (no forward, no store, no dequeue) — consistent with the v7.75 substrate-resilience posture (bounded resources, deliver-or-signal, never silently drop).

Reused V7 floor codes (do not reinvent):

StatusCodeMeaningEmitted by
403capability_deniedcaller lacks the §5.2 cap for this op/namespace (V7 §3.3 / v7.71 authz contract)all
413payload_too_largeexceeds advertised max_envelope_size (V7 §4.10(a) floor)forward, put

Relay-owned codes (conditions V7 has no floor for; the relay owns its code domain per V7 §3.3, as DISCOVERY owns discovery_scan_overflow):

StatusCodeMeaningEmitted by
400ttl_exhaustedttl_hops reached 0 on receiptforward
400invalid_requestroute and next_hop both set but next_hop ≠ route[0] (§3.1.1, rejected pre-dispatch)forward
502no_routeno source route, no next_hop, and no route-table match / destination not directly reachable (§3.1.1)forward
429rate_limitedexceeds advertised forward_rate_limitforward
400namespace_invalidmalformed namespace pathput, poll
404namespace_not_foundnamespace not provisioned (deployments requiring explicit provisioning only; empty ≠ not-found, §4.2)poll
507storage_fullaccepting would exceed advertised max_storage_bytes. The relay MUST refuse and MUST NOT evict an accepted entry to make room (§8.2)put
400expired_on_arrivalexpires_at already past at put timeput
400put_by_mismatchstore-entry.put_by ≠ authenticated session/connection peer (§3.2)put
502no_inbox_relaydestination unreachable and declared no inbox-relay; nothing to store-and-forward to (§3.5/§6.2.1)forward

expired_on_arrival is 400, not 410 by design: :put is a creation op, and an already-expired entry is a dead-on-arrival request (a client error in the submitted content) — not a 410 Gone, which would imply a previously-existing resource that is now gone. Nothing was ever stored, so there is no resource to be Gone.


§5 Capability model

Per reviews/ANALYSIS-RELAY-CAPABILITY-CHAIN.md, authoritative.

§5.1 The two orthogonal permission relationships

When Alice → relay Bob → Charlie:

  1. Alice ↔ Charlie — the actual authority. Alice holds a capability that lets her do this operation on Charlie. Roots at Charlie; verified by Charlie on receipt. Bob constructs no link in this chain.
  2. Alice ↔ Bob — the right to use Bob as a relay. Roots at Bob; ordinary handler-permission; orthogonal to (1).

Bob cannot forge Alice's signature; cannot escalate; worst he can do is drop or delay. Confidentiality from Bob is ENCRYPTION's job, orthogonal.

§5.2 The per-mode relay-side caps

Bob enforces a separate ordinary cap per mode op:

system/capability/relay-forward       ; Mode F — may forward envelopes
system/capability/relay-put           ; Mode S — may put entries at a namespace
system/capability/relay-poll          ; Mode S — may poll a namespace
system/capability/relay-advertise     ; All — may publish advertise entity (typically operator-only)
system/capability/relay-subscribe     ; Mode A (deferred) — may open subscriptions

Scoped per V7 §5.4 patterns. Deployments can grant any subset:

§5.3 V7 §5.8 registry placement

RELAY belongs in the V7 §5.8 non-site column. Relay's cross-peer data flow is transport of an already-constructed chain (Alice→Charlie), already covered by V7 §6.5 dispatch. No V7 amendment required. A one-line documentation clarification in V7 §5.8 naming RELAY as a non-site is sufficient (deferrable to V7's next routine errata).

§5.4 Per-mode cap-scope notes

§5.5 Default grants + fallback authority


§6 Composition with other extensions

§6.1 INBOX (EXTENSION-INBOX.md v5.9)

Mode S is inbox-shaped but self-contained. It is structurally a mailbox at a passive store, but it does not delegate to the INBOX operation:

§6.2 CONTINUATION (EXTENSION-CONTINUATION.md v1.20) — async response correlation

Async response correlation is carried inside the opaque inner envelope, not in any relay field. The relay is genuinely unaware of it. The mechanism reuses INBOX deliver_to / deliver_token (V7 §3.2 + INBOX §3.3):

  1. Alice's inner envelope is an EXECUTE carrying deliver_to (a reply URI in Alice's own tree) + deliver_token (a cap authorizing delivery to that inbox path). Alice installs a system/continuation entity at the reply path.
  2. The relay forwards the inner envelope opaquely; Charlie ingests and verifies it directly (Bob invisible to the cap-chain check — §5.1).
  3. Charlie's response is delivered as an authenticated EXECUTE to deliver_to.
  4. At Alice's reply path, INBOX receive finds the continuation entity and delegates to the continuation handler's advance (INBOX §3.3) — that is the resumption. CONTINUATION advances because a result landed at its path; it does not depend on how the result arrived.

The relay does NOT carry per-request state for response correlation. (Legacy v4.0-Rust's PendingRelayRegistry is retired; its replacement is INBOX delivery + a CONTINUATION at the reply path — not a relay-level correlation field.)

§6.2.1 Delivery to a browser / NAT'd destination

NETWORK §14: browser peers are always connection initiators and cannot accept inbound connections. The rule:

Mode F delivery to a browser/NAT'd destination reuses the destination's live outbound session (NETWORK §14 Browser Peer Considerations; the §6.5.1b peer-initiated dispatch model; the §10 held_connection_client reachability class): the relay pushes down the already-open socket the destination holds to the relay. If the destination has no live session, Mode F falls back to Mode S (store at a namespace; destination polls on reconnect), returning forward-result {status: queued-fallback}.

Fallback target (resolved via the inbox-relay declaration, §3.5). The forwarding relay resolves the destination's system/peer/inbox-relay declaration (§3.5) and stores at the highest-priority reachable (relay, namespace). This is the MX-record lookup — the destination published where to leave its mail. If the destination declared no inbox-relay, the relay MAY store at the default convention — namespace = destination peer_id on the current forwarder (system/relay/store/{destination_peer_id}/{hash}) — which works when the forwarder is itself a relay the destination will poll; if neither a declared relay nor a usable default yields a reachable store target, surface no_inbox_relay (502, never a silent drop). On reconnect the destination polls system/relay:poll {namespace: <its own peer_id>} (or the namespace it declared). The store runs under the forwarder's relay-forward authority and the destination polls under its self-poll default grant (§5.5).

Scope limit — namespace is derivable; the holding relay is not. The namespace needs no side-channel (it IS the destination's identity, fixed by convention). But which relay(s) hold a fallback inbox for a destination is not discoverable in v1: a third party forwarded via a relay the destination may not know about. v1 fallback therefore works when the destination knows which relay(s) to poll — a configured home relay, or a relay the sender and destination both already use. General "any relay might have queued for me" discovery is the same family as Mode A / a registry-of-relays and is deferred (§11.1a). Because the holding-relay problem is unsolved in v1, a per-relay namespace override would only add a second unknown (now the destination must also learn each relay's namespace) for no benefit — so the override is dropped from v1. The forward path, when a fallback-relay-discovery driver arrives, is a derivable {configured_prefix}/{destination_peer_id} suffix so the destination still walks N relays at one known suffix (Q5).

§6.2.2 Registering the NETWORK §10.2 dispatch-fallback seam (sender/responder-side escalation)

§6.2.1 covers the forward path once a relay is already carrying the message. It does not cover the originating peer's own dispatch deciding to use store-and-forward when its direct delivery fails — the "deliver to an offline stranger / respond to an offline caller" case. That decision is the NETWORK §10.2 dispatch_fallback seam, and RELAY is its v1 implementation.

When RELAY is installed, it registers dispatch_fallback(peer_id, execute) at NETWORK §10 step 4. The policy:

  1. Resolve the target's system/peer/inbox-relay declaration (§3.5) — via REGISTRY (landed v1.0; the A+MX-in-one-zone resolution) or a configured declaration — verifying its signature against the target's identity before use (§3.5 resolver MUST; same fail-closed verify R8's SIG-1 vector gates). No resolution path ⇒ the seam returns null (→ NETWORK's unchanged terminal; no new infrastructure required).
  2. If this peer holds relay-put authority at the declared (relay, namespace) or shares a relay with the target, Mode-S :put directly.
  3. Else Mode-F forward through a relay this peer can forward through (relay-forward grant) targeting the destination; that relay performs the §6.2.1 MX store under forward authority (§5.5). Prefer rung 3 (Mode-F-through-a-relay) over rung 2 where both are available — it carries the §6.2.1 fallback + §5.5 authority story without the sender needing put rights; fall to direct :put only where the sender already shares the relay.
  4. Target declared no inbox-relay and no forward path exists ⇒ return null (→ NETWORK's 502, surfaced as RELAY's existing no_inbox_relay, §4.3).

v1 limit — which relay to forward through. Rung 3 requires the sender to know a relay that grants it relay-forward: v1 = a configured home relay (the same "holding-relay is not discoverable" limit §6.2.1 already documents). General relay discovery stays deferred (Mode A / registry).

Conformance gates the outcome, not the rung. The MUST is: a RELAY peer's §10 dispatch to an offline target with a resolvable, signature-verified inbox-relay declaration escalates to store-and-forward (the envelope lands at the target's inbox; the target polls and verifies the sender signature byte-identical to a direct delivery, §9). The rung-2-vs-rung-3 choice is policy. No new capability, no new error code. The forcing function is the cross-impl INBOX-RELAY-FALLBACK-1 flow (Go build-tested GREEN; cohort mirrors at impl time). Per NETWORK Amendment 11 / proposals/implemented/PROPOSAL-DISPATCH-FALLBACK-SEAM-SENDER-SIDE-STORE-AND-FORWARD.md.

§6.3 NETWORK (EXTENSION-NETWORK.md Amdt 10)

Active modes use NETWORK's transport primitives. NETWORK §6.5 transport profiles apply directly; NETWORK §10 outbound dispatch resolves the target's reachability. Mode S backed by a static CDN uses BRIDGE-HTTP for put/get translation; the NETWORK §6.5 http-poll transport profile (§6.5.3) specifies the wire shape, and the two-hop pointer discipline is NETWORK §6.5.3.1 (Amendment 6, normative). Mode S backed by a live peer uses NETWORK directly (no bridge).

§6.4 REGISTRY (EXTENSION-REGISTRY.md v1.0)

Registry-peer-as-Mode-S-publisher. A registry peer publishes its binding entities at system/registry/binding/{binding_hash} (REGISTRY §3); consumers fetch via Mode S (static-CDN-hosted or live). This is REGISTRY §8.1 verbatim — no special registry transport.

Aggregator-as-meta-registry. A peer running Mode A subscribed to N registry peers' binding subtrees serves the combined view. A consumer MAY add the aggregator peer as one backend entry in its own (peer-local) resolver chain — resolver-config is peer-local and not synced (REGISTRY §4), so the aggregator is consumed, it does not install itself into another peer's config.

v1 deferral. Depends on RELAY Mode A, deferred from v1 (§11.1a). REGISTRY §8.2 independently marks it v1-deferred behind Mode A. Cross-registry federation lands when Mode A lands. See EXTENSION-REGISTRY.md §8.2.

§6.5 STORAGE-SUBSTITUTE-HTTP (PROPOSAL-EXTENSION-STORAGE-SUBSTITUTE-HTTP.md, in-flight)

STORAGE-SUBSTITUTE-HTTP IS Mode S relay for content. From the CONTENT side: local store misses → consult substitute sources, one being a static-CDN-hosted peer. From the RELAY side: that peer is a Mode S relay holding content at a namespace. Same wire shape; same trust discipline. v1 RELAY documents it as the first deployed Mode S; no re-specification.

Mechanism A, not BRIDGE-HTTP (the recurring conflation). The static-CDN Mode-S surface is HTTP-as-storage-transport (NETWORK §6.5 http-poll: the bytes on the wire ARE entity bytes, hash-verified) — the read side already exists in the http-poll serving routes (TREE_GET/CONTENT_GET, §6.5.3.1). It is NOT BRIDGE-HTTP, which is Mechanism B (fetch foreign HTTP content — HTML/JSON — and wrap it as system/bridge/http/fetched; not on the v1 critical path). A truly-static CDN is read-only over the protocol: the publisher writes to the static origin out-of-band (deployment action), and a receiver polls + fetches over http-poll — so the read flow is exercisable today with no proposal landed. A protocol-mediated write to an HTTP origin (if wanted) is STORAGE-SUBSTITUTE-HTTP's surface (in-flight), still not BRIDGE-HTTP.

§6.6 ENCRYPTION (PROPOSAL-EXTENSION-ENCRYPTION.md, in-flight)

Confidentiality from the relay is ENCRYPTION's job. The relay carries an opaque content-addressed inner-envelope ref; ENCRYPTION wraps the inner content so the relay (and any observer of its storage) sees only ciphertext. Orthogonal to transport mechanics.

§6.7 DISCOVERY + REGISTRY relay-finding (EXTENSION-DISCOVERY.md v1.0, EXTENSION-REGISTRY.md v1.0)

RELAY's advertise entity describes a relay's capabilities; it does not solve finding the relay. Relay-finding is delegated:

The clean-separation boundary holds: REGISTRY = name→peer, DISCOVERY = find unknown peers, RELAY = transport between found peers. RELAY assumes the relay peer is already reachable by one of these means.

§6.8 ROUTE (EXTENSION-ROUTE.md v1) — the routing plane

RELAY is the forwarding plane (move the envelope one hop); ROUTE is the routing plane (the stored table answering "to reach D, who's next?"). When a forward-request arrives with no source route, RELAY reads the local system/route table and applies ROUTE §3's documented match (exact-or-* default, lowest metric, expiry-skip) to pick the next hop (§3.1.1 source 3). RELAY consumes the table; it never computes or populates routes — production (manual config / DISCOVERY-learned / GOSSIP-learned) is the peer's job (ROUTE §4). The per-hop relay-forward cap still gates each routed hop (a forward route to a relay that won't grant it fails capability_denied/403; ROUTE §5). ROUTE is optional: RELAY works without it (single-hop + source route), and gains table routing when a table is present.


§7 Mode S signed mutable pointer (tracked blocker)

For Mode S to serve "current state of publisher's tree" use cases, a signed mutable pointer at a named location (the IPNS-equivalent) is required. This lives in CONTENT, not RELAY — it's a "current head of tree" question at the content layer (reviews/DESIGN-STATIC-TRANSPORT-AS-RELAY.md §3; likely a small CONTENT amendment).

Tracked blocker. Mode S content-addressed fetch-by-hash works fully in v1. The "give me the publisher's latest" (no hash specified) cases are gated on the CONTENT signed-mutable-pointer work and are not deliverable in RELAY v1. The cohort should not assume the latest-pointer case ships with Mode S v1.


§8 GC posture (per GUIDE-GC.md)

§8.1 The retention ceiling — declared, published, and clamping [MUST when present, v1.3]

A Mode S relay MAY enforce a maximum lifetime for stored entries. Where it does:

This is the shape EXTENSION-REGISTRY §6a.9.1 already ruled for ttl, with retention substituted: mandate that the bound exists, is declared, and is enforced; leave the value to the deployment. It is transplanted rather than redesigned.

§8.2 A full store refuses; it does not evict [MUST, v1.3]

When accepting a :put would exceed the relay's advertised limits.max_storage_bytes, the relay MUST refuse the new entry with storage_full (507, §4.3) and MUST NOT evict an already-accepted entry to make room.

The two behaviours are cross-peer observable and differently honest. A 507 tells the sender its message was not taken, and the sender can try another relay from the destination's §3.5 declaration. A silent eviction discards a message the sender was already told was stored — the same class of failure §4.3's deliver-or-signal, never silently drop posture exists to prevent, arriving after the operation returned.

What max_storage_bytes counts (normative, v1.4). The bound is on what the relay persists, summed relay-wide. A u64 declares the type; it does not declare the unit, and a bound whose metric is inferred is a bound two conformant relays enforce at different points.

Consequence for the §8 storage-full check. Because the count is taken in the relay's own stored encoding, two conformant relays MAY differ by a small constant for the same logical content. A conformance check therefore fills past the advertised bound by a margin and asserts the refusal; it MUST NOT assert the exact byte at which refusal begins, and MUST NOT be authored against one implementation's constant.

§8.1's ceiling is what makes this safe, and neither rule is landable alone. Refuse-without-evict on its own is a denial of service: one putter fills the store with null-expiry entries, nothing may ever be evicted, and the relay refuses everyone forever. Time bounds the store (§8.1), refusal bounds the burst (§8.2), and no eviction policy is needed as a spec item. Per-namespace eviction remains an operator affordance and is not a conformance surface.

The same question was ruled at the other end of the pipe: EXTENSION-NETWORK §8.4's sender-side pending-delivery queue is reject-new, not evict-old. The relay's inbound store asks the identical question and inherits the same answer.

Knobs exposed; operators pick policy. Where a knob bounds behaviour a counterparty depends on, it is published in §4.1 — a bound nobody can read is a bound nobody can plan against.


§9 Envelope opacity (the central transport invariant)

There are two envelopes; only one is decoded.


§10 Cross-impl conformance

§10.1 MUST implement

A conformant RELAY v1 implementation MUST implement both Mode F (forward) and Mode S (put/poll) — both are part of the v1 floor and both MUST be testable. A deployment MAY advertise and enable any subset of modes (a static-CDN relay enables only Mode S; a forwarding relay enables only Mode F). Conformance is gated on the implementation's mode support; mode enablement is deployment policy.

Conformance class for the v1.3 store-bound rows, per GUIDE-CONFORMANCE §7.0 and §5.2b.1. All four are validate-peer behavioral checks in the relay category — oracle-authored, driven over the wire against a running peer — not fixture-corpus vectors: none of them produces a canonical-byte artifact, and what is under test is a peer's behaviour at an operation boundary.

Satisfaction mode, stated at the point of the MUST because two of these are not reachable against a default peer. The clamp rows (§8.1) are constructible by any conformance client: put a store-entry whose expires_at exceeds the advertised ceiling, poll it back, compare. The refusal row (§8.2) is not — it requires a peer configured with a max_storage_bytes small enough to reach, which is an operator knob and not a wire input. The harness therefore needs a peer under test started with a declared storage bound, and that requirement is part of the check rather than an assumption about the deployment. A run that cannot reach the bound reports could-not-look, never a pass.

§10.2 SHOULD implement

§10.3 MAY implement

§10.4 MUST NOT


§11 What this extension does NOT cover (v1 scope cuts)

§11.1 Mode C (circuit relay) — deferred

Circuit relay for NAT traversal is structurally distinct (bidirectional virtual circuit; reservation discipline; bandwidth accounting). Defer to a follow-on proposal when a driver materializes; the static-host Mode S pattern covers most NAT cases via Mode S inbox addressing (§6.2.1). Entity types named for forward-compatibility (§3.4); operations named (§4); spec text deferred.

§11.1a Mode A (aggregate) — deferred from v1

RELAY v1 ships Mode F + Mode S only. Mode A's "aggregator subscribes to N publisher peers' subtrees" requires cross-peer subscription initiation that does not exist in current substrate — subscription engines (the landed EXTENSION-SUBSCRIPTION and the cohort impls) are local-tree-only; cross-peer subscription is new choreography layered above. Mode A is the federation answer (Nostr / ATProto) and the substrate for aggregator-as-meta-registry (§6.4); both depend on a cross-peer subscription mechanism that deserves its own proposal. RELAY v1 explicitly does NOT deliver registry federation. REGISTRY §8.2 inherits this deferral.

Transport-fallback dependency. Mode F's "forward to next_hop" is itself a transport call; if it fails, RELAY needs the fallback loop the transport-composition exploration will specify (try alternate transports / re-resolve via registry / give up cleanly). Mode F's routing-on-failure is gated on EXPLORATION-TRANSPORT-COMPOSITION-AND-FALLBACK.md (Phase 2). v1 Mode F ships with explicit next_hop required plus the Mode-S fallback (§6.2.1) until that lands.

§11.2–§11.6 Other cuts


§12 Configurations


§13 Open questions (informative)


§14 Cross-references