Przejdź do treści

Proposal 035: Agora — Topic-Addressed Record Relay and Shared Record Substrate

Based on:

  • doc/project/40-proposals/013-whisper-social-signal-exchange.md
  • doc/project/40-proposals/021-service-offers-orders-and-procurement-bridge.md
  • doc/project/40-proposals/023-federated-offer-distribution-and-catalog-listener.md
  • doc/project/40-proposals/024-capability-passports-and-network-ledger-delegation.md
  • doc/project/40-proposals/025-seed-directory-as-capability-catalog.md
  • doc/project/40-proposals/026-resource-opinions-and-discussion-surfaces.md
  • doc/project/40-proposals/032-key-delegation-passports.md
  • doc/normative/20-vision/VISION.md (Memarium, Swarm)
  • doc/project/60-solutions/SOLUTIONS.md
  • orbidocs/AGENTS.md (Node-attached roles)

Status

Accepted

Date

2026-04-11

Executive Summary

Orbiplex today has several subsystems that each maintain their own append-only, signed, topic-shaped state:

  • the seed directory replicates capability passports and service offers,
  • the offer catalog holds federated offer listings,
  • resource-opinion.v1 from proposal 026 authors opinions about external resources with no shared substrate to store or retrieve them,
  • and whisper-signal.v1 from proposal 013 dispatches privacy-bounded social rumors without any durable query surface.

Every one of these subsystems invents its own topic, its own relay, its own replication rhythm, and its own query API. The consequence is that adding one more kind of shared record — a comment under a URL, a note under a product, a public log entry under a workflow run — means designing one more bespoke store.

This proposal introduces one neutral substrate:

  • Agora — a topic-addressed, append-only, signed record relay, operated by umbrella operators and consumable by any Node.

And one neutral envelope:

  • agora-record.v1

The key decisions are:

  1. a record is a signed, timestamped, content-addressed artifact authored by a participant and placed under one topic,
  2. a topic is addressed by an opaque topic/key string; the substrate does not interpret, type, or split it, and no single use case owns the shape of topic keys,
  3. the record envelope is extensible by record/kind and content/schema, so that opinions, comments, public logs, offer snapshots, or swarm gossip can all live side by side under one transport contract,
  4. when a record is meaningfully about an external subject (a URL, a product, a node, an org), it MAY carry an optional record/about list that references the resource identity model from proposal 026; this relation is orthogonal to topic placement and is not used as the primary key,
  5. Agora relays are umbrella-operated and federated, following the same operational pattern as the seed directory (proposal 025) and the offer distribution layer (proposal 023),
  6. Matrix (via a dedicated homeserver deployment) is the reference backend for the first implementation, but the agora-record.v1 envelope is backend-neutral and a later native transport may replace or complement it.

Agora is orthogonal to Memarium. Memarium, as described in the vision document, is the curated memory-and-knowledge layer that decides what must not disappear. Agora is a lower primitive: a durable, citeable log of public records indexed by topic. A community-scale Memarium space may use Agora as one of its underlying substrates, but it is not the only one and Agora does not depend on Memarium being present.

Context and Problem Statement

Several existing proposals implicitly demand a public, topic-addressed, append-only substrate:

  • Proposal 013 (Whisper) defines a privacy-bounded rumor flow. Rumors are ephemeral by design and therefore do not need Agora, but the interest semantics and the threshold-reached events can cleanly degrade into durable records once a threshold is crossed.
  • Proposal 023 (federated offer distribution) is a domain-specific append-only replication layer. It works, but it reinvents parts of what Agora would provide once, centrally.
  • Proposal 025 (seed directory as capability catalog) is a similar replication layer with a different payload shape and a different query model.
  • Proposal 026 (resource opinions) defines the opinion artifact and the generic resource identity model, but explicitly leaves the storage and discussion surface as a later attached layer.

Three separate systems, three separate relay mechanisms, three separate caches, three separate federation rhythms, three separate retention rules. The cost of adding the fourth one — comments on proposal pages, public notes under workflow runs, shared annotations under a code artifact — is disproportionately high.

The missing contract is not a schema. The missing contract is one neutral substrate that any future topic-shaped record can sit on without rewriting the transport.

What is absent today

