.NET / SQL / Enterprise Engineering
Hierarchical Meeting Rooms and Agent Routing for Enterprise MATM
Report summary
The best fit for this MATM requirement is a hybrid architecture : a first-class Room Service backed by a relational system of record, a relationship-based authorization graph, an append-only transcript/event stream, and a single-writer room coordinator per durable room or routing subject. That recom
Key topics
- .NET / SQL / Enterprise Engineering
- .NET
- SQL
- Enterprise Engineering
- AI
- Semantic Systems
- Research Archive
- Strategy
- Audit
Research provenance
For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.
Source availability: 31 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.
This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.
Full report
On this page
Recommended architecture
The best fit for this MATM requirement is a hybrid architecture: a first-class Room Service backed by a relational system of record, a relationship-based authorization graph, an append-only transcript/event stream, and a single-writer room coordinator per durable room or routing subject. That recommendation is grounded in several mature patterns: actor systems provide stable, isolated state and one-writer coordination; durable workflow engines provide resumable onboarding and routing; chat protocols provide room history, edits, receipts, and pagination; and collaboration platforms demonstrate that hierarchy, discoverability, and content restrictions must be modeled explicitly rather than improvised in message bodies. Orleans, Dapr, and Akka all emphasize isolated stateful entities and turn-based or single-writer execution; Temporal emphasizes durable messages, updates, and lazy workflow creation; Matrix and XMPP treat rooms, archives, receipts, and corrections as explicit protocol objects; Confluence models hierarchy and restrictions separately from page content.
That leads to a clear subsystem split:
| Subsystem | Responsibility | Why it is separate |
|---|---|---|
| Room Topology Service | room definitions, hierarchy, discoverability, room templates, canonical links | hierarchy and navigation are not transcript concerns |
| Authorization Service | company/workspace/project inheritance, confidential narrowing, service-role exceptions | transcript access must be derived from enterprise auth, not inferred from room membership rows alone |
| Identity Negotiation Service | asks who the agent is, why it is present, and what it is working on; assigns canonical agent identity | the display name shown in a room is not the authentication subject |
| Routing Service | lane selection, coordinator assignment, objective creation, reassignment, conflict resolution | routing decisions are durable control records, not just chat messages |
| Transcript Service | append-only messages, relations, edits, supersessions, redactions, pagination | durable room conversation must remain independent from task/workflow state |
| Cursor and Notification Service | read markers, unread counts, subscriptions, pushes/webhooks | read state is per principal per room, not a property of messages |
| Assignment Service | coordinator ownership, claims, returns, escalations, pauses, completions | work assignment is a lifecycle of its own |
| Evidence and Memory Service | decisions, evidence blobs, provenance, long-term summaries | durable knowledge is company/workspace/project-owned, never user-owned |
The core design principle is that a room is an addressable collaboration object, not a memory record and not a free-form field on another entity. Matrix models room hierarchies, room state, room history, receipts, edits, and redactions as distinct concerns. XMPP MUC likewise models rooms, member lists, archives, nicknames, and room controls separately. Microsoft Teams’ retention model also distinguishes chat visibility from compliance storage, which is another sign that rooms and transcripts deserve their own subsystem boundary.
My recommendation is therefore:
- Use a single relational database as the transactional source of truth for room metadata, routing decisions, assignments, cursors, and transcript rows.
- Use an authorization graph in Zanzibar/OpenFGA style for inheritance and narrowing rules.
- Use a single-writer room actor or grain per room for mutable room state, and a single-writer routing actor/workflow per routing subject to prevent conflicting coordinator decisions.
- Use an outbox/event bus for search indexing, notifications, analytics, and downstream workflow triggers.
- Use a durable workflow for onboarding and complex rerouting, especially when human approval, timeouts, or escalation are involved. Temporal’s Signals, Updates, Queries, and Signal-With-Start are particularly aligned with “always-available welcome room” semantics and resumable routing.
Competing approaches and why the hybrid wins
A pure workflow-engine-centric design is tempting because onboarding and routing are obviously workflows. The problem is that workflow engines are not full collaboration substrates: they do not natively give you room discovery, room-local presence, transcript pagination, read cursors, edits, redactions, or space-like hierarchy. Temporal is excellent for durable message handling, resumable state, child workflows, and escalation control, but it should orchestrate room behavior, not replace room storage. Its own documentation distinguishes asynchronous Signals, synchronous tracked Updates, and child workflow composition, which fits routing and reassignment well, but not room transcript semantics by itself.
A pure chat-protocol-centric design, such as “just use Matrix rooms for everything,” is better than the workflow-only option because Matrix already covers spaces, room history, receipts, edits, relations, redactions, and sync tokens. XMPP MUC plus MAM covers strong room control, archives, read heuristics, and paging. But enterprise MATM has requirements that public chat protocols do not solve end to end: company/workspace/project authorization inheritance, confidential narrowing against tenant objects, machine-routable objectives, coordinator election, structured evidence expectations, and durable assignment/resolution lifecycles require an application-specific domain model on top. In practice, Matrix/XMPP are excellent inspiration and interoperability references, but the room service still needs its own application-level control plane.
A pure actor-system-centric design is the best choice for concurrency control around room state and routing conflicts, but not as the only persistence interface. Akka cluster sharding is strong when you have many durable entities that must run on exactly one node at a time; its persistence model leans on a single-writer principle for a given identity. Orleans provides similar stable identities, persistence, placement, and single-threaded execution defaults; Dapr actors similarly enforce turn-based concurrency and make the timer-versus-reminder distinction explicit. Those are exactly the properties you want for room coordinators and routing subjects. But transcripts, analytics, search, retention reports, and wiki-like navigation still need a relational/query-friendly model.
The memory-record hack should be rejected outright. Mature protocols and platforms separate messages, membership, receipts, archives, edits, and access restrictions because those concerns evolve differently and are governed differently. Teams also demonstrates that compliance retention and end-user visibility cannot be collapsed into “whatever is currently displayed”; that is especially important for temporary goal/task rooms whose decisions and evidence outlive the room’s active phase.
The practical conclusion is:
| Approach | Strength | Weakness | Verdict |
|---|---|---|---|
| Workflow-first | great for onboarding, retries, escalation | poor room/transcript semantics | use as orchestrator only |
| Chat-first | great for transcripts, receipts, edits, pagination | weak app-specific auth/routing/governance | use as design reference, not sole platform |
| Actor-first | excellent conflict control and room-local state | poor analytics/query surface alone | use for coordination layer |
| Hybrid | combines durable workflows, coordination, auth graph, and transcript model | more components | recommended |
Authorization, tenancy, and room hierarchy
The hierarchy should be represented explicitly as:
account membership -> company -> workspace -> project -> room
with accounts and companies many-to-many, and with knowledge owned only by company, workspace, or project. No durable memory artifact should have a user principal as its owner scope. That is both a practical governance rule and a simplifying rule for tenant isolation.
For authorization, a Zanzibar/OpenFGA-style relationship model is the right match because it handles direct and implied relationships, inheritance over object graphs, and conditions on grants. Zanzibar’s published design highlights a uniform access-control model with external consistency under changing ACLs. OpenFGA models authorization as relationship tuples and also supports conditions on tuples, including time-bounded grants. That is a clean fit for company memberships, workspace/project membership inheritance, welcome-room access, confidential-room narrowing, and temporary coordinator grants.
A minimal authorization model should express object relationships such as:
account -> companyas direct membershipworkspace -> parent_companyproject -> parent_workspaceroom -> parent_company | parent_workspace | parent_projectroom -> confidentiality_policyservice_principal -> roleagent_instance -> authenticated_as principal
Then compute permissions such as:
enter_welcome_room(company)if authenticated principal is a company memberdiscover_workspace_rooms(workspace)if principal can view the workspaceenter_project_room(room)if principal inherits from the room’s parent project and room policy does not narrow furtherenter_confidential_room(room)only if principal is explicitly granted or is in an allowed usersetread_room(room)andwrite_room(room)separatelyroute_into(room)as a stronger permission thandiscover(room)
The important isolation rule is rooms can narrow inherited permissions; rooms cannot broaden them across the tenant graph. Atlassian’s Confluence documentation is a useful analogy here: space permissions and content restrictions are distinct, and content restrictions can further restrict access but cannot grant broader access than the container. That same principle should apply here.
For tenant isolation, enforce the following invariants:
| Rule | Required behavior |
|---|---|
| Tenant scoping | every room, transcript row, assignment, cursor, and evidence item carries company_id and a higher-scope foreign key where applicable |
| Auth before existence leak | signed-out or unauthorized callers receive uniform not-found/forbidden behavior for room URLs, transcript fetches, search, and membership probes |
| No cross-tenant backlinks | relationship links may only reference rooms and artifacts in the same company unless a special federated-sharing feature exists |
| Confidential narrowing | confidential room memberships are computed from parent scope plus explicit allowlists/usersets; never from display names or message mentions |
| No transcript inheritance | authorization inheritance applies to room access, not to transcript copying; moving an agent between rooms does not automatically expose prior transcripts |
| Search isolation | indexing pipeline must partition indices by company and apply room ACL filters at query time |
| Storage isolation | encryption keys, object-store prefixes, and export jobs must be company-scoped |
The always-available company welcome room should be a pre-provisioned durable room under each company scope. Every authenticated agent with company membership can enter it, but entering that welcome room should not imply access to any workspace or project transcript. The welcome room is there to negotiate identity and routing, not to act as a “catch-all visibility escalation.”
Identity negotiation and agent spoofing prevention
Identity negotiation must separate at least four things:
| Concept | Meaning |
|---|---|
principal_id | the authenticated subject from your identity provider |
agent_type | the software/role class the principal is allowed to run |
agent_instance_id | the concrete running session or process |
display_name | the human-readable room label, negotiated and conflict-resolved |
The system should ask, in the welcome room workflow:
- Who are you?
- Why are you present?
- What are you working on?
- Which company/workspace/project context do you believe applies?
That answer becomes a claim, not truth. The policy engine validates it against the authenticated principal, company membership, and any allowed agent profiles, then issues a canonical identity record and a route decision.
Display-name conflicts should be resolved exactly once during negotiation. XMPP MUC is instructive here: room nicknames are treated as explicit managed state, some rooms require a reserved nickname, and anonymized rooms still need stable occupant identity to prevent impersonation. XEP-0421 is especially relevant because it introduces stable, anonymous occupant identifiers to prevent impersonation across reconnects and renames. The direct lesson for MATM is that the display label must never be the security identity. The sender identity must be server-minted, stable, and opaque; the display name is merely a presentation field.
So each message should store immutable sender fields such as:
sender_principal_idsender_agent_instance_idsender_canonical_identity_iddisplay_name_snapshotsender_auth_context_version
and the API must ignore any client-supplied “from” value. The client can suggest a display name during negotiation; it must never be allowed to choose the stored sender identity.
Data model, lifecycles, and endpoint contracts
The normalized model below keeps rooms, membership grants, routing decisions, messages, cursors, notifications, assignments, and durable memory distinct. That mirrors the way Matrix distinguishes room state, room history, receipts, edits, and relations; XMPP distinguishes room control, archives, and visible markers; and workflow engines distinguish workflow messages from durable business state.
Normalized data model
| Table | Key columns | Purpose |
|---|---|---|
accounts | account_id | human account or service account |
companies | company_id | tenant root |
account_company_memberships | account_id, company_id, role_set, status | many-to-many account/company membership |
workspaces | workspace_id, company_id | company child scope |
projects | project_id, workspace_id, company_id | workspace child scope |
rooms | room_id, company_id, owner_scope_type, owner_scope_id, room_kind, durability_class, visibility_class, canonical_slug, state | room definition and canonical navigation |
room_relationships | parent_room_id, child_room_id, rel_type | room-to-room graph such as parent/child, related, supersedes |
room_templates | room_template_id, room_kind, default_policy_id | provision rules |
room_membership_grants | grant_id, room_id, subject_type, subject_id, grant_kind, condition_json, state, authz_provenance | explicit room-level grants or narrowing grants |
canonical_identities | canonical_identity_id, company_id, principal_id, agent_type, assigned_display_name, state | negotiated room identity |
identity_claims | claim_id, company_id, principal_id, claimed_name, claimed_purpose, claimed_work_item, policy_result | negotiation transcript |
routing_subjects | routing_subject_id, company_id, scope_type, scope_id, current_route_version | lock point for single-writer routing |
routing_decisions | routing_decision_id, routing_subject_id, route_version, lane, room_id, objective_id, coordinator_identity_id, ack_required, state | structured routing outcome |
assignments | assignment_id, company_id, room_id, objective_id, assignee_kind, assignee_id, state, claimed_at, returned_at | work responsibility |
objectives | objective_id, company_id, scope_type, scope_id, objective_kind, title, expected_evidence_json, state | route target and business intent |
messages | message_id, room_id, seq_no, sender_canonical_identity_id, message_type, body_json, created_at | append-only room messages |
message_relations | source_message_id, target_message_id, rel_type, metadata_json | reply, thread, supersedes, cites, evidence-for |
message_revisions | revision_message_id, original_message_id, supersedes_message_id | edit/supersession lineage |
message_redactions | redaction_id, message_id, reason_code, redacted_by, redacted_at | content masking while retaining audit shell |
read_cursors | room_id, principal_id, fully_read_seq_no, read_receipt_seq_no, cursor_token, updated_at | read progress and unread computation |
presence_sessions | presence_session_id, room_id, principal_id, agent_instance_id, presence_state, last_seen_at | active presence |
notifications | notification_id, principal_id, room_id, message_id, delivery_channel, state | fan-out tracking |
decision_records | decision_id, room_id, objective_id, summary, status, supersedes_decision_id | room outcome record |
evidence_items | evidence_id, company_id, owner_scope_type, owner_scope_id, linked_room_id, linked_message_id, prov_json | durable retained evidence |
memory_documents | memory_id, company_id, owner_scope_type, owner_scope_id, source_decision_id, summary_text, retention_class | durable company/workspace/project memory |
audit_events | audit_event_id, company_id, interaction_id, event_type, actor_id, subject_ref, payload_json | security, compliance, and operations trail |
outbox_events | outbox_event_id, aggregate_type, aggregate_id, event_type, payload_json, published_at | reliable event publishing |
A few design choices matter a lot:
rooms.owner_scope_typeis one ofcompany | workspace | project | goal | task, but durable knowledge objects use onlycompany | workspace | project.messages.seq_nois a room-local monotonic sequence for deterministic pagination and read cursors.message_relationsandmessage_revisionsare separate so replies/threads do not get tangled with edits/supersessions.routing_decisions.route_versionenforces conflict-safe compare-and-swap.audit_events.interaction_idfollows OWASP’s guidance that logs should capture the “when, where, who, and what,” and that an interaction identifier should link related events for a user interaction.
Lifecycle state machines
For durable rooms:
stateDiagram-v2
[*] --> Provisioning
Provisioning --> Active: created + policy attached
Active --> Archived: archive requested
Archived --> Reopening: reopen requested
Reopening --> Active: policy revalidated
Archived --> Purged: retention expired + no legal hold
For temporary goal/task rooms:
stateDiagram-v2
[*] --> Draft
Draft --> Active: route accepted
Active --> Resolving: objective met or cancelled
Resolving --> Archived: decision package stored
Archived --> Reopened: coordinator reopens
Reopened --> Active
For assignments:
stateDiagram-v2
[*] --> Unassigned
Unassigned --> Claimed
Claimed --> InProgress
InProgress --> Paused
Paused --> InProgress
Claimed --> Returned
InProgress --> Returned
InProgress --> Completed
Claimed --> Reassigned
Returned --> Unassigned
Reassigned --> Claimed
That assignment model is intentionally close to the user-task patterns documented by Camunda: assignment is distinct from work progress; tasks may be claimed, assigned, returned, paused, resumed, and completed; and applications often need custom action semantics on top of a smaller set of engine-level lifecycle events.
Consistency, pagination, corrections, and archival
For transcript access, use cursor-based pagination and since tokens, not offset paging. Slack’s API docs explicitly recommend cursor-based pagination for large conversational collections, Matrix uses next_batch / since sync tokens and room message paging, and XMPP MAM requires Result Set Management for archive paging. XMPP MAM also supports “flipped pages,” which are useful when agents scroll backward through history and want newest-in-page first.
For read state, keep two distinct watermarks:
- read receipt watermark: “up to which event has this actor acknowledged?”
- fully read watermark: “up to which event should unread be cleared?”
Matrix explicitly distinguishes receipts from m.fully_read markers, and also separates threaded from unthreaded receipts. That is a strong pattern for MATM because a room can have parallel objectives or thread-like sub-conversations while still needing a main-room unread marker.
For edits and supersession, use append-only revision events rather than destructive updates. Matrix’s replacement-event model is the right precedent: the original message remains intact, later revision events point to it, and clients render the latest valid replacement. Redaction is separate again: it strips visible content while preserving the protocol shell and auditability. That is exactly what an enterprise MATM system needs for message correction, moderation, and legal defensibility.
For retention, distinguish:
- active transcript visibility
- archived but discoverable transcript
- deleted from UI but retained for compliance hold
- hard-purged after retention expiry
Teams’ first-party retention documentation is particularly useful here because it shows that user-visible messages can disappear while retained copies remain in secured compliance storage, and that conflicting retention rules are resolved in favor of retention and longer retention. That is a good model for temporary goal/task rooms whose active room can close while decisions and evidence remain discoverable.
Endpoint contract
The API should be explicit and machine-friendly. Suggested core endpoints:
| Endpoint | Purpose |
|---|---|
POST /v1/companies/{companyId}/welcome-sessions | create-or-resume identity negotiation and routing session |
POST /v1/rooms | create room from template or objective |
GET /v1/rooms/{roomId} | room descriptor with links, policy summary, and current synopsis |
GET /v1/rooms/{roomId}/transcript?cursor=...&limit=... | transcript page |
POST /v1/rooms/{roomId}/messages | append message with idempotency |
POST /v1/rooms/{roomId}/read-cursors | update fully-read and receipt positions |
POST /v1/rooms/{roomId}/membership-grants | explicit grant/narrowing |
POST /v1/routing/decisions | route or reroute a subject |
POST /v1/assignments | claim, return, reassign, pause, complete |
POST /v1/rooms/{roomId}/archive | archive room |
POST /v1/rooms/{roomId}/reopen | reopen room |
GET /v1/notifications | principal-specific unread and delivery state |
Use an Idempotency-Key header on any write that can be retried. Matrix explicitly uses transaction IDs to ensure idempotent event sending, and Temporal recommends durable idempotency keys for retried activities and messages.
Structured routing response
The routing response should be a first-class contract, not inferred from chat text:
{
"routingDecisionId": "rd_01J0XQKDA9D9B1V3M2M3R5N7QH",
"routingSubjectId": "rs_company_9f4d_agentinstance_72c1",
"routeVersion": 12,
"lane": "project-coordination",
"canonicalRoom": {
"roomId": "room_proj_7x2",
"slug": "acme/payments/reconciliation",
"ownerScopeType": "project",
"ownerScopeId": "proj_7x2",
"durabilityClass": "durable"
},
"specificObjective": {
"objectiveId": "obj_8fd1",
"kind": "goal",
"title": "Stabilize July reconciliation workflow",
"summary": "Investigate failed imports, agree remediation, produce evidence package."
},
"expectedEvidence": [
{
"evidenceType": "log-excerpt",
"required": true,
"description": "Import failure traces for the last 24 hours"
},
{
"evidenceType": "decision-record",
"required": true,
"description": "Coordinator-approved remediation decision"
}
],
"responsibleCoordinator": {
"canonicalIdentityId": "cid_coord_001",
"displayName": "Payments Coordinator",
"roomId": "room_proj_7x2"
},
"nextAction": {
"type": "enter-room-and-acknowledge",
"roomId": "room_proj_7x2",
"messageTemplate": "Introduce yourself briefly and attach current findings."
},
"acknowledgementRequired": {
"required": true,
"deadlineUtc": "2026-07-12T16:25:00Z",
"method": "message"
},
"supersedesRoutingDecisionId": "rd_01J0XQHWR4H3...",
"consistency": {
"authzRevision": "authz_rev_9812231",
"roomStateVersion": 441,
"routeIssuedAtUtc": "2026-07-12T16:15:42Z"
}
}
A synchronous accepted route is preferable when the route can be decided immediately. If coordinator arbitration is slow or requires human approval, return 202 Accepted and surface a resumable workflow handle, using Temporal-style update/query semantics underneath.
Human and agent experience
Humans and agents should see the same underlying rooms, but with different presentation envelopes.
For humans, the experience should be wiki-like and navigable. Confluence is the right reference point here: parent/child hierarchy in a visible page tree, shortcuts, labels, and layered permissions. A room page should therefore have breadcrumbs, related-room links, decision cards, evidence links, assignment state, and transcript tabs. Humans should feel like they are moving through a navigable collaboration graph, not through raw queue entries. Atlassian’s documentation on parent/child pages, sidebar shortcuts, labels, and separate space-versus-content permissions is directly relevant.
For agents, the experience should be compact, machine-readable, and bounded. Large full-history replay is usually the wrong default. Matrix explicitly supports lightweight sync with since tokens and room deltas, and XMPP MAM allows bounded archive queries with paging. LangChain’s multi-agent docs are also useful here: they distinguish handoffs, routers, and subagents, and they call out the token and latency cost of sequential handoffs with growing context. OpenAI’s Agents SDK similarly distinguishes handoffs from agents-as-tools. The architectural lesson is to send agents only the context they need for the next step, plus strong links to fetch more if needed.
A good agent room descriptor response looks like this:
{
"room": {
"roomId": "room_proj_7x2",
"title": "Payments Reconciliation",
"kind": "project",
"ownerScope": { "type": "project", "id": "proj_7x2" },
"links": [
{ "rel": "parent-workspace", "href": "/v1/workspaces/ws_19" },
{ "rel": "parent-company", "href": "/v1/companies/co_acme" },
{ "rel": "related-goal-room", "href": "/v1/rooms/room_goal_23" }
]
},
"synopsis": {
"summaryVersion": 44,
"shortSummary": "Current focus: import failures on vendor file set B.",
"openDecisions": 2,
"openAssignments": 1
},
"transcriptWindow": {
"cursor": "seq:441",
"items": [
{
"seqNo": 437,
"messageId": "msg_437",
"sender": { "displayName": "Payments Coordinator", "canonicalIdentityId": "cid_coord_001" },
"type": "decision",
"body": { "text": "Need import traces and vendor schema diff." }
}
]
},
"unread": {
"fromSeqNo": 438,
"toSeqNo": 441,
"count": 4
},
"fetchHints": {
"olderCursor": "seq:436",
"newerSince": "seq:441",
"pageSizeDefault": 50
}
}
Late-arriving agent recovery
Late-arriving agents should not have to reconstruct room state by reading thousands of messages. Recovery should be a three-part bundle:
- room synopsis
- decision ledger since last acknowledged point
- unread transcript delta
This approach is justified by durable-chat precedents. Matrix returns room state deltas and may signal gaps when timelines are limited; XMPP MAM supports archive windows and paging; collaboration APIs such as Slack also use cursor-based history retrieval for large conversations.
So the recovery algorithm is:
- if the principal has a
read_cursor, fetch all unread items afterfully_read_seq_no - additionally fetch all
decision_recordswithdecision_seq_no > decision_cursor - if the unread gap exceeds a threshold, replace the first portion with a machine summary and mark
gap_recovered=true - if the room was archived and reopened, include the most recent archival decision package
Onboarding sequence diagram
sequenceDiagram
participant A as Authenticated Agent
participant W as Company Welcome Room
participant I as Identity Negotiation Service
participant P as Policy/AuthZ
participant R as Routing Service
participant C as Coordinator
participant T as Target Room
A->>W: Enter welcome room
W->>I: Start or resume welcome-session
I->>A: Ask who are you / why present / what working on
A->>I: Submit claims
I->>P: Validate principal, memberships, allowed agent profile
P-->>I: Allowed scopes + negotiated identity constraints
I->>R: Request route with claimed objective/context
R->>C: Obtain coordinator or policy decision
C-->>R: Route selected + evidence expectations + ack requirement
R-->>I: Structured routing decision
I-->>A: Canonical identity + route response
A->>T: Enter canonical room
T-->>A: Compact synopsis + unread delta + required acknowledgement
That flow maps cleanly to Temporal’s Signal-With-Start pattern for creating or resuming an onboarding workflow, then issuing durable updates/queries as routing progresses.
Operations, failure handling, and test strategy
Notification integration
Notifications should come from the outbox, not directly from message writes. CloudEvents is a sensible envelope for those domain events because it standardizes common event metadata for interoperable routing across services and platforms. Use event types such as:
room.message.appendedroom.message.redactedrouting.decision.issuedassignment.claimedroom.archivedroom.reopened
Then have delivery adapters for websocket fan-out, mobile push, email digests, incident channels, or external workflow hooks. CloudEvents exists precisely to reduce the cost of every consumer inventing a different event envelope.
Notification rows should track:
| Field | Use |
|---|---|
notification_id | dedupe and delivery audit |
principal_id | recipient |
reason | mention, assignment, unread threshold, route issued |
room_id / message_id | navigation target |
delivery_channel | websocket, mobile push, email, webhook |
delivery_state | pending, sent, failed, acknowledged |
collapse_key | prevent spammy duplicate pushes |
Moderation, redaction, confidentiality, and prompt-injection containment
For moderation and correction, prefer supersession and redaction over silent mutation. Matrix’s edit and redaction model is the most useful ready-made precedent. For confidentiality, copy Confluence’s restriction principle: a room or page may be more restrictive than its container, not less restrictive.
For prompt-injection containment, do not treat transcript content as executable instructions. OWASP’s LLM Prompt Injection Prevention guidance is directly applicable: separate instructions from user data, validate inputs, monitor outputs, use least-privilege tool scopes, validate agent tool calls against session context, and require human approval for destructive actions. Their attacker model explicitly includes remote or indirect prompt injection through documents, emails, issue descriptions, and fetched web content, which is highly relevant for agent meeting rooms that can contain pasted logs or linked artifacts.
So the room service should enforce:
| Threat | Required control |
|---|---|
| message spoofing another agent | sender identity comes only from auth/session context; no client-controlled sender field |
| pasted prompt tries to reconfigure agent | transcript rendered as data channel; system instructions remain out-of-band |
| linked evidence contains hidden prompt | sanitize and classify remote content before injecting into model context |
| tool misuse from malicious transcript | per-tool auth checks against user/agent permissions and current room scope |
| room mention leaks confidential room | unauthorized room links render as opaque placeholders or not at all |
| display-name impersonation | canonical identity ID remains primary; display names are negotiated labels only |
Simultaneous coordinators and conflicting routing decisions
This is where the actor/workflow layer matters. Dapr actors enforce turn-based concurrency; Orleans defaults to safe single-activation execution; Akka persistence stresses the single-writer principle for each durable identity. The easiest way to use those lessons is to make routing subject a first-class aggregate with one active coordinator of record and a monotonic route_version.
Recommended conflict rule:
- Every route proposal targets a
routing_subject_id. - The routing writer uses optimistic concurrency on
current_route_version. - If two coordinators propose simultaneously, only one update advances the version.
- The loser becomes
superseded. - If a split-brain or duplicate-writer event is detected, require fencing tokens and emit an audit event.
If the organization wants collaborative routing rather than strict single-owner routing, keep one effective route and allow multiple advisory proposals. That preserves the transcript/audit trail without creating ambiguous execution state.
Audit records and signed-out behavior
OWASP’s logging guidance is useful here because it names the kinds of events that must be logged: input validation failures, authentication successes and failures, authorization failures, higher-risk admin actions, sensitive-data access, imports/exports, and suspicious flow-bypass behavior. Those map directly to room systems.
A strong audit policy logs at minimum:
- welcome-room entry and identity negotiation result
- all room creation, archival, reopen, and deletion attempts
- all membership-grant writes and authz denials
- all routing decisions, supersessions, and coordinator overrides
- all assignment claims, returns, pauses, completions, and reassignments
- all message posts, edits, redactions, and moderation actions
- all evidence uploads, exports, and retention deletions
- all unusual transcript fetches, bulk pagination, or failed row-scope checks
- all prompt-injection detections or tool-approval denials
Signed-out behavior should be simple: no room discovery, no transcript visibility, no membership introspection, and no room-title leakage. The only public surface should be the sign-in entry point or explicitly public marketing/help content. If you ever choose to support anonymous/public rooms, treat that as an explicit exception policy, not the default. Atlassian’s docs are a good reminder that anonymous access is possible, but it is a conscious configuration, not an accidental outcome.
Operational metrics
Track the subsystem like a collaboration platform and like a workflow engine:
| Metric | Target signal |
|---|---|
| authz check p95 latency | room enter and transcript fetch responsiveness |
| room enter success rate | onboarding health |
| routing decision p95 latency | coordinator responsiveness |
| route conflict rate | quality of coordinator arbitration |
| message append p95 latency | collaboration usability |
| read-after-write success rate | perceived consistency |
| cursor lag | unread correctness |
| outbox publish lag | notification/search freshness |
| notification delivery success | end-user reliability |
| redaction SLA | moderation responsiveness |
| archive/reopen duration | operational efficiency |
| search indexing lag | discoverability freshness |
| cross-tenant denial count | tenant-isolation pressure |
| recovery bundle size | late-arrival efficiency |
| retention backlog age | compliance risk |
Zanzibar’s paper is a useful benchmark reminder that authorization latency and consistency are first-order product qualities, not implementation details.
Failure scenarios
| Scenario | Expected behavior |
|---|---|
duplicate POST /messages due to retry | idempotency key returns prior result |
| room actor fails after write but before notify | transcript row is committed; outbox retries |
| welcome-session times out | session resumes from workflow state |
| coordinator crash during reassignment | routing subject remains at last committed version; new coordinator retries |
| authz graph change while join in progress | use authz revision token; re-check before transcript delivery |
| tenant index misconfiguration | fail closed; do not return partial cross-tenant results |
| moderation redacts original after edits exist | original becomes hidden; revisions remain lineage/audit only |
| temporary room archived before evidence persisted | archive blocked until decision package transaction succeeds |
| long unread gap after retention purge | return synopsis + decision ledger + gap flag, not incomplete raw history |
| split-brain coordinators | only one fencing token can commit next route version |
Comprehensive integration tests
The integration test suite should be large and deterministic. At minimum, include end-to-end cases for:
| Category | Test |
|---|---|
| tenant isolation | user in company A cannot discover, name-resolve, fetch, paginate, or search company B rooms |
| auth inheritance | company membership enters welcome room; workspace/project room access follows parent scope; confidential room narrows access correctly |
| identity negotiation | conflicting claimed names produce canonical resolution; unauthorized claimed profile is rejected |
| spoofing prevention | message body saying “I am Coordinator” does not affect sender identity; client-supplied sender field is ignored |
| routing | route issuance creates exactly one effective route version under concurrent proposals |
| onboarding | welcome-session can be resumed after crash or timeout |
| transcript consistency | message post is immediately readable to poster and visible in subsequent pagination |
| pagination | concurrent writes do not duplicate or skip transcript items across cursors |
| read cursors | fully-read and receipt markers update independently and compute unread correctly |
| edits and redactions | latest supersession renders; redaction hides content while preserving audit shell |
| temporary rooms | goal/task room archival preserves decision record and evidence links |
| reopen | archived durable room reopens without losing transcript or room identity |
| retention | expired temporary-room transcript disappears from UI but decision/evidence remain according to policy |
| prompt injection | malicious pasted instructions do not alter system policy or unauthorized tool access |
| notifications | websocket/email/mobile dedupe correctly with collapse keys |
| conflict holds | legal hold prevents purge even after room archival |
| late arrival | late-arriving agent receives synopsis + decisions + unread delta without full replay |
| assignment lifecycle | claim, pause, resume, return, reassign, and complete transitions enforce auth and comments where required |
Done well, this architecture gives you what mature platforms already converged on: rooms as durable collaboration objects, authorization as a separate graph, routing as a governed control plane, transcripts as append-only timelines with revision/redaction semantics, and memory/evidence as retained domain records rather than ad hoc chat residue. The sources above do not hand you this exact MATM system ready-made, but they do converge strongly on the same design boundaries. The most defensible enterprise implementation is to adopt those boundaries deliberately instead of rediscovering them through production failures.