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¶
| Role | Space | Every comment room | Meaning |
|---|---|---|---|
| Site Admin | 100 | 100 | Full site-level operational powers; may manage managers, moderators, stickers, secrets, retirement and upgrades |
| Manager | 75 | 75 | Site-level deputy, replicated into every comment room; may appoint/revoke room moderators |
| Room moderator | — | 50 | Appointed per room, in that room only |
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):
| Change in the Space | Effect on room A | bob |
|---|---|---|
carol becomes a manager |
carol: 75 is added to A |
untouched |
alice's manager role is revoked |
alice: 75 is removed from A |
untouched |
| bob's moderator role changes in A | no participation | changed by an admin/manager |
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 site register <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). - For API/operator-appointed roles, the target Matrix account sends
cumments-claim:<token>as a direct message to the AppService bot in a 1:1 DM (the only two members are the bot and the sender). 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 a 1:1 DM (the
only two members are the bot and the sender). 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.
Claim lifecycle¶
pending → activated → applied
│ │
└── revoked ┘
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.
| Endpoint | Method | Payload | Effect |
|---|---|---|---|
/api/v1/sites/{site_id}/admins |
POST / DELETE | POST body or DELETE ?user_id= |
Appoint/revoke a site admin (POST returns a pending claim + token) |
/api/v1/sites/{site_id}/managers |
POST / DELETE | POST body or DELETE ?user_id= |
Appoint/revoke a manager (POST returns a pending claim + token) |
/api/v1/sites/{site_id}/pages/{page_slug}/moderators |
POST / DELETE | POST body or DELETE ?user_id= |
Appoint/revoke a room moderator (POST returns a pending claim + token) |
/api/v1/sites/{site_id}/roles |
GET | — | Projected admins and managers |
/api/v1/sites/{site_id}/pages/{page_slug}/roles |
GET | — | Projected admins, managers and moderators |
/api/v1/sites/{site_id}/ownership/transfer |
POST | { "user_id": ... } |
Start a two-phase ownership transfer |
/api/v1/sites/{site_id}/claim-token/rotate |
POST | — | Owner rotates their own claim 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 the admin and manager operations locally
(cumments sites add-admin / remove-admin, add-manager /
remove-manager) and can start a transfer with cumments sites transfer-owner:
add-* stores a pending claim and prints the verify_token, while
remove-* revokes a pending claim. The CLI never writes power levels, so an
already-applied role must be removed from a Matrix client or the Operator
API (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/transfer. 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.