Need Current state
One portable record envelope usable for many record kinds Not present
A generic, backend-neutral topic addressing model shared across subsystems Not present; each subsystem invents its own (proposal 026's resource identity addresses subjects, not topics, and is orthogonal to this question)
One umbrella-operated relay role for topic replication Per-subsystem ad hoc
Subscription and replay under one topic Per-subsystem ad hoc
A stable place for comments, public annotations, long-lived notes Missing
A substrate for the community layer of Memarium Missing

Goals

  • Define one neutral, signed, content-addressed record envelope (agora-record.v1) suitable for many record kinds.
  • Address topics by an opaque topic/key string, so that the substrate never has to interpret, type, parse, or split topic identifiers, and no single use case dictates their shape.
  • Keep resource identity (proposal 026) as an orthogonal axis, reachable through an optional record/about field, so that the two questions where is this record? and what is this record about? remain cleanly separated.
  • Define the role of an Agora topic relay and its federation responsibilities.
  • Define one minimal query and subscription API that any Node can call.
  • Pick a reference backend (Matrix homeserver) but keep the envelope backend- neutral.
  • Leave room for migrating existing subsystems (offer catalog, opinion storage) onto Agora without breaking them.

Non-Goals

  • This proposal does not define how Memarium curates, indexes, or retrieves records semantically. Memarium remains a separate primitive.
  • This proposal does not define E2E-encrypted private topics. Agora records are public-by-default at the relay level; private channels are a later proposal.
  • This proposal does not define moderation, sanctions, or content policy. Those attach to record kinds, not to the substrate.
  • This proposal does not mandate Matrix. Matrix is picked as the reference backend to avoid inventing a new transport before the envelope is proven.
  • This proposal does not migrate existing offer catalog or seed directory storage onto Agora. That migration is a later decision, discussed under Consequences.

Decision

1. Record Identity and Addressing

Every Agora record MUST be addressable by the pair:

  • topic/key — an opaque string identifying the topic under which the record lives,
  • record/id — a content-addressed identifier of the record itself.

1.1. Topic Key

topic/key is an opaque UTF-8 string. The substrate MUST NOT:

  • parse topic/key for internal structure,
  • split it on any delimiter,
  • attach type semantics to any prefix or suffix,
  • assume URI, URN, hierarchical, or resource-identity semantics.

The substrate MUST enforce only the following canonicalization rules:

  • Unicode NFC normalization,
  • printable characters only; control characters (C0, C1, DEL) are rejected,
  • leading and trailing whitespace is rejected (not trimmed),
  • maximum length of 512 bytes after UTF-8 encoding,
  • empty topic/key is rejected.

Applications choose their own topic-key conventions. Common patterns include:

  • namespace-prefixed path-like strings, for example ai.orbiplex.proposals/035, mycompany/build-log,
  • random or content-derived identifiers, for example 01JRCY0Y7T4Y9JQK8K7R6K4M3M or sha256:4b7c...,
  • human-readable channel names scoped by operator, for example example-coop/announcements.

Applications that want topics derived from resource identity (proposal 026) MAY construct topic keys that embed the resource identity as a substring (for example ai.orbiplex.opinions/url/sha256:4b7c...), but this is a convention of the record kind contract, not a rule of the substrate.

Two applications that pick the same topic/key will share a topic. The substrate does not enforce global namespacing; applications SHOULD use operator- or project-scoped prefixes to avoid collisions. Kind contracts MAY require stricter prefixes.

The recommended namespace scheme — six named prefix classes (ai.orbiplex.<subsystem>/<name>, participant:<pid>/…, node:<node-id>/…, federation:<fid>/…, local/…, private/…) plus the rules that turn those prefixes into cryptographic ownership at the relay ingress — is defined in proposal 046 (Agora Topic-Key Namespace Conventions). That proposal is non-normative for the substrate: topic/key remains opaque here; all enforcement lives in relay policy and author-side hygiene. Deployments that adopt 046 run the one-compare core discriminator (starts_with("ai.orbiplex.")) and the five-check ingest policy before any 041 attestation tiering.

1.2. Record Identifier

record/id is a content-addressed identifier computed as:

  • record/id = "sha256:" + base64url-no-pad(sha256(canonical-json(record-without-id-signature-and-relay-fields)))

The relay fields excluded from this payload are relay/received-at, relay/id, and relay/hops.

This matches the Orbiplex canonical hash convention already used by node/capability/src/lib.rs::sha256_base64url for signed artifacts and passport hashes, so that a single content-address prefix covers both the capability layer and the record layer.

Content addressing is necessary so that citations across relays, mirrors, and cached views reliably resolve to the same record regardless of which relay served it.

1.3. Optional Subject Reference

A record MAY carry an optional record/about field that links the record to one or more external subjects using the resource identity model from proposal 026:

"record/about": [
  { "resource/kind": "url", "resource/id": "https://example.org/article" },
  { "resource/kind": "ean", "resource/id": "5901234123457" }
]

Rules:

  • record/about is OPTIONAL and MAY be empty or absent.
  • When present, each entry MUST follow the resource identity rules from proposal 026.
  • record/about is not a topic key and MUST NOT be used as the primary index by the substrate. It is a secondary, optional index that enables cross-topic queries of the form show me every record about this subject, regardless of where it was posted.
  • A kind contract MAY require record/about to be present for records of a given record/kind (for example opinion records MAY be required to carry exactly one record/about entry). The substrate itself does not enforce such rules.

2. agora-record.v1 Envelope

The MVP envelope is:

  • schema = "agora-record.v1"

Required fields:

  • schema
  • record/id
  • record/kind
  • topic/key
  • author/participant-id
  • authored/at
  • content/schema
  • content
  • signature

Optional fields:

  • record/about — list of { resource/kind, resource/id } entries naming external subjects this record refers to (see section 1.3)
  • record/parent — one parent record/id when the record is a reply, annotation, or successor
  • record/supersedes — one prior record/id replaced by this revision
  • record/policy — one policy record reference. For comment threads this points to a thread-policy record whose content/schema is comment-thread-policy.v1.
  • record/tags — short free-form tags (application use)
  • record/lang — BCP 47 language tag
  • author/nym-proof — when author/participant-id carries a nym:did:key:... pseudonymous identity (see invariant 9 below), this optional field carries a nym-authorship-proof.v1; in the M4 baseline the proof inlines enough nym-certificate.v1 material to verify the current nym without remote lookup, while optional refs may point to longer lineage, audit, or reputation history. The proof is part of the signed canonical bytes so the binding cannot be stripped in transit.
  • relay/received-at — stamped by the relay on ingest; never part of the signed payload
  • relay/id — identifier of the relay that first ingested the record
  • relay/hops — relay-local hop count stamped by relay implementations; never part of the signed payload

Ingest invariants:

  1. topic/key MUST satisfy the canonicalization rules from section 1.1.
  2. record/id MUST equal the content hash of the canonical payload with record/id, signature, relay/received-at, relay/id, and relay/hops omitted.
  3. signature MUST be a valid Ed25519 signature by the envelope author subject key named by author/participant-id. For ordinary records this is the participant key and is verifiable against the participant's current capability passport chain (proposals 024 and 032). For records whose kind permits pseudonymous authorship, invariant 10 defines the nym-key path.
  4. content/schema MUST be present and MUST name a known content schema identifier; relays MUST accept unknown schemas but MAY mark them as non-indexable until a schema contract is registered.
  5. authored/at MUST be within the relay's configured clock skew window (default: ±10 minutes) relative to relay wall clock, or the record is flagged clock_skew and still stored but excluded from live query results until the window passes.
  6. record/parent and record/supersedes, when present, MUST resolve to known records under the same topic/key or be flagged dangling until the parent appears.
  7. author/participant-id MUST carry a currently valid author subject: normally participant:did:key:..., or nym:did:key:... only when the record kind explicitly permits pseudonymous authorship. Sanctioned or revoked author subjects are still stored but excluded from default query output.
  8. record/about, when present, MUST follow the resource identity rules of proposal 026. The substrate MUST NOT derive topic/key from record/about and MUST NOT require record/about to match the topic.
  9. record/policy, when present, MUST resolve to a known policy record under the same topic/key or be flagged dangling until the policy appears. The substrate validates only the reference shape. Kind-specific policy evaluators decide whether a missing or stale policy blocks publication.
  10. Pseudonymous authorship. author/participant-id MAY carry a nym:did:key:... identity in addition to a participant:did:key:... identity, when the record kind permits pseudonymous authorship (currently: whisper, see §3). In that case:

  11. the envelope signature MUST be Ed25519 by the private key of the nym, not of any underlying participant;

  12. the envelope MUST carry author/nym-proof containing a currently valid nym-authorship-proof.v1; in the M4 baseline this proof inlines a nym-certificate.v1 (proposal 015) that binds the nym to its issuing council's authorization scope and can be verified without a network lookup;
  13. the ingest-attestation gate (proposal 041) MUST accept the nym certificate in place of a participant capability passport for ingest admission, under the consumer-local ingest mode configured for the topic;
  14. subject sanctions and revocations attach to the nym identity for the nym's bounded validity window; no attempt is made to retroactively correlate nym records with the underlying participant on the substrate layer.

3. Record Kinds and Content Schema Extension

record/kind names the what of the record. content/schema names the shape of the payload inside content.

The envelope is generic: it does not prescribe the list of kinds, only the registration rule. Each record kind is defined by a separate, small schema contract that lives next to the subsystem that owns it. Examples for MVP:

record/kind content/schema Owner
opinion resource-opinion.v1 proposal 026
comment plain-comment.v1 Agora base kind
thread-policy comment-thread-policy.v1 Agora base kind
gossip public-gossip.v1 Agora base kind
annotation plain-annotation.v1 Agora base kind
whisper whisper-signal.v1 proposal 013
whisper-durable whisper-threshold-record.v1 proposal 013 follow-up
public-log public-log-entry.v1 application use

Rules:

  • An unknown record/kind MUST be stored and served, but MUST be treated as opaque by the relay query layer until a schema contract is registered.
  • A schema contract MUST be a separate proposal or schema file; it MUST NOT be merged into this proposal.
  • Applications MUST NOT assume that all relays understand all kinds.

4. Topic Relay Role

An Agora topic relay is an umbrella-operated Node-attached service whose responsibilities are:

  1. Ingest — accept signed agora-record.v1 records from authorized participants, verify them, content-address them, and write them to durable storage under their topic/key.
  2. Federate — replicate records across peer relays according to a configured federation policy; relays do not have to replicate every topic, only the topics they carry.
  3. Serve — expose query and subscription APIs for Nodes.
  4. Retain — enforce per-topic retention rules (time, count, size).
  5. Exclude — honor participant sanctions and record-level flags by excluding records from default query output without deleting them.

Three operational modes are defined:

  • Canonical relay — operated by an umbrella operator, publishes a stable base URL, holds the durable source of truth for a set of topics.
  • Cache relay — operated by a Node, mirrors a subset of topics for local availability and offline operation. A cache relay does not have to be reachable by other relays.
  • Origin relay — operated by the author's Node, used to sign and first- ingest the record before it reaches a canonical relay. An origin relay is ephemeral and MAY be the same process as a cache relay.

The deployment pattern is analogous to the seed directory (proposal 025) and the federated offer catalog (proposal 023): each umbrella operator runs one or more canonical relays, Nodes run cache relays, and federation happens between canonicals.

5. Query and Subscription API

Minimum Node-visible API contract. Every path parameter that carries a topic/key MUST be percent-encoded by the caller; the substrate MUST NOT interpret the decoded value beyond the canonicalization rules from section 1.1.

5.1. List records under a topic

GET /v1/agora/topics/{topic-key}/records
  ?since={cursor}
  &limit={n}
  &kind={record-kind}
  &include_flagged={bool}

Response:

{
  "records": [ { ...agora-record.v1... }, ... ],
  "next_cursor": "opaque-next-cursor",
  "cursor_pruned": {
    "requested_from": 5,
    "first_available": 10
  },
  "query_attestation": { "...agora-query-attestation.v1...": "..." }
}

Cursors MUST be opaque. Relays MAY reorder within a given time slice but MUST be stable for a given cursor.

5.1a. Query attestations

Historical query responses SHOULD include an agora-query-attestation.v1 object under query_attestation.

The attestation binds:

  • query mode (topic-records or subject-records),
  • topic key or subject resource,
  • normalized filter after URL decoding and limit defaults,
  • returned record/id values in page order,
  • next_cursor,
  • cursor-pruning notice, if any,
  • deterministic response digest.

The digest is sha256: over canonical JSON using the Agora NFC profile (jcs-nfc-sha256-base64url). It covers the query scope, normalized filter, returned record ids, and pagination/pruning metadata. It intentionally does not cover local HTTP headers or transport details.

An attestation MAY be unsigned in early local relays. In that case it is still useful as a deterministic page proof for local audit and replay comparison, but it does not prove relay accountability. Signed deployments SHOULD sign the canonical JSON of agora-query-attestation.v1 with signature omitted.

Subject-index queries MUST attach the attestation after subscribe-policy filtering. Otherwise the proof would describe the unfiltered subject index instead of the response actually returned to the caller.

5.2. Fetch one record by content id

GET /v1/agora/records/{record-id}

Returns the stored record or 404. This endpoint MUST return the same record regardless of which relay serves it, for a given record/id.

5.3. Subscribe to live updates

POST /v1/agora/topics/{topic-key}/subscribe

Opens a streaming connection (long-poll or server-sent events in MVP; WebSocket post-MVP) that delivers new records under the topic. The subscription is advisory and MAY be dropped by the relay under load.

5.4. Submit a record

POST /v1/agora/topics/{topic-key}/records

Body: agora-record.v1 signed by the author. Relay verifies invariants and returns the canonical stored record (with relay/received-at, relay/id, and relay/hops) or an error.

5.5. Cross-topic query by subject

GET /v1/agora/about/{resource-kind}/{resource-id}/records
  ?since={cursor}
  &limit={n}
  &kind={record-kind}
  &include_flagged={bool}

Returns records whose record/about list contains an entry matching the given resource identity, regardless of topic/key. This endpoint answers questions like what records across all topics refer to this URL / this product / this node?. It is a secondary index and MAY be served more slowly than topic-keyed queries; relays MAY limit its result window.

This endpoint MUST NOT be used as a substitute for topic-keyed queries. Authors who want their records to be findable under a subject SHOULD populate record/about explicitly; the substrate does not synthesize record/about from topic keys.

5.6. Local deployment channels and authentication

When Agora is attached to a Node as a supervised local service, it SHOULD expose the Agora HTTP API on its own port rather than through the daemon's request-response middleware proxy. SSE subscriptions are long-lived streams, and a dedicated port lets the operator decide whether the relay API remains on loopback or is exposed on a public interface.

The Node-attached deployment has three distinct communication relationships:

Channel Direction Purpose Token boundary
Lifecycle daemon -> Agora readiness, initialization, shutdown daemon-generated middleware authtok, validated by Agora
Host capability API Agora -> daemon node identity, capability passport lookup/publication, future policy checks daemon-generated host-capability authtok, validated by daemon
Agora API UI/CLI/client -> Agora ingest, query, fetch, subscribe open in MVP; a separate client authtok MAY be enabled post-MVP

The Agora middleware SHOULD declare its own operator-facing UI surface through the middleware module report. The host UI may mount that surface under its local middleware namespace, but the Agora component owns the Agora-specific views and operator vocabulary. A Node-attached Agora service SHOULD expose a local operator_visibility status read model covering relay endpoint, namespace policy, topic ACL, authority-root summary, strict gates, Matrix posture, HTTP/SSE limits, rate-limit persistence, retention summary, passport state, and client-auth posture. Full authority-root lists should be shown only on a client-authenticated local operator surface; unauthenticated status responses should expose counts and redaction markers instead. This read model is diagnostic only; it is not a federation protocol and does not authorize publish or subscribe operations.

The host capability API is a local, authenticated, module-initiated channel. Agora MUST NOT implement node identity, Seed Directory publication, or capability-passport issuance itself. It asks the daemon for those host-owned capabilities, and the daemon remains the broker for identity and passport policy. In concrete Node deployments this is represented by environment variables equivalent to:

ORBIPLEX_HOST_CAPABILITY_BASE_URL
ORBIPLEX_HOST_CAPABILITY_AUTH_HEADER
ORBIPLEX_HOST_CAPABILITY_AUTHTOK_FILE

If client authentication is enabled for the Agora API, it MUST use a token separate from both lifecycle and host-capability tokens. Absence of such a token means open access for development and local-only MVP operation.

5.6.1. Agora CLI TLS trust configuration

Local CLI clients MAY connect to Agora over either http:// or https:// base URLs. The CLI TLS trust contract is deliberately small:

  • the default HTTPS mode uses the built-in WebPKI root set and rejects self-signed or otherwise untrusted certificates;
  • --tls-ca-file PATH adds one or more PEM-encoded CA certificates to the trust store; ORBIPLEX_AGORA_TLS_CA_FILE provides the same value from the environment, and the explicit CLI flag takes precedence;
  • --tls-insecure-skip-verify is a diagnostic-only bypass for local debugging. It MUST print an operator-visible warning and MUST NOT be used in production.

The custom CA mode is for local labs, private CA deployments, and small operator-controlled federations. It is not a substitute for explicit relay identity policy. Certificate or SPKI pinning remains a future extension for public relay deployments that need tighter endpoint continuity than CA trust alone.

5.7. Capability passport semantics for agora.relay

The middleware capability report and the capability passport are separate facts:

  • the module report declares capability: "this supervised process can serve agora.relay";
  • the capability passport declares authorization: "the operator officially offers agora.relay from this node under this issuer, scope, and metadata".

Passport issuance MUST NOT be automatic merely because Agora reports agora.relay. The operator issues the passport through the same host-owned identity and capability flow used by other passport-backed capabilities. This allows the passport to be signed by a high-assurance participant or delegated operator key rather than by an ephemeral module identity.

Operationally:

  • Agora without an agora.relay passport remains a valid local relay: UI, CLI, and local Python clients may still ingest and query records.
  • Agora with an agora.relay passport becomes discoverable through the Seed Directory and eligible for federation according to local policy.
  • At startup, a Node-attached Agora service SHOULD query the daemon for its current agora.relay passport through the host capability API and expose the result in its status surface.

The agora.relay passport metadata schema is defined in doc/project/60-solutions/008-agora/agora-relay-capability.v1.md. It includes at least the relay API endpoint, relay role, relay domain, supported transport set, API version, and advertised canonical topic set.

6. Reference Backend: Matrix

The first reference implementation of the topic relay role MUST use a Matrix homeserver (Dendrite or Conduit are preferred for footprint reasons) as the underlying durable substrate. Rationale:

  • Matrix rooms map cleanly to Agora topics (one room per topic/key).
  • Matrix events are already signed, content-addressed, and ordered in a DAG, which matches the agora-record.v1 invariants.
  • Matrix federation provides cross-relay replication without Orbiplex having to invent transport.
  • Matrix is already cited as prior inspiration for Orbiplex's messaging strata.

Mapping rules:

  1. A topic MUST be represented as a Matrix room. The room alias MUST be derived deterministically from the opaque topic/key using a fixed transformation, so that the same topic resolves to joinable rooms on every relay that carries it. The MVP derivation is:
alias = "#agora." + lowercase-hex(sha256(topic/key)) + ":" + relay-domain

The hash is computed over the UTF-8 bytes of the already-canonicalized topic/key. Lowercase hex is used here — rather than the canonical Orbiplex sha256_base64url convention used for record/id — because the Matrix room-alias localpart is case-insensitive and restricted to [a-z0-9._=/+-]; base64url would introduce uppercase letters that Matrix would silently fold, breaking deterministic resolution. The choice is a Matrix-compatibility concession, not a second content address: the authoritative record identifier remains record/id. Relays MUST NOT derive the alias from parsed or typed parts of the topic key; the key is treated as an opaque byte string by the derivation function. 2. The relay MUST maintain a separate mapping from topic/key to human-readable Matrix room names for operator tooling. This mapping is advisory; it MUST NOT be used as the canonical identifier of the topic. 3. An agora-record.v1 MUST be serialized into a Matrix event of type org.orbiplex.agora.record.v1 whose content field carries the full record envelope including topic/key and, when present, record/about. 4. record/id is computed from the Agora canonical payload, not from the Matrix event_id. Consumers MUST index by record/id for citation. 5. The Matrix homeserver's own event signature provides relay-level authority; the Orbiplex signature field provides author-level authority. Both MUST be verified. 6. Matrix E2E encryption MUST be disabled on Agora rooms in MVP; Agora records are public by default. 7. The cross-topic record/about index from section 5.5 is maintained by the relay outside the Matrix room model, as a secondary index over ingested events. Matrix itself is not asked to answer subject-based queries.

The envelope is backend-neutral. A later native Orbiplex transport may replace Matrix for topics that must not leave the Orbiplex federation, but such a transport is out of scope for this proposal.

6.1. Federation trust boundary

Matrix federation is a transport and replay channel, not an admission oracle. A receiving Agora relay MUST NOT trust a donor relay merely because the record arrived through Matrix or because the Matrix event itself is valid. The receiving relay MUST treat the Matrix event as a carrier for an agora-record.v1 envelope and run the same local admission pipeline it would run for any other inbound record.

The receiver therefore verifies, at minimum:

  1. the agora-record.v1 envelope shape and canonical record/id,
  2. the envelope signature, author key, nym certificate, or delegated/proxy proof, as applicable,
  3. content-schema conformance for content/schema,
  4. topic ACL and relay-role policy,
  5. authority, capability, revocation, and custody policy required by the topic or record kind,
  6. idempotent deduplication by record/id.

The Matrix homeserver signature and Matrix event id are transport provenance and diagnostics. They may support relay health, abuse analysis, or federation trust scoring, but they are not sufficient proof that the embedded Agora record is valid or locally admissible.

Operationally:

relay is transport
envelope is evidence
local policy is admission

This allows two Agora servers to synchronize accepted records without requiring full mutual trust between relay operators. A malicious or buggy donor may deliver bytes, but the receiver admits only records whose envelope and local policy pass.

7. Retention

Retention is configured per topic and per record kind, with relay-level defaults. Minimum retention policy dimensions:

  • max_age — oldest allowed record age,
  • max_count — maximum number of records kept per topic,
  • max_bytes — maximum total size per topic,
  • pin_by_kind — kinds that MUST NOT be evicted regardless of age or count.

The relay MUST expose the active retention policy for every topic via GET /v1/agora/topics/{topic-key}/retention. Authors MAY submit records that exceed policy; the relay MAY reject, truncate, or accept-and-evict depending on configuration, but MUST return a clear error in the reject case.

Retention does not imply deletion from origin relays. Content-addressed records may persist in cache relays or in participant-local archives even after a canonical relay evicts them.

8. Sanctions and Exclusion

Agora is a substrate, not a moderation engine. It honors two exclusion signals without defining policy:

  1. Participant revocation — records authored by a participant whose current capability passport chain is revoked MUST be excluded from default query output. The records are retained so that audit queries (include_flagged=true) can still reach them.
  2. Record-level flags — a separate artifact (out of scope for this proposal) may flag a record as hidden, under-review, or withdrawn. Relays MUST honor these flags in default query output. Flag artifacts MUST be content-addressed and verifiable themselves.

No substrate-level moderation is defined. Kind-specific moderation rules belong in the schema contract for that kind.

Record Envelope Examples

An opinion about a URL. The topic is a named channel dedicated to URL opinions; the subject relation is carried in record/about:

{
  "schema": "agora-record.v1",
  "record/id": "sha256:4Q7x3kCDfm2yVwrbn8Hj5tlsOe9zApiU6Gq3wXYAbCd",
  "record/kind": "opinion",
  "topic/key": "ai.orbiplex.opinions/url",
  "record/about": [
    { "resource/kind": "url", "resource/id": "https://example.org/article" }
  ],
  "author/participant-id": "participant:did:key:z6MkExample",
  "authored/at": "2026-04-11T08:15:00Z",
  "content/schema": "resource-opinion.v1",
  "content": {
    "schema": "resource-opinion.v1",
    "opinion/text": "Useful overview, but the sourcing is thin in the final section.",
    "opinion/lang": "en",
    "opinion/rating": 1
  },
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

The same opinion, placed instead under a per-URL topic (an alternative convention some applications may prefer). The substrate accepts both shapes; which one to use is a decision of the kind contract for record/kind = "opinion":

{
  "schema": "agora-record.v1",
  "record/id": "sha256:4Q7x3kCDfm2yVwrbn8Hj5tlsOe9zApiU6Gq3wXYAbCd",
  "record/kind": "opinion",
  "topic/key": "ai.orbiplex.opinions/url/sha256:4b7c9fzkMyAL8GBfQExamplePerUrlTopic01",
  "record/about": [
    { "resource/kind": "url", "resource/id": "https://example.org/article" }
  ],
  "author/participant-id": "participant:did:key:z6MkExample",
  "authored/at": "2026-04-11T08:15:00Z",
  "content/schema": "resource-opinion.v1",
  "content": {
    "schema": "resource-opinion.v1",
    "opinion/text": "Useful overview, but the sourcing is thin in the final section.",
    "opinion/lang": "en"
  },
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

A plain comment in a general channel. No external subject, no record/about:

{
  "schema": "agora-record.v1",
  "record/id": "sha256:Zp2m9ThKqgX4r7v8C5n1Yb6jD3WlFsTaephQkU2mov8",
  "record/kind": "comment",
  "topic/key": "ai.orbiplex.announcements/default",
  "author/participant-id": "participant:did:key:z6MkExample",
  "authored/at": "2026-04-11T09:00:00Z",
  "content/schema": "plain-comment.v1",
  "content": {
    "body": "Agora MVP landed on canary relay.",
    "lang": "en"
  },
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

A comment thread under a proposal. The topic is freely named by the publishing application; the parent link points to the first record in the same topic:

{
  "schema": "agora-record.v1",
  "record/id": "sha256:TH7q1WknMyAL8GBfQC5XrExampleThreadStart01",
  "record/kind": "comment",
  "topic/key": "ai.orbiplex.proposals/035/discussion",
  "author/participant-id": "participant:did:key:z6MkExample",
  "authored/at": "2026-04-11T10:30:00Z",
  "content/schema": "plain-comment.v1",
  "content": {
    "body": "Retention rules should be configurable per record kind, not only per topic.",
    "lang": "en"
  },
  "record/parent": "sha256:PR0p05kFtQYABcD1eFgH2iJKlMnOpExampleParent9",
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

A thread policy record can be published in the same topic before the root comment. The root comment then references it through record/policy. The policy is not embedded in plain-comment.v1; comments carry speech, while a thread-policy record carries the access rule for joining the thread.

{
  "schema": "agora-record.v1",
  "record/id": "sha256:PolicyPhoneConfirmedExample001",
  "record/kind": "thread-policy",
  "topic/key": "ai.orbiplex.proposals/035/discussion",
  "author/participant-id": "participant:did:key:z6MkExample",
  "authored/at": "2026-04-11T10:29:00Z",
  "content/schema": "comment-thread-policy.v1",
  "content": {
    "schema": "comment-thread-policy.v1",
    "policy/thread-topic-key": "ai.orbiplex.proposals/035/discussion",
    "policy/min-attestation": "phone-confirmed",
    "policy/inheritance": "descendants",
    "policy/may-tighten": true,
    "policy/may-loosen": false
  },
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

The effective policy for a comment is inherited from the nearest applicable ancestor policy:

root or ancestor policy
  -> child comment
  -> child may attach stricter policy for its own subtree
  -> child MUST NOT loosen inherited policy

Domain policy, not the Agora substrate, defines the ordering among attestation levels such as phone-confirmed, national-id, or community-trusted.

A public log entry produced by a workflow run. The topic is derived from the run identifier, and there is no external subject to reference:

{
  "schema": "agora-record.v1",
  "record/id": "sha256:WF001rk5d8m2hxQP4N7bT6vLYcFj3gsOwueazR9ikmt",
  "record/kind": "public-log",
  "topic/key": "ai.orbiplex.workflow-runs/01JRCY0Y7T4Y9JQK8K7R6K4M3M",
  "author/participant-id": "participant:did:key:z6MkWorkflow",
  "authored/at": "2026-04-11T11:00:00Z",
  "content/schema": "public-log-entry.v1",
  "content": {
    "step": "fan-out",
    "outcome": "any_one_satisfied",
    "dispatch_count": 3
  },
  "signature": { "alg": "ed25519", "value": "BASE64URL..." }
}

Hard MVP Scope

Feature MVP
agora-record.v1 envelope Yes
Content-addressed record/id Yes
Opaque topic/key with canonicalization rules Yes
Optional record/about linking to proposal 026 resource identity Yes
Cross-topic query by record/about (secondary index) Yes
Matrix backend (Dendrite or Conduit) Yes
Three relay roles (canonical, cache, origin) Yes
Query by topic with cursor Yes
Fetch by record/id Yes
Subscribe via long-poll or SSE Yes
Retention: age + count Yes
Retention: size + pin-by-kind Deferred
Record-level flags and hidden/withdrawn handling Deferred
agora-vault-entry.v1 encrypted opaque artifact vault Yes
E2E-encrypted private topics Deferred
Kind-specific schema registry Deferred
Native Orbiplex transport (non-Matrix) Deferred
WebSocket subscription Deferred

Deferred items are post-MVP and MUST NOT be implemented before the MVP scope is clean and tested.

Agora Vault

Agora Vault is a separate encrypted-artifact surface, not a topic record profile. It exists for cases where the system needs durable, admission-controlled opaque storage but must not expose the author, participant, nym, topic, or domain metadata that ordinary agora-record.v1 would reveal.

The canonical entry schema is agora-vault-entry.v1. Its public shape exposes only:

  • vault-entry/id;
  • opaque artifact/id;
  • artifact/kind;
  • the cryptographic envelope;
  • ciphertext.

The artifact payload and all domain metadata live inside ciphertext. A vault entry that contains plaintext payload fields, lacks ciphertext, or lacks at least one key envelope is invalid. Public lookup is GET /v1/agora/vault-artifacts/{artifact_id} and returns ciphertext-bearing entries only. Scoped list/get/delete use GET|DELETE /v1/agora/vaults/{vault_subject}/artifacts.... In the supervised local Node runtime those routes are gated by the Agora service client token and daemon host capability dispatch. Federated or remote-provider deployments bind the same operations to the frozen agora-vault@v1 capability/passport profile scoped to the opaque vault_subject.

The MVP writer used by recorded messaging may emit key-wrap = "dev-key-commitment". This is a development custody commitment to the discarded symmetric key; it proves that ciphertext was produced but does not make the entry decryptable by itself. Production recovery uses recipient key wrapping under x25519-hkdf-sha256+xchacha20-poly1305.

The implemented MVP runtime exposes these concrete operations:

Operation Runtime path Authorization boundary Notes
Put encrypted artifact POST /v1/agora/vaults/{vault_subject}/artifacts Agora client token locally; agora-vault@v1 remotely Request body is an agora-vault-entry.v1; invalid plaintext or missing key envelopes are refused.
List scoped artifacts GET /v1/agora/vaults/{vault_subject}/artifacts Same scoped vault authority Response includes list items with artifact/id, artifact/kind, vault-entry/id, stored/at, and optional ciphertext digest.
Get scoped artifact GET /v1/agora/vaults/{vault_subject}/artifacts/{artifact_id} Same scoped vault authority Returns the full encrypted entry after export schema validation.
Delete scoped artifact DELETE /v1/agora/vaults/{vault_subject}/artifacts/{artifact_id} Same scoped vault authority Performs a soft delete; scoped and public reads ignore deleted entries.
Public encrypted lookup GET /v1/agora/vault-artifacts/{artifact_id} Public lookup by opaque artifact id Returns filtered ciphertext-bearing entries only and does not reveal vault_subject, participant, nym, author, topic, or plaintext domain metadata.

The supervised agora-service stores MVP vault entries in agora-vault.v1.sqlite. The scoped uniqueness boundary is (vault_subject, artifact_id), so the same opaque artifact/id may exist in multiple vault subjects. Public lookup by artifact id may return more than one ciphertext entry, but each entry remains subject-opaque.

The supervised agora-service also registers daemon host capability handlers:

  • agora.vault.put;
  • agora.vault.list;
  • agora.vault.get;
  • agora.vault.delete.

Recovery is deliberately separated from the public Agora record surface. agora-vault-ref.v1 is a plaintext record shape intended to be sealed inside pseudonym-vault.v1; it maps recovered participant or nym material to the provider, opaque vault/subject, capability references, and allowed artifact kinds. A bare public participant id is not enough to list, get, or delete vault contents.

Recorded messaging is the first profile that uses Agora Vault: it stores a signed message-envelope.v1 as a generic encrypted artifact rather than as an Agora topic record. Vault writes are best-effort: temporary failure does not roll back message delivery, and the messaging outbox worker retries due failed-retryable vault jobs.

Interaction With Existing Proposals

Proposal Relation
013 — Whisper social signal exchange whisper-signal.v1 is a first-class Agora content/schema under record/kind = "whisper", using pseudonymous nym:did:key:... authorship (§2 invariant 9) anchored by author/nym-proof with inline-first nym-certificate.v1 material; Agora is one of two distribution mechanisms (the other is direct node-to-node exchange, see proposal 013 §Distribution), and Memarium is the local storage surface for both. Threshold-reached events remain a separate whisper-durable kind for when a whisper crosses bootstrap thresholds and needs a different durability posture.
023 — federated offer distribution The offer catalog today maintains its own storage; a future migration may express offer snapshots as Agora records under an operator-chosen topic key (for example orbiplex/offer-catalog) with record/kind = "offer-snapshot", retiring the bespoke replication layer
024 — capability passports Author signatures on Agora records are verified via the same passport chain
025 — seed directory as capability catalog Seed directory remains a separate primitive for capability discovery; it MAY later publish its listings as records on an Agora topic for uniform federation
026 — resource opinions and discussion surfaces Proposal 026 remains the single source of truth for resource identity (resource/kind + resource/id). Agora does not embed resource identity in its primary key; it reaches proposal 026 only through the optional record/about field. resource-opinion.v1 becomes the first content/schema on Agora, with record/kind = "opinion", and opinion records carry the referenced resource in record/about
032 — key delegation passports Delegated signing keys used for authoring Agora records follow the passport verification rules
034 — node operator binding Orthogonal. Agora does not participate in operator assurance gating

Relationship to Memarium

Memarium, as defined in the vision document, is the memory-and-knowledge layer: curated archives, community resources, cultural artifacts, with federation, replication, versioning, durability rules, and semantic retrieval. Memarium answers the question what must not disappear.

Agora is a narrower primitive. It answers the question where do topic- addressed public records live and how are they federated. It is a substrate, not a curator, and it makes no decisions about what is worth preserving.

The relationship:

  • A community-scale Memarium space MAY use Agora as one of its underlying substrates for the ingestion of public records into the community layer.
  • Memarium's private and personal spaces are out of scope for Agora. Personal notes, idiolectal archives, and private working spaces belong in a Node-local store.
  • Memarium curation (indexing, semantic search, summarization) happens on top of Agora records or Node-local stores, not inside the substrate.
  • A Node may run a cache relay for Agora without running any Memarium component, and vice versa.

When a separate Memarium proposal lands, it will describe how curation attaches to Agora records and Node-local stores, and will not redefine Agora.

Implementation Notes

The suggested first implementation path is:

  1. Relay shell — a Rust service that wraps a Dendrite or Conduit homeserver, exposes the Agora HTTP API, and translates calls into Matrix room operations.
  2. Record verification library — a standalone Rust crate that encodes the canonical JSON rules, computes record/id, verifies signatures, and enforces ingest invariants. Reusable across relay, Node, and any future native transport.
  3. Node client — a thin client in node/agora-client that lets the daemon submit, query, subscribe, and cache.
  4. Middleware seam — Agora relay may run as a middleware-attached service on an umbrella operator's Node, following the same supervision pattern as proposal 019 (supervised local HTTP-JSON middleware).

The first concrete migration target is proposal 026: serve resource opinions through Agora as record/kind = "opinion" with content/schema = "resource-opinion.v1".

Implemented crate architecture

The implementation follows a stratified layering, where each crate has a single responsibility and communicates through thin trait or data contracts:

L4.b  agora-http             REST/SSE surface (framework-neutral)
L4.a  agora-relay-matrix     composition: SQLite store + Matrix transport
L3    agora-matrix-client    MatrixRelayTransport<S: MatrixEventSink>
      agora-matrix           pure data bridge (alias, content translation)
L2    HttpMatrixEventSink    thin Matrix CS API sink (reqwest + ruma types)
L1    MatrixCsClient         typed join/send/sync over Matrix CS API
L0    AuthenticatedHttpClient bearer-auth HTTP + rate-limit retry
      agora-relay-sqlite     persistent local relay (SQLite WAL)
      agora-relay-mem        in-memory reference relay
      agora-relay-trait      AgoraRelayTransport / AgoraRelay / SequencedRecord
      agora-core             envelope verification, canonical JSON, signatures

Matrix transport: thin HTTP sink

The Matrix transport uses reqwest::blocking and ruma 0.14.1 (types and endpoint definitions only, feature client-api-c) plus a small hand-written Client-Server HTTP wrapper. This keeps the transport auditable, avoids a full client state machine, and keeps Agora relay semantics in the relay crates rather than in a Matrix runtime. The MatrixEventSink trait decouples MatrixRelayTransport from the concrete HTTP implementation while preserving a single production path for the MVP.

Three Matrix CS API endpoints are used:

  • POST /_matrix/client/v3/join/{roomIdOrAlias} — idempotent room join
  • PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId} — send event with Agora record/id as deterministic txnId for server-side idempotency
  • GET /_matrix/client/v3/sync — one global long-poll loop per sink, events demultiplexed to per-(room, event_type) subscribers

Federated relay composition (L4.a)

MatrixBackedRelay<S: MatrixEventSink> composes SqliteRelay (local persistent store) with MatrixRelayTransport<S> (federation channel) and implements the full AgoraRelay trait (ingest + subscribe + query + fetch). Key properties:

  • Local-first ingest: records are persisted in SQLite before being forwarded to Matrix. Matrix failure does not block local persistence.
  • Forward-only-fresh: duplicate records (by record/id) are not forwarded to Matrix, preventing redundant federation traffic.
  • Relay metadata stamping: relay/received-at (RFC 3339 UTC), relay/id, and relay/hops are stamped by the relay on every ingest, as specified in section 2. These fields are excluded from content address and signature (via VerifiedRecord relay metadata helpers), so stamping does not break the envelope invariant.
  • Inbound bridge: a background thread per topic subscribes to the Matrix transport, verifies inbound events, and ingests them into the local store. The bridge starts lazily (Cache mode) or eagerly (Canonical mode) and includes a cooldown to prevent tight respawn loops on transport failure.
  • Retention sweep: sweep_retention() enforces max_age and max_count per topic, then rebuilds the subject index to prune evicted entries.
  • Three relay roles are configuration, not separate implementations: Canonical (eager inbound bridges for configured topics), Cache (lazy bridges on first subscribe/query), Origin (outbound only).

Subject index (L4.a internal)

The cross-topic record/about index from section 5.5 is implemented as a SubjectIndex trait internal to agora-relay-matrix. It is populated as a side-effect of ingest and queried by the HTTP API for subject-based lookups. The MVP implementation is in-memory (InMemorySubjectIndex) and rebuildable from the local SQLite store via rebuild_subject_index(). The index is idempotent: re-indexing the same record is a no-op. The trait supports clear() for rebuild cycles triggered by retention sweeps or process restarts.

HTTP API surface (L4.b)

agora-http is a framework-neutral adapter that maps proposal 035 §5 endpoints to AgoraRelay + SubjectIndex calls. It depends on no HTTP framework (axum, hyper, actix-web); a daemon or middleware layer wraps it in actual HTTP routing. Key design choices:

  • Opaque cursors: pagination uses base64url-encoded sequence numbers. Clients must treat cursors as opaque strings. The Cursor type encodes/decodes on the HTTP boundary; relay internals use bare u64 sequences via SequencedRecord.
  • SSE for subscriptions: SseRecordStream formats verified records as standard Server-Sent Events (event: agora.record, id: {record/id}, data: {json}).
  • Topic mismatch validation: POST /topics/{topic-key}/records rejects records whose topic/key does not match the URL path.
  • Partial about-filter rejection: when exactly one of about_kind or about_id is present, the API returns an explicit error instead of silently ignoring the filter.
  • include_flagged wire compatibility: the parameter is accepted but has no effect in MVP; flag semantics are deferred per section 8.
  • Query attestations: historical query pages include agora-query-attestation.v1. The reference adapter emits unsigned attestations that bind query scope, normalized filter, record ids, cursor metadata, and a deterministic digest. Relay signatures over attestations are a later signer/authority integration, not an agora-core concern.

Local runtime data-dir ownership

An Agora service instance has exclusive ownership of its local runtime data directory. This is the Agora middleware data directory, not the Node profile root. Under daemon supervision the default path is:

<node-data-dir>/middleware/agora-service/data

The Agora middleware data directory is a single-writer local state root, not a multi-process database contract. At most one live Agora service instance MUST use a given Agora middleware data directory at a time.

This ownership invariant covers the SQLite relay store, the subject index rebuild cycle, retention sweeps, rate-limiter snapshots, and any other process-local caches or timers derived from the same root. Adding file locks around individual snapshot reads and writes would protect only those I/O operations; it would not make two Agora runtimes share one coherent timeline, one cache state, or one retention policy execution.

Implementations SHOULD enforce the invariant with an exclusive startup-time lock file inside the Agora middleware data directory and fail fast with an operator-readable error when the lock is already held. Snapshot writes SHOULD remain crash-safe through the usual temp-file, fsync, and atomic rename discipline.

Per-file shared/exclusive locks MAY be added later as a compatibility mechanism for external diagnostic, backup, or migration tools that need to read snapshot files while Agora is running. Such locks MUST NOT be interpreted as permission to run multiple Agora service instances against the same Agora middleware data directory.

MVP implementation status vs this proposal

Implemented

  • Envelope verification (agora-core): content address, Ed25519 signature, schema version, topic key syntax (empty, length, whitespace, control chars). Cross-language fidelity verified against a Python verifier (agora-verifier middleware module).
  • Local relays (agora-relay-mem, agora-relay-sqlite): ingest with idempotent duplicate detection, per-topic append log, monotonic sequence assignment, QueryFilter-based query with SequencedRecord results, replay-capable subscriptions, fetch by record/id.
  • Relay metadata stamping: relay/received-at, relay/id, and relay/hops stamped by the federated relay (agora-relay-matrix) on ingest, via VerifiedRecord relay metadata helpers which preserve the envelope invariant (relay fields are excluded from content address and signature).
  • Matrix transport (agora-matrix-client): thin HTTP sink using reqwest::blocking + ruma 0.14.1 types. Three CS API endpoints (join, send with deterministic txnId, global sync with demux).
  • Matrix data bridge (agora-matrix): deterministic room alias derivation (#agora.<hex-sha256-nfc>:<domain>), event type org.orbiplex.agora.record.v1, bidirectional record ↔ event content translation with full envelope verification on inbound.
  • Federated relay (agora-relay-matrix): MatrixBackedRelay<S> composing SQLite + Matrix transport. Local-first ingest, forward-only-fresh, lazy/eager inbound bridges with cooldown, three relay roles (Canonical/Cache/Origin), retention sweep (max_age, max_count).
  • Subject index (agora-relay-matrix): in-memory SubjectIndex for cross-topic record/about queries, idempotent, rebuildable from local store.
  • HTTP API surface (agora-http): framework-neutral adapter for proposal 035 §5 endpoints. Opaque cursors, SSE subscriptions, topic mismatch validation, partial about-filter rejection.
  • Service and daemon integration (agora-service + supervised middleware): the relay runs as a daemon-managed local HTTP middleware service. The daemon supervises the process, provisions module/client auth tokens, and exposes capability-provider discovery while the service owns its REST/SSE surface.
  • Retention (age + count + scheduling): sweep_retention() with automatic subject index rebuild after purge, driven by the agora-service retention sweep thread when topic policies are configured.

Ingest invariants still deferred or limited

The following invariants from section 2 use "flag, not reject" semantics in this proposal and are not blocking for the first deployment:

  • Invariant 5 (authored/at clock skew window) — not checked; the field is validated as non-empty only.
  • Invariant 6 (record/parent / record/supersedes dangling detection) — implemented at the publisher edge in agora-service. The default is warn-and-accept to preserve eventual consistency; strict_parent_refs = true turns unresolved references into HTTP 422. Federation ingress remains warn-only.
  • Invariant 7 (passport chain verification via proposals 024 and 032) — the Rust AgoraSignature struct now carries a stratified shape with key/public (the actual signing key bytes) and an optional key/delegation block (delegation proof chain per proposal 032). Direct-key signatures are fully verified end-to-end (key match against the author's participant id is a hard block via SignAdapterError::PublicKeyMismatch). Delegated signatures are accepted structurally and verified through the CapabilityDelegationVerifier trait wired in L4.b of the 035-impl doc; the default deployment installs a permissive verifier, and hardened deployments plug in the agora-capability-bridge proof checker. The legacy signature.key/ref shape is retired and no longer appears in the schema or the code.
  • Invariant 8 (record/about resource identity structural validation per proposal 026) — the array is accepted structurally; individual entries are not validated against resource-ref.v1.
  • Schema-level patterns (record/kind, content/schema, record/id length bounds) — not enforced by the verification library; the library checks non-empty semantics only. Schema-level validation is a relay-side concern.
  • Topic key NFC normalization — the verification library checks syntax only (empty, length, whitespace, control chars). NFC normalization is applied by the canonical JSON layer before hashing and signing, so content addresses are stable regardless of input NFC form, but the relay does not currently reject or normalize non-NFC topic keys at the ingest boundary.

Still deferred / intentionally limited

  • Subject index persistence: for MVP, the record/about SubjectIndex is intentionally a derived, rebuildable view over the local relay store. It is not a source of truth and does not need its own persisted projector to satisfy the v1 contract. rebuild_subject_index() reconstructs it from the local SQLite store on startup; a SQLite-backed subject index should be added later only when listing volume, restart latency, or retention churn makes rebuild cost material.
  • Room-to-topic name mapping (section 6, rule 2): the relay does not yet maintain a human-readable room-name mapping for operator tooling.

Consequences

Positive:

  • one portable record envelope replaces several bespoke storage contracts across future subsystems,
  • proposal 026 gains a concrete storage and federation substrate without having to invent one,
  • the offer catalog and seed directory gain a future migration target if umbrella operators want to consolidate replication on one substrate,
  • Matrix reuse avoids inventing a federation transport,
  • topic addressing is unified with resource addressing across Orbiplex.

Tradeoffs:

  • introducing Matrix as a backend adds an operational dependency for umbrella operators running canonical relays; Nodes running cache relays can avoid it via HTTP-only cache mode,
  • Agora records are public by default in MVP, which forces applications that need private channels to wait for a later proposal,
  • content addressing forces strict canonical-JSON discipline on authors; any post-signature mutation invalidates the record,
  • the offer catalog currently maintains its own storage; migration is a future decision, not a hard requirement of this proposal.

Alternatives Considered

  • NATS JetStream as backend. Rejected because NATS is optimized for low-latency messaging, not citeable archive state, and because its cluster model assumes trusted operators rather than cross-umbrella federation. A later proposal may add JetStream as an optional backend for latency-sensitive record kinds that do not require long-term archival.
  • Invent a native transport from the start. Rejected because the envelope value is independent of the transport, and shipping the envelope on a proven transport first lets the schema mature before the transport is locked in.
  • Extend proposal 025 (seed directory) into a generic substrate. Rejected because the seed directory has a specific payload contract (capability passports and offers) that would blur if generalized.
  • Extend proposal 023 (offer distribution) into a generic substrate. Rejected for the same reason.

Known Limitations

  • MVP has no private topics. Any record submitted to Agora is considered public and visible to every participant that can reach the relay.
  • MVP has no substrate-level moderation. Record-kind contracts must carry their own moderation rules.
  • MVP has no formal schema registry for record/kind; registration happens through proposals and schema files.
  • Matrix homeservers have their own operational footprint; small Nodes running cache relays only SHOULD NOT be required to run a full Matrix homeserver.

Open Questions

  1. Should Agora topics have a lightweight per-topic policy document (for example agora-topic-policy.v1) that constrains authorized record kinds, retention, and writer set, or should those constraints remain relay-wide?
  2. Should record/supersedes be enforced as one-author-one-current-record per topic for specific kinds (for example opinion), or is that a kind-specific rule? Proposed: kind-specific, not substrate-level.
  3. Should the offer catalog and seed directory migrate onto Agora? Proposed: not in this proposal. Evaluate after the MVP is in production for at least one umbrella operator.
  4. Should Agora records be signable by delegated keys issued under a capability passport, or only by the participant root key? Proposed: delegated keys are allowed via proposal 032 passport chains.
  5. Should the substrate offer any supported topic-key conventions (for example a recommended hierarchy grammar) even though it does not parse them, or should convention be left entirely to kind contracts? Proposed: publish a non-normative conventions note alongside the schema file, not a substrate rule.
  6. Should the record/about cross-topic index be rate-limited or quota- bounded by relays to prevent abuse of subject-driven fan-out? Proposed: relays MAY impose result windows and rate limits, with defaults chosen during MVP deployment.
  7. Should the relay expose a canonical federation tick rate and a federation lag metric, and if so, where should it be surfaced? Proposed: reuse the observability pattern from proposal 023.

Follow-Up

If adopted, the next artifacts should be:

  1. one schema file for agora-record.v1,
  2. one schema file for plain-comment.v1 as the first general-purpose base kind,
  3. one schema file registering resource-opinion.v1 as an Agora record/kind = "opinion",
  4. one implementation note describing the Matrix mapping and the Dendrite or Conduit deployment footprint,
  5. one separate proposal for private (encrypted) Agora topics,
  6. one separate proposal for Memarium, describing how curation attaches to Agora records and Node-local stores,
  7. proposal 040 (custodial redelivery and tombstones) defines the readback tombstone vocabulary (410 Gone with retention_expired, removed_by_policy, storage_lost, superseded), operator sovereignty over re-acceptance, custodian negotiation via capability-passport.v1, and the author-scoped sondage endpoints (count, listing, digest) gated by agora-author-proof.v1. Agora implementations SHOULD implement the tombstone surface as soon as the relay serves more than one node; pure local-only MVP relays MAY keep 404 as the sole absence signal,
  8. proposal 041 (Agora ingest attestation and tiered access) defines the author/attestation_ref optional signed envelope field, the agora-attestation-proof.v1 short-lived proof token, the ingest mode matrix (open, allowlist, passport, passport_scoped, layered), a predicate grammar for tiered gates, and a stable refusal reason-code vocabulary symmetric with proposal 040's tombstone matrix. The verification primitive is placed as a reusable attestation-gate alongside signer and sealer in the daemon's trust-boundary zone and is consumable by every component that needs to ask "is this author attested under a passport I currently accept", not only Agora.