Matrix Integration Topology & Gaps
This page captures an independent architecture review of the Drupal–Matrix chat
integration (workstream lead: John Locke — see Matrix for module
status). It’s derived from reading the entity-relationship
diagram already on this site plus the upstream
matrix_api module’s own
integration overview.
It is commentary and analysis, not a spec — treat it as a reference for
questions to raise with the workstream lead, not as a description of agreed
scope.
Topology
Section titled “Topology”flowchart TB
subgraph matrix["Matrix side (self-hosted, admin-controlled)"]
client["Matrix clients"]
synapse[("Synapse homeserver<br/>own Postgres DB<br/>full event history")]
client -->|"Client-Server API"| synapse
end
subgraph drupalside["Drupal side"]
as["matrix_api_application_server<br/>(AS endpoints, txn dedup)"]
sub["matrix_api_entity<br/>MatrixEventSubscriber"]
filter{"Thread reply to a<br/>known Drupal node?"}
inbound["InboundSyncService"]
outbound["OutboundSyncService"]
eca["ECA events / conditions / actions"]
idmod["matrix_api_identity"]
idp[("External IDP<br/>Keycloak / Simple_OAuth")]
db[("Drupal database<br/>matrix_event_map<br/>matrix_identity_map<br/>comments / nodes")]
drop(["Dropped —<br/>never persisted to Drupal"])
end
synapse -->|"PUT /_matrix/app/v1/transactions/{txnId}<br/>(synchronous, one request per transaction)"| as
as --> sub
sub --> filter
filter -->|"No"| drop
filter -->|"Yes"| inbound
inbound -->|"writes"| db
sub -.->|"fires event"| eca
db -.->|"hook_node_insert/update,<br/>hook_comment_insert"| outbound
outbound -->|"sendMessage / sendThreadReply / redactEvent"| synapse
idmod <-->|"account linking"| idp
idmod -->|"writes"| db
The full Matrix event history lives in Synapse’s own Postgres database — that’s
by design (the ERD’s implementation-status table marks MATRIX_EVENT and
MATRIX_USER “External,” not Drupal-stored). Only events that resolve to a
thread reply under a Drupal node pass MatrixEventSubscriber’s filter and get
written into Drupal (matrix_event_map, plus a real Drupal comment entity).
Everything else — ambient chatter, DMs, any room with no Drupal-side content
anchor — is inspected and discarded in the same request; it never touches the
Drupal database.
What’s genuinely novel here
Section titled “What’s genuinely novel here”Stripping out what’s just the Matrix protocol doing its job (federation, spaces, threading, room versioning, Synapse itself — all vendored, not invented), the actual original contribution is the Drupal-to-Matrix mapping layer:
- An Application Service implemented inside the CMS, not as a standalone
bridge. Most Matrix integrations (Slack, IRC bridges) run as separate
microservices using
matrix-appservice-bridgein Go/Python/Node.matrix_api_application_serverembeds the AS directly in Drupal/PHP — an unusual, CMS-native architectural bet. - Tiered inbound transports for varying levels of homeserver access.
ApplicationServer/CronPoller/DrushDaemondegrade gracefully depending on whether you administer the Synapse instance or only hold a bot account on someone else’s. Most bridges assume AS registration rights and skip the fallback tiers entirely. - Two-way, thread-preserving content sync mapped onto Drupal’s actual
entity model.
m.replaceto update a message while preserving the originalevent_idas thread root;m.thread/m.in_reply_tofor comment replies; the polymorphicmatrix_event_mapindex (entity_type + entity_id → event_id + room_id) resolving both directions. Generic bridges post one-way status updates; this is round-trip sync shaped specifically to Drupal’s node/comment hierarchy. - ECA exposure — business rules as configuration, not PHP. Matrix events and conditions (“is this a thread reply?”, “is the sender linked to a Drupal account?”) become ECA primitives, so site builders wire behavior without code. This only makes sense inside the Drupal ecosystem.
- Group/Space hierarchy mapping. Drupal Groups → Matrix Spaces → Rooms,
keyed off a generic
drupal_entity_type/drupal_entity_idpolymorphic attachment, so a Space or Room can represent a Group, node, or taxonomy term without coupling to a specific implementation.
What’s not novel: Synapse, the AS spec, m.space.child nesting, room v12
restricted joins, OIDC account linking as a Matrix capability — all
pre-existing protocol/homeserver features being consumed as-is.
What’s missing or unresolved
Section titled “What’s missing or unresolved”- The cited implementation-details spec doesn’t exist.
integration-overview.mdpoints to/docs/superpowers/specs/2026-06-18-matrix-drupal-integration-design.mdfor implementation details. That file is not in thematrix_apirepo’s3.0.xtree — only an unrelated Key-module-integration spec exists there. The doc that would answer the harder architecture questions is currently a dead link. - No write-behind / queueing for inbound events. Synapse pushes an AS
transaction and
matrix_api_application_server“dispatches each event synchronously” (the overview doc’s own words) — inline, in the same request/response Synapse is waiting on, with no Drupal Queue API item and no parallel drain workers. The de facto “queue” is Synapse’s own retry/backoff on a slow or erroring AS endpoint, which is the opposite of a queue that drains quickly once it engages. matrix_event_map.event_idis only indexed, not unique. Loop-prevention depends on this; under concurrent or retried transactions (which Matrix federation does routinely), a non-unique index allows a race to create two map rows for the same event.- Cross-entity-type uniqueness has no DB-level enforcement.
room_idmust be globally unique across bothMATRIX_ROOMandMATRIX_SPACE(spaces are rooms too), but these are two separate Drupal entity types — Drupal has no native cross-table unique constraint for this, so it needs an explicit app-level validation check that isn’t yet specified. PROCESSED_TRANSACTION’s expiry isn’t confirmed to be reaped. The dedup table has anexpires_atcolumn, which is the right shape to bound its growth — but a TTL column with no cron/queue worker actually deleting expired rows is equivalent to no TTL at all.- No homeserver capacity plan for concurrent scale. Neither doc names a homeserver implementation choice, version, worker/sharding topology, or database tuning for a given concurrent-user target. Synapse at real scale needs horizontally-scaled worker mode plus a tuned Postgres — that’s out of scope for both docs as they stand.
- Drupal is not a mirror of Matrix activity. By design, only the content-linked subset (thread roots/replies matched to existing Drupal nodes) ever reaches the Drupal database. If any consuming service expects Drupal’s database to hold a queryable copy of all Matrix activity, that is not what this architecture provides — worth confirming as a scoping question, not assuming as a gap to fix.
These are open questions to raise with the workstream lead, not defects in
what’s shipped so far — the modules implemented to date (matrix_event_map,
matrix_identity_map, declared Matrix ID, application-service settings,
PROCESSED_TRANSACTION) match what the entity-relationship
diagram describes as “Implemented.”
