Site governance
Site governance lives in Matrix itself. A site is a Matrix Space; the
Space's m.room.power_levels and each comment room's own
m.room.power_levels are the source of truth for who may manage and
moderate it. Cumments projects those state events into disposable read
tables for API visibility; it never enforces a separate local roster.
Why this design
Governance must not conflate the platform operator with the owner of every site: a multi-site deployment would otherwise leave each site owner with a foreign high-privilege account permanently resident in their Space. Cumments keeps the two apart:
- the AppService sender is the room creator and therefore the platform's backstop — it is never represented as a role;
- ownership is a claim-token capability held by the site owner;
- each site has site admins (100), managers (75) and per-room moderators (50), encoded purely in Matrix power levels;
- day-to-day management happens in a Matrix client or through the bot; the API and Operator API are just other writers to the same Matrix state;
- the backend projects power levels into
site_roles/room_rolesand reconciles them, but never enforces a local copy.
Roles and levels
The site owner is not a Matrix role: the owner is whoever holds the site's claim token. Owners appoint and remove site admins and transfer ownership through the claim-token API. A site may have zero or more site admins; the owner's own Matrix account is usually the first one.
ban, kick, redact and the state_default thresholds stay at Matrix's
default 50, so managers (75) and moderators (50) have the same practical
moderation powers inside a room. The 75 level exists so site-managed roles
can be told apart from per-room moderators, which is what the reconciliation
pass keys on.
The m.room.power_levels event itself is locked to 100 in the Space and
75 in comment rooms (events: {"m.room.power_levels": 75} in rooms),
and m.room.tombstone is locked to 150. Managers can therefore appoint room
moderators exactly like a Matrix client, while only site admins and the room
creator (the AppService sender) can change the Space. The AppService sender
is the room creator: it is the platform's backstop and is not represented as
a role.
The reconciliation boundary: ≥ 75, never 50
The Space is where site-level roles are managed, but every comment room has its own copy of the power levels. A background pass keeps each room aligned with the Space:
- entries at ≥ 75 (admins 100 + managers 75) are replicated from the Space into every room — new managers are added, revoked ones removed;
- entries at 50 are per-room moderators and are never touched by the pass; site admins and managers manage them room by room.
This makes 75 the "manager-only" slot: anyone holding 75 in a room must also be a Space manager, otherwise the pass removes them from that room. To let someone help in just one room, appoint them as a 50 moderator.
Example: the Space has {admin: 100, alice: 75} and room A has
{admin: 100, alice: 75, bob: 50} (bob moderates only room A):
The pass is triggered by the 60-second reconcile cycle, by an API write, and by Space power-level pushes; it is idempotent, retried on failure, and serialized per site. New rooms are seeded from the Space roster at creation time (admins + managers, moderators start empty).
Day-to-day workflow
- Self-service: the site owner sends
!cumments sites register <site_id>in a DM with the bot. The bot registers the site, creates the Space and writes the sender as the first site admin immediately. Alternatively, register through the API and appoint any Matrix account as the first admin (the API returns a one-time verification token because the registrant and target are not necessarily the same account). The DM for!cumments(including this firstsites register) must be unencrypted — the Bot currently does not decryptm.room.encryptedand therefore cannot read commands in E2EE rooms (it will join but not respond). If the homeserver is configured withencryption_enabled_by_default_for_room_type = "invite"(Tuwunel) or the client creates E2EE DMs by default, create the DM with encryption disabled, re-invite the bot and resend the command. This is a Bot implementation limit, not a Matrix or DM-in-general requirement; comment rooms may be encrypted or not, and the site owner may be on a different homeserver (federated) — e.g.@alice:matrix.orginviting@_cumments_bot:comments.example— as long as the Bot DM itself is unencrypted. - For API/operator-appointed roles, the target Matrix account sends
cumments-claim:<token>as a direct message to the AppService bot in an unencrypted 1:1 DM (the only two members are the bot and the sender; samem.room.encryptedlimitation as above — the Bot cannot readcumments-claim:in an encrypted room). Once the homeserver pushes that DM, Cumments activates the claim and writes the role into Matrix power levels. - Site admins manage the Space in a Matrix client or through the bot: they add/remove managers, retire rooms, issue secrets and appoint room moderators.
- Managers appoint and revoke room moderators through their Matrix client or the bot; the room's power-levels threshold (75) lets them edit the room exactly like a Matrix client would.
- The homeserver pushes state events to Cumments, which projects them into the read model. The background pass keeps room-level site roles aligned with the Space.
Token-DM verification
Every role registration through the API/operator path — including the first
site admin — starts as a pending claim with a 24-hour expiry. The claim
does not affect Matrix until the target MXID proves ownership by sending the
exact text cumments-claim:<token> to the AppService bot in an
unencrypted 1:1 DM (the only two members are the bot and the sender;
the Bot does not decrypt m.room.encrypted, so the token DM must also be
unencrypted). The bot's self-service
site register path skips this second step because the homeserver has
already authenticated the sender in the DM; it records an applied claim
instead.
The site owner may use a Matrix account on a different homeserver from the
Cumments instance (e.g. @alice:matrix.org inviting
@_cumments_bot:comments.example). Self-service invite admission is
available to any normal Matrix user — local or federated — that passes the
existing governance identity check (valid MXID, not AS-managed), subject to
a per-inviter invite rate limit (5/min). Group-room invites may still cause
the bot to join, but governance commands remain blocked by the
is_private_channel (exactly bot + sender joined) gate.
Claim lifecycle
POSTcreates (or, for an existing claim on the same role, rotates the token of) apendingclaim and returns the one-timeverify_token.- The projector matches the DM, checks
sender == target MXID, and marks the claimactivated, then wakes the reconciler. - The reconciler writes the role to power levels and marks it
applied; the homeserver pushes the power-levels event back, and normal projection records the role insite_roles/room_roles. DELETErevokes apendingoractivatedclaim without touching Matrix; a role that is already applied is removed from power levels directly.
The claim table is process state, not the source of truth: the applied role
is authoritative in Matrix. After a database reset, pending claims are gone
and must be re-registered (acceptable for a short-lived one-time token), while
applied roles rebuild from power levels via backfill.
Matching rules
Only a plain m.text message with normal style and no relation activates a
claim: m.emote / m.notice are rejected, formatted_body is ignored (the
body is authoritative), and the token is compared in constant time against
the SHA-256 hash stored in the claim row. Non-matching DMs are silently
ignored. The bot never replies, so there is no forged callback surface.
Security properties
- The token is 32 random bytes, hex-encoded, one-time and short-lived (24 h).
sender == target MXIDis guaranteed by the homeserver; Cumments cannot be tricked into activating a claim for another account.- Claim creation stays behind the site's claim token (or operator token) and the governance rate limiter, and the number of pending claims is bounded.
API
Site-owner (claim-token) operations authenticate with the token returned at
site registration (X-Cumments-Claim-Token). Operator fallbacks live under
/api/v1/operator/sites/{site_id}/... and use the operator token.
POST responses are
{ "pending": true, "user_id", "level", "verify_token", "expires_at" };
DELETE responses are { "revoked": true, "user_id", "level" } and are
idempotent. Reads come from the projection and are therefore eventually
consistent with Matrix.
The CLI mirrors admin, manager and moderator operations locally
(cumments sites admins add / remove, managers add / remove,
moderators add / remove) and can start a transfer with
cumments sites owners transfer: add stores a pending claim and prints the
verify_token, while remove cancels pending claims or removes applied roles
through the shared management use case (see CLI).
Ownership transfer and recovery
The owner (claim-token holder) can start a two-phase transfer with
POST /api/v1/sites/{site_id}/ownership-transfers. The target verifies the
usual cumments-claim token; once verified, Cumments resets the site-admin
roster to the new owner's verified account, rotates the claim token and
delivers the new token in the bot DM. Old site admins are removed with the
transfer.
If the claim token is lost, the Cumments operator can rotate it through the Operator API and appoint a fresh site admin. The AppService sender is the creator of every Space and room, so it remains the last-resort backstop even when a site has no admins.