Skip to main content

Module store

Module store 

Source
Expand description

Matrix-shaped event store — rooms/events/state/members/relations/ receipts/account-data/txn-dedup/filters half of messenger.db (the devices/keys/backup half is matrix_keys_store.rs, a separate work item). See the messenger protocol notes §2 for the full DDL this module implements (the “Manager decisions on this plan” section at the top of that file overrides the body — this module follows those corrections, noted inline where they apply) and §1 for the id-format rules.

This server adopts Matrix’s own event envelope, state-resolution model, and CS API shape for everything except federation and /login — CS API v1.19, room version 11. Crypto stays entirely client-side: every content blob this module stores is opaque JSON it never inspects except for m.relates_to (relation bookkeeping, kept in cleartext at the top level of content even for m.room.encrypted, per the Matrix spec’s own accepted trade-off — plan §10 item 3) and the redaction allow-list (§2’s redact_event, which strips by event_type, never by interpreting the payload’s meaning).

§Single-writer discipline

Every function here takes an already-open Connection; the caller (state.rs, from P14 onward) holds it behind one std::sync::Mutex, the same discipline db/social_db already use — this module never locks anything itself and never calls std::sync::Mutex internally. That single-writer guarantee is what makes [next_stream_id] safe: it is always called inside the same transaction as the row it stamps, and there is never a second writer racing it.

§Cross-database rule

This module stores user_id INTEGER. The nick lives on messenger_sessions, which crate::nick reads and writes. The matrix_users.nick column is left in place and is not the source of truth. There is no identity database. matrix_users maps user_id to its Matrix id (mxid) once: public_id is immutable, so the mapping does not change.

The one place a label does end up in this database is the displayname field of an m.room.member event’s content — the Matrix convention, and what a client names a DM and lists members by. Callers stamp the identity database’s effective label into every join/invite member event they write, and refresh_member_displayname re-stamps it into the user’s existing member events when the label changes.

Structs§

