Data model
This page describes how Matrix events are shaped into the typed comment model that the API and SSE stream expose. It documents the mapping and the storage layout; the runtime system around them is covered in architecture.
The read model separates Matrix-derived facts from local control state. Fact
projection rows can be rebuilt from Matrix history with cumments backfill;
submission queues, idempotency records, claims, secrets, audit entries,
quarantine decisions and anti-resurrection tombstones are local control-plane
state with their own durability rules (see
architecture).
Design principles
- Content is a sealed type system. Each displayable kind of content has
its own structured payload instead of one universal JSON blob. Clients
dispatch on the
typetag. - Raw JSON is the escape hatch. The protocol keeps evolving; unknown or future types keep their original payload so history is never lost and can be reinterpreted later.
- Relations are not content. Replies, threads, edits, reactions and deletions are attributes or edges of a message, modeled separately from the body. Redaction removes the message's active content from public presentation but does not erase already-established relation metadata from the projection.
- Authors follow live profiles. Display name and avatar render from the
author's current joined
m.room.memberprofile, so renames and avatar changes propagate to old comments. The values captured at projection time are kept as a fallback for authors who left the room. - Boundaries are explicit. Voice/video calls are out of scope, and encrypted content is modeled only as a placeholder.
- Matrix semantics are preserved at the domain boundary. When a Matrix concept is represented by the Cumments model, semantically distinct Matrix concepts remain distinct in the model even when Cumments exposes a simplified API. Matrix-specific wire representation remains an implementation detail of the Matrix integration layer.
Message shape
Edit revisions are stored in a dedicated message_revisions table, but the API
currently exposes only edited_at. A revision is an immutable relation fact;
redacting an edit hides that revision and the displayed content falls back to
the latest surviving revision or the original message.
Redaction also replaces the revision payload with a redacted tombstone; only
its event metadata remains for replay/audit.
room_id, sender_mxid and the raw Matrix content are internal integrity
fields and are never serialized to API or SSE clients.
Visitor authors carry an Ed25519 public_key; their virtual-user Matrix ID is
an implementation detail and is not exposed. Matrix-native authors carry
their mxid and never a public_key.
Content types
Matrix event mapping
Reactions and poll responses are annotation edges, not comment messages: they are stored in their own tables and aggregated onto the target message.
Avatars
Matrix has no single avatar entity; avatars live in three spec-defined places and Cumments projects all of them:
- Global profile (
avatar_urlprofile field): the canonical identity avatar of a user. Visitors set it through the visitor avatar API, which stores it on the virtual user's profile and propagates it to joined rooms asm.room.memberevents (MSC4466propagate_to: allquery parameter). m.room.member.avatar_url: the per-room profile snapshot. It is the source used when projecting message authors; leave events keep the last known value instead of wiping the snapshot, and redaction removes it.m.room.avatar: the room's own avatar.urlabsent means "no avatar";info.thumbnail_urlis preserved in the raw state JSON but is not part of the API contract — the room endpoint derivesavatar_thumbnail_urlfrom the main image through the 96×96 crop thumbnail variant instead.
Author profiles are projected into author_display_name / author_avatar_url
when a message is stored, but the public read path (message list, single
message, SSE) overlays the author's current joined m.room.member profile.
Visitors and Matrix-native authors behave identically: display data is Matrix
profile state, never signed event content. Members who
left the room keep the stored projection as a fallback; a redacted member
state keeps the membership but drops the profile, so reads show no profile,
same as a cleared display name or avatar.
All avatar URLs are stored as mxc:// and rewritten to signed media-proxy
URLs on the way out of the API (see Media proxy); the proxy
itself uses the authenticated /_matrix/client/v1/media endpoints with the
AppService token.
Not part of the comment stream
Two classes of events are deliberately excluded from the Message model:
- Ephemeral events (
m.typing, receipts, presence) are not part of room history and cannot be backfilled; they flow through a separate channel, see Ephemeral events. - Room state events (
m.room.member, name, topic, avatar,m.room.power_levels, tombstones,m.space.*) are in room history, but they are room metadata rather than comments. A light metadata model (room_members+room_state_events) records joins/leaves and name, topic and avatar changes;GET .../roomreturns that metadata (includingavatar_thumbnail_url) and the most recent system messages. Power levels feed the governance projection instead (see site governance).
Voice/video calls are not modeled at all.
Storage
Notes on the layout:
- Edit revisions include redaction metadata so removing one replacement can roll the public view back deterministically. Redaction clears the authored replacement payload.
- Parent deletion sanitizes the parent and removes all of its revisions; late replacements cannot restore deleted content.
- Deleting a comment also clears its original/current payloads and raw Matrix content.
- The author proof (
signature,challenge) is verified at projection time and is not stored in the read model; only the public key is kept for edit/delete authorization. reactionsandpoll_response_eventsare keyed by event ID, making push redelivery and backfill idempotent. Poll aggregation selects each voter's latest non-redacted response; it stores normalized MSC3381 selections while retaining a first-choice index for older rows. Redacting that response clears its selections and restores the previous valid vote.formatted_bodyis passed through unchanged. Clients that render plain text only may skip sanitization; any client rendering HTML must sanitize it first.media_uploads.page_slugis nullable: comment media records the page it was authorized for, while visitor avatars are site-scoped records with aNULLpage. Avatar media is marked referenced at upload time so the unused-media sweep never collects a profile avatar.
Ephemeral events
Typing indicators, read receipts and presence are not comments, but they make
the comment section feel alive. Cumments keeps a resident /sync connection
with the AppService token and bot identity (the push transaction stream does
not carry ephemeral events), filters for ephemeral/presence only, and keeps
per-room in-memory state:
- Subscribing SSE clients receive the current snapshot first, then
incremental
ephemeralevents (type: typing/receipt/presence). - Presence is filtered to users who are joined members of a subscribed room,
so it cannot leak across sites; the typing snapshot carries the member's
display name from
room_members. - Nothing is persisted or projected; it is an in-memory event channel.
- Protocol limits still apply: private read receipts (MSC2285) are invisible to the AppService, so only public receipts (MSC2666) can be surfaced.
Visitor sending capability
The API turns typed requests into Matrix events sent by each visitor's virtual
user. Every event carries a signed proof block under the
host.curious.cumments content namespace, which the projector verifies
before trusting the projection.
Reactions and votes are sent synchronously and are naturally idempotent
(reaction dedupe by sender + key, latest-vote-wins); text, media, location
and other comment-shaped writes go through the async submission queue with an
Idempotency-Key (see API).
Media proxy
mxc:// URIs require Matrix credentials to download, so Cumments exposes a
public, read-only proxy with short-lived signed URLs. The proxy is limited
to the configured homeserver, rate limited, size-capped, filtered by content
type, answers media and thumbnail requests through the authenticated
/_matrix/client/v1/media endpoints (MSC3916) with the AppService token, and
applies Matrix media-id/CSP/disposition safety rules.
Thumbnail requests carry signed width/height/method parameters;
message thumbnails default to 320×240 scale and avatars to 96×96 crop.
It is deliberately read-only: site administrators browse media directly in
their Matrix client, which is one benefit of building on Matrix (see
API).