matrix-axon

ADR 0055 — Room metadata exposure strategy

Context

The cross-account room list (GET /v1/rooms, ADR 0019) returns a curated RoomDto projected from RoomSummary (crates/axon-api/src/dto.rs, crates/axon-store/src/rooms.rs): name, topic, avatar_url, canonical_alias, last_activity_ts, last_event_id. That is the only room-metadata surface clients have.

Several requested features need metadata the server already stores but does not expose:

ADR 0016 established the storage projections (room state and account data as O(1) point lookups, global account data under a '' room_id sentinel). The store layer can already answer all of these reads; the gap is purely the read API surface.

The question this ADR answers: how should Axon expose room metadata generally, rather than bolting on a one-off field each time a client needs another signal?

Decision

Adopt a two-tier model. Do not add a single “all metadata” endpoint: room metadata is heterogeneous in privacy (room-public state vs. account-private data), shape (single state values vs. maps vs. derived booleans vs. counts), and cost (an O(1) name lookup vs. a membership aggregation). One blob would force all of that into one shape and couple list performance to its most expensive field.

Tier 1 — curated derived fields on the read DTOs

Extend RoomSummary → RoomDto (the existing ADR 0016 projection) with a small, bounded set of server-computed signals that the room list needs to decorate, sort, or filter cheaply, and that require server-side derivation or are universally useful:

room_type and tags are the stronger case for Tier 1: both are per-room state/account-data, so a Tier-2-only design would need one API call per room to decorate a list of N rooms (Tier 2 has no batch-read endpoint) — an N+1 cost that scales with list size. That per-room fan-out, not “the server can’t expose it,” is the actual performance argument for elevating them.

Member/unread counts are explicitly out of scope for Tier 1’s list projection because they are the expensive case; if needed they belong on the per-room detail read below (or a separately cached summary), so list latency does not scale with the priciest field.

Tier 1 is opinionated and curated: it is where the product decides which signals are first-class enough to drive list UX. New first-class signals are added here deliberately, with review, not reflexively.

Tier 2 — generic account-data and state reads (the escape hatch)

Mirror the Matrix client-server API so any other metadata is reachable without a bespoke DTO field, keeping the list lean and avoiding DTO churn as needs grow:

These return the stored content JSON as-is. They are the completeness layer; interpretation of less-common types is the client’s responsibility. This is the read half of the previously proposed generic passthrough endpoint (issue #130); writes remain out of scope here (see ADR 0038 Phase 2 / the mutations gateway, ADR 0021).

The dividing line

A field graduates from Tier 2 to Tier 1 only when it earns first-class list UX (as is_direct and tags do now).

Optional: per-room detail read

A GET /v1/accounts/{account_id}/rooms/{room_id} returning a richer room object (summary + derived fields + counts) is a natural home for fields too expensive for the list (member counts) that are wanted on room-open. Specified as a follow-on, not required for is_direct.

Alternatives considered

Consequences

Suggested implementation sequence

  1. Store/API (Tier 1): add is_direct (and room_type, tags) to RoomSummary → RoomDto, computing is_direct from m.direct. room_type has shipped; is_direct and tags are ADR 0103 / issue #367.
  2. API (Tier 2): the generic account-data / state read endpoints. ADR 0084 superseded this for four state clusters; remaining global account-data GET/PUT is not part of ADR 0103.
  3. TUI: consume is_direct for a room-list DM indicator; later, migrate pinning to m.tag via the new reads. Both are ADR 0103 / issues #369 and #370.

Each is its own PR/silo; Tier 1 unblocks the TUI DM indicator on its own.