AccountDataRow
DisplaynameRefresh
What one refresh_member_displayname pass did.
DmAdoption
What adopt_dm_room_for_legacy needs to bind a native DM room to a legacy conversation, grouped for the same lint-clean reason RoomBootstrap’s doc states.
DmMigrationCounts
What one migrate_dm_conversation call actually wrote.
DmMigrationExtras
Everything migrate_dm_conversation needs beyond the room bootstrap itself, grouped into one value for the same “stays lint-clean without #[allow(clippy::too_many_arguments)]” reason RoomBootstrap’s own doc comment states.
LegacyDmDirectHint
One m.direct merge hint (plan P12 scope: “merge into their existing m.direct, do not overwrite other entries”) — user_id’s global m.direct account data gains peer_mxid mapped to the migrated room id, alongside whatever entries it already has.
LegacyDmMessageImport
One legacy dm_messages row queued for import (plan §7 step 5). event_id is minted by the caller ([crate::matrix_migration] in the binary crate) so it can be recorded in the caller’s own bookkeeping (the idempotency proof compares event ids across two runs) without a second round trip back into this module. content is already the full org.example.legacy_dm JSON body ({"legacy_message_id","legacy_conversation_id","nonce_b64","ciphertext_b64"}).
LegacyDmReadReceipt
One reader’s m.read receipt to set once its target message has been imported — up_to_legacy_message_id is the LAST dm_messages.id this reader actually had read_at stamped for in social.db (never a later one — plan P12 scope: “never over-count as read”). ts_ms is that same message’s own read_at, converted to epoch milliseconds.
MatrixEvent
NewStateEvent
One state event queued for create_room_with_state — the same shape apply_state_event takes per-field, minus room_id/origin_server_ts/ now (shared by the whole batch, passed once to create_room_with_state itself rather than repeated per event).
PublicRoomSummary
One row of GET /publicRooms’s chunk — a public-join_rule room’s directory summary. name/topic are None when the room never had an m.room.name/m.room.topic state event (plan §5: only a channel-kind room is ever join_rule = 'public', but this reads the column directly rather than also filtering on kind, so it stays correct even if a future piece ever makes a group room public).
ReceiptRow
Redaction
The redaction redact_event_marked writes: which event, in which room, by whom, under which new event id.
Room
RoomBootstrap
The room-metadata half of create_room_with_state’s input — every field [insert_room_row] needs, grouped into one value instead of nine positional arguments (this function’s own params stay lint-clean without an #[allow(clippy::too_many_arguments)], unlike the legacy single-row create_room/[insert_room_row] this batch shares its INSERT with).
RoomMember
StateEventWrite
One state event for apply_state_event: its identity, the state slot it fills, its content, and the two clocks (origin_server_ts in milliseconds for the event row, now as RFC 3339 for the membership cache’s updated_at).

Enums§

DedupedWrite
What insert_timeline_event_deduped/redact_event_deduped found: a brand-new write, or the SAME event a prior submission of this (user, device, txn_id) already produced (plan §4 send/redact rows: “idempotent per (user, device, txn_id): a repeat returns the SAME event_id with no second insert”).
HistoryVisibility
HistoryWindow
How much of a room’s timeline a caller may read, per visible_upper_bound. UpTo is INCLUSIVE (a caller who left the room still sees the m.room.member leave event itself, since that is the last event they witnessed).
JoinRule
MatrixIdError
Why parsing an mxid failed.
MatrixStoreError
Why a write into this store was refused, on top of a real database failure — same shape as dm_db::MessageError/IdentityKeyError.
Membership
PowerAction
An action m.room.power_levels gates by a named threshold field, plus StateDefault for “may send an otherwise-unlisted state event type”.
ReceiptType
RoomKind
Our own room-kind label (rooms.kind) — drives the power-level/history- visibility defaults of plan §5. Not a Matrix wire field.
TxnDedupEntry
What txn_dedup_lookup found for a given (user_id, device_id, txn_id).

Constants§

GLOBAL_ACCOUNT_DATA_ROOM
room_id value meaning “global account data” — see create_matrix_schema’s doc comment on why this is "", never NULL.
MATRIX_EVENT_CONTENT_MAX_BYTES
Ceiling on the serialized content of any state/timeline event this server accepts (plan §3.8, mirroring dm_db::DM_MAX_CIPHERTEXT_BYTES) — checked by every route that takes client-supplied event content (routes::matrix::rooms::put_state first; messaging/to-device pieces reuse the same constant rather than minting their own).
MATRIX_ROOM_VERSION
Room version pinned for every room this server creates (plan §1) — v11 still lists the creator explicitly in m.room.power_levels.users, unlike v12’s “infinite implicit power” simplification, which this plan does not adopt.
RESERVED_LOCALPART_PREFIX
Localpart prefix reserved for a future Application Service (bridge) namespace (plan §10 item 6). matrix_store’s own id generators never mint a localpart starting with this — see ensure_matrix_user — so a later AS registration slots in without a retroactive id-collision audit.

Functions§

account_data_since
Every (user_id, room_id) account-data row changed strictly after since_stream — the /sync delta shape for both global and per-room account data (caller passes GLOBAL_ACCOUNT_DATA_ROOM or a real room id).
adopt_dm_room_for_legacy
Bind an EXISTING native DM room to legacy conversation adoption.legacy_dm_id, instead of minting a second room for the same pair (rooms.dm_pair_key is UNIQUE, so a second insert is refused): sets rooms.legacy_dm_id, writes any missing legacy key state event, then imports messages and receipts through the shared catch-up body ([catch_up_dm_in_tx]) — all in ONE transaction, so a failure leaves the room exactly as it was. messages follow catch_up_dm_conversation’s contract (not yet in legacy_dm_message_map, ascending by legacy_message_id).
apply_state_event
Insert one state event and refresh the two projections that read off it: current_state (always) and, for m.room.member/m.room.power_levels, the denormalized room_members cache (plan §2). All inside one transaction with the event row.
can
Whether mxid’s user_level reaches the threshold action names (falling back to the Matrix default when the corresponding field is missing — see PowerAction::field_and_default).
can_act_on
Matrix’s room-v11 membership-change authorization rule for acting on ANOTHER user (manager review, 2026-09-24 — a real gap in the first cut of can alone): reaching the flat action threshold via can is necessary but not sufficient — the sender’s own level must ALSO be STRICTLY GREATER than the target’s current level, so a level-50 admin can never kick/ban/unban a level-100 owner or an equal-level peer, even though 50 reaches the flat kick/ban threshold. self_leave is the one exception the spec carves out: a member may always change their OWN membership to leave (declining an invite, or a self-kick, is just a leave) regardless of level — pass true only when new_membership for this call is leave and let the identity check below decide whether it actually applies.
catch_up_dm_conversation
A catch-up pass for an ALREADY migrated room (P15) — the binary crate’s own matrix_migration module calls this both from a live legacy DM send’s bridge (routes::dm::create_message, right after its own insert commits) and from a boot pass over a conversation room_by_legacy_dm_id already finds a room for. messages (already filtered by the caller to exclude anything already in legacy_dm_message_map, ordered ascending by legacy_message_id) lands as new org.example.legacy_dm timeline events plus their legacy_dm_message_map rows; then each of receipts advances that reader’s m.read receipt — never moved backwards ([upsert_receipt_in_tx]’s own guarantee, the SAME one a live /receipt call gets) — all in ONE transaction. Reuses the exact per-event helpers migrate_dm_conversation itself uses, so a caught-up room’s rows are indistinguishable from ones a fresh migration (or a live Matrix send) would have produced.
clear_dm_pair_key
Free room_id’s dm_pair_key slot (set it to NULL) — called when a prior DM between the same pair is no longer live (one party left), so a freshly created room between the same two users can claim that pair key without violating rooms.dm_pair_key’s UNIQUE constraint. The old room keeps every other field; only its claim on the pair key is released.
create_filter
Store an opaque Filter JSON object, returning its new filter_id.
create_matrix_schema
Create every table/index this module needs — idempotent (IF NOT EXISTS/INSERT OR IGNORE throughout), so it runs on every boot. Called from [init_messenger_db] and directly by this module’s own tests against a plain in-memory connection (no SQLCipher key needed for schema creation) — same split social_db::create_social_schema uses.
create_room
Insert a room’s metadata row only — no bootstrap state events (those go through apply_state_event separately, e.g. m.room.create, m.room.member, m.room.power_levels, per plan §5/§7). Kept as a standalone single-row entry point for a future non-transactional caller and for this module’s own tests; create_room_with_state is the P5 batch entry point everything under routes::matrix::rooms uses instead.
create_room_with_state
Create a room AND its full bootstrap event set in ONE transaction (plan P5 correction) — the room row ([insert_room_row]), then every entry of state_events in order via [apply_state_event_in_tx] (so, e.g., the creator’s own m.room.member MUST precede m.room.power_levels in state_events for [refresh_power_levels] to see them as an existing member to stamp — see that function’s own doc comment), then commit. A failure on the room insert OR on any one state event leaves NEITHER a room row NOR any event row behind (rusqlite rolls back a Transaction dropped without commit(), the same guarantee [populate_relations]’s duplicate-annotation refusal already relies on).
current_state_all
current_state_event
ensure_matrix_user
Register user_id’s mxid on first touch (idempotent — a later call for the same user_id is a no-op) and return it. Refuses to mint a reserved localpart.
ensure_schema
Creates every messenger table that does not exist yet (idempotent).
event_level
The power level required to send an event of event_type: power_levels.events[event_type] if present, else state_default (Matrix default 50) for a state event or events_default (Matrix default 0) for a timeline event.
events_in_room_after
The oldest-first timeline window strictly after since_stream, capped at limit — the incremental-/sync and forward-/messages shape.
events_in_room_before
The newest-first page strictly before before_stream, capped at limit — GET /messages?dir=b and the initial-/sync “newest N” window (§3.2: the caller reverses it if an ascending page is needed).
forget_membership
Delete user_id’s room_members row in room_id, but ONLY if its current membership is leave — POST /rooms/{roomId}/forget’s own gate AND its effect are the same one-row conditional delete, so there is no separate read-then-write race to worry about. Returns the number of rows deleted (0 if the row was absent or not currently leave), so the route layer can tell a genuine forget from a no-op gate refusal.
get_account_data
get_event
get_filter
Fetch user_id’s own filter_id’s definition — scoped to user_id so one account can never read another’s stored filter by guessing an id.
get_receipt
get_room
highest_mapped_legacy_message_id
The highest legacy_message_id already imported into room_id’s org.example.legacy_dm timeline (P15: matrix_migration’s own catch-up cursor). legacy_dm_message_map carries no room_id of its own — dm_messages.id is a single autoincrement column shared by every legacy conversation, not scoped per conversation — so this joins through events to find only the rows imported into THIS room. Message ids are ascending within one conversation and every migration/catch-up pass imports every not-yet-mapped row up to “now”, so id > this is exactly that conversation’s not-yet-imported set — the caller’s own cheap alternative to re-checking legacy_dm_message_event_id once per message. None for a room with no legacy message imported yet (a conversation that had zero messages at migration time).
insert_legacy_dm_message_map
insert_timeline_event
Insert one timeline event (state_key always NULL) inside its own transaction: mint the next stream id, insert the row, and — if content carries a top-level m.relates_to (cleartext even for m.room.encrypted, plan §10 item 3) — populate [relations]. Refusing a duplicate m.annotation rolls back the whole transaction, so a refused call leaves no partial events row. It never records a txn_id: a write that carries one goes through insert_timeline_event_deduped, which stores it next to its dedup record in the same transaction.
insert_timeline_event_deduped
insert_timeline_event, but dedup-checked and recorded in the SAME transaction as the insert (P6 binding rule) — a repeat of (sender_user_id, device_id, txn_id) returns the ORIGINAL event without a second events row, and this is race-free even under a hypothetical second writer (never actually possible under this module’s single-writer discipline) because the lookup, the insert, and the dedup record all commit or roll back together. txn_dedup_lookup/txn_dedup_record take &Connection; passing &tx (a Transaction) works via Deref — see apply_state_event_in_tx’s own sibling functions for the same pattern.
is_local_server_name
Whether name is the minted server name or an enabled local alias.
is_reserved_localpart
True if localpart is reserved for the future Application Service namespace (plan §10 item 6) and must never be minted as a real user’s mxid localpart.
legacy_dm_message_event_id
matrix_server_name
Homeserver name used when minting and parsing local ids.
matrix_user_ids
Every user_id that has a [matrix_users] row — the boot backfill’s worklist of users whose member events may predate the displayname stamping.
max_stream_id
The last stream id issued to any table — the counter’s current value. Since every stream-ordered table shares this one counter, this is also the highest stream_id that exists anywhere in the database right now.
member_state_changed_in_window
Every m.room.member state event whose CURRENT value as of upto_inclusive was itself set within (since_exclusive, upto_inclusive] — routes::matrix::sync’s §3.3 clause-(b) lazy-load “gap rule” (matrix-spec#942): a member who joined/left/changed profile inside a truncated timeline gap must still be reported even when they never sent anything into the visible window. Bounded to upto_inclusive throughout (never the LIVE current_state projection, which could have moved past a /sync build’s own snapshot) so this stays part of one consistent cut.
member_state_keys_in_window
Every mxid (state key) that has an m.room.member event in room_id with stream_id in (from_exclusive, to_inclusive] — the candidates whose membership MAY have changed inside the window; the caller compares room_member against membership_at to find the real transitions.
membership_at
mxid’s membership in room_id as of at_stream_id: the latest m.room.member event for that state key with stream_id <= at_stream_id, or None when there was none yet (or its content carries no known membership). The point-in-time counterpart of room_member — the device-list delta needs “what was this user’s membership BEFORE the sync window opened” to tell a newly shared room from a profile update.
messenger_db_config
The SQLCipher store configuration for path and a hex key (see parse_db_key).
migrate_dm_conversation
The P12 migration’s own batch entry point: bootstrap’s room row, then every entry of state_events (identical shape to create_room_with_state’s own loop — same order requirement: a member’s m.room.member must precede m.room.power_levels for [refresh_power_levels] to see them, see that function’s own doc), then every extras.messages entry as an org.example.legacy_dm timeline event plus its legacy_dm_message_map row, then extras.receipts, then extras.direct_hints — all in ONE transaction. extras.messages must already be filtered by the caller to exclude any legacy_message_id already present in legacy_dm_message_map and ordered ascending by legacy_message_id; this function does not re-check either, since the binary crate’s own matrix_migration module already established the room does not exist yet via room_by_legacy_dm_id before ever calling this.
mxid_for_public_id
Format public_id (the identity database’s immutable users.public_id) as this server’s own mxid: @<public_id>:example.org.
mxid_of
user_id’s mxid, if ensure_matrix_user has ever run for it.
new_event_id
Mint a fresh event id: "$" + 22 random URL-safe-base64 chars — opaque, no reference-hash content addressing (no federation to need it).
new_room_id
Mint a fresh room id: "!" + 22 random URL-safe-base64 chars + ":example.org".
non_member_state_changed_in_window
Every state event type OTHER than m.room.member whose CURRENT value as of upto_inclusive was itself set within (since_exclusive, upto_inclusive] — routes::matrix::sync’s per-room state delta for every state type this server does not lazy-load (“every OTHER state-event type changed since since … always included in full”). since_exclusive = 0 naturally reproduces every current state event of these types (the initial-sync/full_state case), since every such event’s own stream_id is > 0 — one code path serves both.
notification_count
unread_notifications.notification_count for user_id in room (plan §3.5): the count of message-like timeline events (see [NOTIFICATION_MESSAGE_TYPES_SQL]) strictly after the FURTHER-AHEAD of the caller’s own m.read/m.read.private receipt — resolved by the target events’ own stream_id (timeline position), never by a receipt row’s own stream_id column (a different axis — see upsert_receipt’s own doc) — and bounded above by visible_upper_bound (a leave/ ban’d member’s count never includes anything past their own departure). Excludes the caller’s own sends (nobody is notified about their own message) and, being restricted to [NOTIFICATION_MESSAGE_TYPES_SQL], every state event and m.reaction. Not a member of the room at all (HistoryWindow::Nothing) is always 0 — there is nothing this caller could be notified about.
open_messenger_db
Opens the messenger store (tesserax-store: SQLCipher, WAL, one writer) and makes sure every table exists. The key is applied first on every connection the engine opens.
open_read_pool
Parallel read-only connections (same file, same key) for the store opened by open_messenger_db.
parse_db_key
Parses the 32-byte database key given as 64 hex characters (the raw SQLCipher key).
private_room_messages_sent_since
Timeline (non-state, non-redaction) events sender_user_id has sent into any PRIVATE room (join_rule = 'invite') with origin_server_ts >= since_ms — routes::matrix::messaging’s per-sender daily send-rate counter (plan §3.8: “mirroring DM_MESSAGE_DAILY_LIMIT… own copy of the constant” — the constant itself lives in the route module per this codebase’s convention; this function is only the count query, since raw SQL against events/rooms stays inside this module). State events (state_key IS NOT NULL, sent via /state) and redactions are excluded — this counts actual message-shaped sends, not every write a sender makes.
public_id_from_mxid
Parse @localpart:server_name, returning the localpart — refusing anything not addressed to matrix_server_name() (plan §1: no federation, so a foreign-server mxid can never be a real local user).
public_rooms_page
One page of the public-room directory, ordered by rooms.id — a stable pagination key (a room id never changes once minted). after_room_id is the previous page’s own last room id (None for the first page); search_term, when non-empty, keeps only rooms whose current m.room.name contains it (case-insensitive substring — Matrix’s own generic_search_term).
receipts_changed_in_room
Every receipt row in room_id whose own write-order stream_id (NOT the TARGET event’s position — see upsert_receipt’s own doc on that distinction) falls in (since_exclusive, upto_inclusive] — routes::matrix::sync’s per-room m.receipt ephemeral delta (plan §3.6).
redact_event
redact_event_deduped
redact_event, but dedup-checked and recorded in the SAME transaction as the redaction (P6 binding rule, mirroring insert_timeline_event_deduped — see its own doc for the race-freedom argument).
redact_event_marked
redact_event, but merges extra_content into the resulting m.room.redaction event’s own content, in the SAME transaction as the redaction itself — routes::matrix::moderation’s (P11) ONLY caller, which stamps {"org.example.site_moderation": true} on a site-moderator’s redaction of a public-room message (plan §4’s /api/matrix-admin/rooms/{roomId}/moderate row) so every client can render it distinctly from an ordinary member-initiated redaction. Every other redaction path (redact_event, redact_event_deduped) never needs this and stays untouched.
refresh_member_displayname
Re-stamp displayname into user_id‘s own m.room.member event in every room where they are currently joined or invited, so the other members’ clients see the new name. A room whose current member event already carries exactly displayname is skipped (the pass is idempotent, which is what lets the boot backfill call it for every user on every start). The new event keeps the current content (an invite keeps its is_direct marker) and only replaces displayname; a join refresh is sent by the user, an invite refresh keeps the inviter as its sender — stripped_invite_state reads the inviter off that field. leave/ban rows are never touched.
relations_of
Every event related to target_event_id, newest-first (Matrix’s own default order for GET /relations), optionally narrowed to one rel_type and/or one event_type, strictly before before_stream (pass i64::MAX for a first page), capped at limit — P6’s routes::matrix::messaging::get_relations.
room_by_dm_pair_key
The room currently holding pair_key as its dm_pair_key, if any — the binary crate’s routes::matrix::rooms::create_room’s DM-reuse lookup (plan P5 correction: “a second DM between the same pair returns the existing room id only if it is still a live DM for both”).
room_by_legacy_dm_id
The room already migrated from dm_conversations.id = legacy_dm_id, if any — the binary crate’s own matrix_migration module’s idempotency gate (plan §7 step 2: “For each dm_conversations row without a rooms.legacy_dm_id match”).
room_heroes
Up to limit other members (join/invite) of room_id, oldest membership-change first, excluding exclude_user_id — /sync’s m.heroes (plan §3.4).
room_member
(room_id, user_id)’s single room_members row, if any — the targeted counterpart of room_members for a “is this one user a member, and what membership/power level do they hold” check (every gate in routes::matrix::rooms needs exactly this, not a full-room scan).
room_members
Every member row of room_id, optionally filtered to one membership.
rooms_changed_in_window
Every room in room_ids (typically the caller’s currently joined rooms) with at least one events, receipts, or room-scoped account_data row whose stream_id falls in (since_exclusive, upto_inclusive] — routes::matrix::sync’s changed-room prefilter: an incremental sync must not run a room block builder’s own dozen-odd queries against EVERY joined room just to discover that most did not change (with a few hundred rooms that is thousands of queries per wake, all held under the single messenger.db mutex). This set is EXACT, not merely a safe over-approximation: every field a room’s /sync block can ever report (timeline, state, per-room account_data, and m.receipt) derives from exactly these three tables — the one exception, typing, is an in-memory crate::typing::TypingRegistry fact this function has no visibility into and the caller checks separately. Returns an empty set without querying when room_ids is empty.
rooms_for_user
Every room id user_id has a room_members row in, optionally filtered to one membership — the “which rooms is this user in” query behind /sync’s room list and on_credential_revoked’s wake fan-out.
rooms_with_member_events_in_window
Every room in room_ids that has at least one m.room.member event with stream_id in (from_exclusive, to_inclusive] — ONE indexed query for the whole room set, so the device-list delta only does per-room work for rooms where a membership could have changed (an incremental /sync runs it on every wake; see rooms_changed_in_window for the same rule). Returns an empty vec without querying when room_ids is empty.
set_local_aliases
Local DNS aliases of the one homeserver are a server-name CHECK only: an mxid addressed to an enabled alias resolves to the same local account. No second homeserver exists and ids are always minted with matrix_server_name(). Off by default.
set_matrix_server_name
Set the homeserver name once, before any mxid or room id is minted. The default until this runs is example.org.
state_events_of_type_at
The point-in-time projection of every event_type state event as of at_stream_id — the latest such event per state_key with stream_id <= at_stream_id — for GET /rooms/{roomId}/members?at= (plan §3.3: a client fetching the full member list as of a given /sync token, rather than the live current_state_all projection). A state_key whose first event lands AFTER at_stream_id is correctly absent (it did not exist yet at that point in the room’s history).
stripped_invite_state
The stripped state Matrix attaches as unsigned.invite_room_state on an invite’s own m.room.member event (name/join_rules/encryption/create, plus the inviter’s own membership event) — recomputed fresh every time it is served, NEVER stored on the member event itself: unsigned is not part of the canonical, signed event, and this server’s events.content column holds only the canonical, signed shape. Computing it at read time also means it always reflects the room’s CURRENT state (e.g. a rename after the invite was sent), which is what a client opening an invite it received a while ago actually wants to see. inviter_user_id is the member event’s own sender_user_id — the caller already has it.
stripped_state_json
One {content, state_key, type, sender} Matrix StrippedStateEvent — sender already resolved to an mxid, content already parsed JSON. pub (beyond this module’s own stripped_invite_state) for the binary crate’s routes::matrix::sync module: GET /sync’s invite_state.events needs the SAME stripped shape for the invitee’s OWN m.room.member event, which stripped_invite_state does not itself include (see that function’s own doc — it strips the INVITER’s state, not the invitee’s own invite event, since its other call site, routes::matrix::client_event_json’s unsigned.invite_room_state, is already attaching it TO that very event).
txn_dedup_count_since
How many (user_id, device_id) sends/redacts were recorded since since (an RFC-3339 timestamp, compared as a plain string against created_at values this server ALWAYS writes in that same shape — safe without the datetime() normalization lookup_web_credential’s own doc warns about, which is specifically about comparing against SQLite’s own datetime() output, a different textual shape) — routes::matrix::messaging’s short-window burst-rate counter (plan §3.8: “20 events / 10s per device”). Reads txn_dedup, the one table that already carries a per-DEVICE identity for a write (events itself has no device_id column).
txn_dedup_lookup
Check whether (user_id, device_id, txn_id) was already handled, without recording anything.
txn_dedup_record
Record that (user_id, device_id, txn_id) has now been handled, producing event_id (or None for a to-device send). Call only after txn_dedup_lookup returned TxnDedupEntry::NotSeen — this does not itself check for a race, per this module’s single-writer discipline.
txn_id_for_event
The txn_id (user_id, device_id) used to produce event_id, if any — client_event_json’s unsigned.transaction_id, which Matrix reveals ONLY to the same device that sent the event, never to any other viewer (including the sender’s OTHER devices). A miss (None) is the overwhelmingly common case (every event not authored by this exact device on this exact send) and costs a single indexed txn_dedup primary-key lookup.
upsert_account_data
Upsert one account-data entry. Pass GLOBAL_ACCOUNT_DATA_ROOM for global data (m.direct, m.push_rules, …); a real room id for per-room data.
upsert_receipt
Upsert user_id‘s receipt_type receipt in room_id, refusing to move it BACKWARDS: compared by the TARGET events’ own stream_id (timeline position), never by the receipt row’s own stream_id column (which orders receipt writes for /sync’s ephemeral-since-last-batch filter, a different axis). Returns the resulting stream_id stamped on the row — the existing one, unchanged, on a refused backward move.
user_id_of
The internal user_id behind mxid, if known.
user_ids_with_leave_transition_in_rooms
Every user_id whose m.room.member state transitioned to leave or ban in one of room_ids, with stream_id in (from_exclusive, to_inclusive] — routes::matrix::keys’s GET /keys/changes device_lists.left set (plan §3.7): a user who no longer shares any room with the caller. This function only finds the CANDIDATE departures inside the given room set; the caller (which already knows its own current shared-room membership) is responsible for the second half of the plan’s rule — excluding anyone who still shares SOME OTHER room with it today. Bounded to room_ids (typically every room the caller is or was in) rather than scanning every room this server has ever created. Returns an empty vec without querying when room_ids is empty.
user_level
power_levels.users[mxid], falling back to users_default (Matrix default 0) when either field is missing.
validate_power_levels_change
The m.room.power_levels v11 change-authorization rules beyond the flat PUT state PowerCheck already gating who may send this event type at all (manager review, 2026-09-24 — a real gap: without this, any admin who merely reaches state_default could PUT themselves straight to 100). sender_mxid’s level is read from old (their authority BEFORE this change takes effect):
visible_upper_bound
The history-visibility rule every timeline-reading route enforces (routes::matrix::messaging’s /messages, /event, /relations; P10’s /sync reuses this too):