Files
stack/docs/MATRIX-BRIDGE.md
Jason Woltje 68808c0933 docs(#386): Matrix bridge setup and architecture documentation
- Quick start guide for dev environment
- Architecture overview with service responsibilities
- Command reference with examples
- Configuration reference
- Streaming response architecture
- Deployment considerations

Refs #386

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 02:39:20 -06:00

19 KiB

Matrix Bridge

Integration between Mosaic Stack and the Matrix protocol, enabling workspace management and job orchestration through Matrix chat rooms.

Overview

The Matrix bridge connects Mosaic Stack to any Matrix homeserver (Synapse, Dendrite, Conduit, etc.), allowing users to interact with the platform through Matrix clients like Element, FluffyChat, or any other Matrix-compatible application.

Key capabilities:

  • Command interface -- Issue bot commands (@mosaic fix #42) from any mapped Matrix room
  • Workspace-room mapping -- Each Mosaic workspace can be linked to a Matrix room
  • Threaded job updates -- Job progress is posted to MSC3440 threads, keeping rooms clean
  • Streaming AI responses -- LLM output streams to Matrix via rate-limited message edits
  • Multi-provider broadcasting -- HeraldService broadcasts status updates to all active chat providers (Discord and Matrix can run simultaneously)

Architecture

Matrix Client (Element, FluffyChat, etc.)
         |
         v
  Synapse Homeserver
         |
    matrix-bot-sdk
         |
         v
+------------------+       +---------------------+
|  MatrixService   |<----->| CommandParserService |
|  (IChatProvider) |       | (shared, all platforms)
+------------------+       +---------------------+
    |         |
    |         v
    |   +--------------------+
    |   | MatrixRoomService  |  workspace <-> room mapping
    |   +--------------------+
    |         |
    v         v
+------------------+     +----------------+
| StitcherService  |     | PrismaService  |
| (job dispatch)   |     | (database)     |
+------------------+     +----------------+
         |
         v
+------------------+
|  HeraldService   |  broadcasts to CHAT_PROVIDERS[]
+------------------+
         |
         v
+---------------------------+
| MatrixStreamingService    |  streaming AI responses
| (m.replace edits, typing) |
+---------------------------+

Quick Start

1. Start the dev environment

The Matrix dev environment uses a Docker Compose overlay that adds Synapse and Element Web alongside the existing Mosaic Stack services.

# Using Makefile (recommended)
make matrix-up

# Or manually
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d

This starts:

Service URL Purpose
Synapse http://localhost:8008 Matrix homeserver
Element Web http://localhost:8501 Web-based Matrix client

Both services share the existing Mosaic PostgreSQL instance. A synapse-db-init container runs once to create the synapse database and user, then exits.

2. Create the bot account

After Synapse is healthy, run the setup script to create admin and bot accounts:

make matrix-setup-bot

# Or directly
docker/matrix/scripts/setup-bot.sh

The script:

  1. Registers an admin account (admin / admin-dev-password)
  2. Obtains an admin access token
  3. Creates the bot account (mosaic-bot / mosaic-bot-dev-password)
  4. Retrieves the bot access token
  5. Prints the environment variables to add to .env

Custom credentials can be passed:

docker/matrix/scripts/setup-bot.sh \
  --username custom-bot \
  --password custom-pass \
  --admin-username myadmin \
  --admin-password myadmin-pass

3. Configure environment variables

Copy the output from the setup script into your .env file:

# Matrix Bridge Configuration
MATRIX_HOMESERVER_URL=http://localhost:8008
MATRIX_ACCESS_TOKEN=<token from setup-bot.sh>
MATRIX_BOT_USER_ID=@mosaic-bot:localhost
MATRIX_CONTROL_ROOM_ID=!roomid:localhost
MATRIX_WORKSPACE_ID=<your-workspace-uuid>

If running the API inside the Docker Compose network, use the internal hostname:

MATRIX_HOMESERVER_URL=http://synapse:8008

4. Restart the API

pnpm dev:api
# or
make docker-restart

The BridgeModule will detect MATRIX_ACCESS_TOKEN and enable the Matrix bridge automatically.

5. Test in Element Web

  1. Open http://localhost:8501
  2. Register or log in with any account
  3. Create a room and invite @mosaic-bot:localhost
  4. Send @mosaic help or !mosaic help

Configuration

Environment Variables

Variable Description Example
MATRIX_HOMESERVER_URL Matrix server URL http://localhost:8008
MATRIX_ACCESS_TOKEN Bot access token (from setup script or login) syt_bW9z...
MATRIX_BOT_USER_ID Bot's full Matrix user ID @mosaic-bot:localhost
MATRIX_CONTROL_ROOM_ID Default room for status broadcasts !abcdef:localhost
MATRIX_WORKSPACE_ID Default workspace UUID for the control room 550e8400-e29b-41d4-a716-...

All variables are read from process.env at service construction time. The bridge activates only when MATRIX_ACCESS_TOKEN is set.

Dev Environment Variables (docker-compose.matrix.yml)

These configure the local Synapse and Element Web instances:

Variable Default Purpose
SYNAPSE_POSTGRES_DB synapse Synapse database name
SYNAPSE_POSTGRES_USER synapse Synapse database user
SYNAPSE_POSTGRES_PASSWORD synapse_dev_password Synapse database password
SYNAPSE_CLIENT_PORT 8008 Synapse client API port
SYNAPSE_FEDERATION_PORT 8448 Synapse federation port
ELEMENT_PORT 8501 Element Web port

Architecture

Service Responsibilities

MatrixService (apps/api/src/bridge/matrix/matrix.service.ts)

The primary Matrix integration. Implements the IChatProvider interface.

  • Connects to the homeserver using matrix-bot-sdk
  • Listens for room.message events in all joined rooms
  • Resolves workspace context via MatrixRoomService (or falls back to control room)
  • Normalizes !mosaic prefix to @mosaic for the shared CommandParserService
  • Dispatches parsed commands to StitcherService for job execution
  • Creates MSC3440 threads for job updates
  • Auto-joins rooms when invited (AutojoinRoomsMixin)

MatrixRoomService (apps/api/src/bridge/matrix/matrix-room.service.ts)

Manages the mapping between Mosaic workspaces and Matrix rooms.

  • Provision: Creates a private Matrix room named Mosaic: {workspace_name} with alias #mosaic-{slug}:{server}
  • Link/Unlink: Maps existing rooms to workspaces via workspace.matrixRoomId
  • Lookup: Forward lookup (workspace -> room) and reverse lookup (room -> workspace)
  • Room mappings are stored in the workspace table's matrixRoomId column

MatrixStreamingService (apps/api/src/bridge/matrix/matrix-streaming.service.ts)

Streams AI responses to Matrix rooms using incremental message edits.

  • Sends an initial "Thinking..." placeholder message
  • Activates typing indicator during generation
  • Buffers incoming tokens and edits the message every 500ms (rate-limited)
  • On completion, sends a final clean edit with optional token usage stats
  • On error, edits the message with an error notice
  • Supports threaded responses via MSC3440

CommandParserService (apps/api/src/bridge/parser/command-parser.service.ts)

Shared, platform-agnostic command parser used by both Discord and Matrix bridges.

  • Parses @mosaic <action> [args] commands
  • Supports issue references in multiple formats: #42, owner/repo#42, full URL
  • Returns typed ParsedCommand objects or structured parse errors with help text

BridgeModule (apps/api/src/bridge/bridge.module.ts)

Conditional module loader. Inspects environment variables at startup:

  • If DISCORD_BOT_TOKEN is set, Discord bridge is added to CHAT_PROVIDERS
  • If MATRIX_ACCESS_TOKEN is set, Matrix bridge is added to CHAT_PROVIDERS
  • Both can run simultaneously; neither is a dependency of the other

HeraldService (apps/api/src/herald/herald.service.ts)

Status broadcaster that sends job event updates to all active chat providers.

  • Iterates over the CHAT_PROVIDERS injection token
  • Sends thread messages for job lifecycle events (created, started, completed, failed, etc.)
  • Uses PDA-friendly language (no "OVERDUE", "URGENT", etc.)
  • If one provider fails, others still receive the broadcast

Data Flow

1. User sends "@mosaic fix #42" in a Matrix room
2. MatrixService receives room.message event
3. MatrixRoomService resolves room -> workspace mapping
4. CommandParserService parses the command (action=FIX, issue=#42)
5. MatrixService creates a thread (MSC3440) for job updates
6. StitcherService dispatches the job with workspace context
7. HeraldService receives job events and broadcasts to all CHAT_PROVIDERS
8. Thread messages appear in the Matrix room thread

Thread Model (MSC3440)

Matrix threads are implemented per MSC3440:

  • A thread root is created by sending a regular m.room.message event
  • Subsequent messages reference the root via m.relates_to with rel_type: "m.thread"
  • The is_falling_back: true flag and m.in_reply_to provide compatibility with clients that do not support threads
  • Thread root event IDs are stored in job metadata for HeraldService to post updates

Commands

All commands accept either @mosaic or !mosaic prefix. The !mosaic form is normalized to @mosaic internally before parsing.

Command Description Example
@mosaic fix <issue> Start a job for an issue @mosaic fix #42
@mosaic status <job-id> Check job status @mosaic status job-abc123
@mosaic cancel <job-id> Cancel a running job @mosaic cancel job-abc123
@mosaic retry <job-id> Retry a failed job @mosaic retry job-abc123
@mosaic verbose <job-id> Stream full logs to thread @mosaic verbose job-abc123
@mosaic quiet Reduce notification verbosity @mosaic quiet
@mosaic help Show available commands @mosaic help

Issue Reference Formats

The fix command accepts issue references in multiple formats:

@mosaic fix #42                                    # Current repo
@mosaic fix owner/repo#42                          # Cross-repo
@mosaic fix https://git.example.com/o/r/issues/42  # Full URL

Noise Management

Job updates are scoped to threads to keep main rooms clean:

  • Main room: Low verbosity -- milestone completions only
  • Job threads: Medium verbosity -- step completions and status changes
  • DMs: Configurable per user (planned)

Workspace-Room Mapping

Each Mosaic workspace can be associated with one Matrix room. The mapping is stored in the workspace table's matrixRoomId column.

Automatic Provisioning

When a workspace needs a Matrix room, MatrixRoomService provisions one:

Room name:  "Mosaic: My Workspace"
Room alias: #mosaic-my-workspace:localhost
Visibility: private

The room ID is then stored in workspace.matrixRoomId.

Manual Linking

Existing rooms can be linked to workspaces:

await matrixRoomService.linkWorkspaceToRoom(workspaceId, "!roomid:localhost");

And unlinked:

await matrixRoomService.unlinkWorkspace(workspaceId);

Message Routing

When a message arrives in a room:

  1. MatrixRoomService performs a reverse lookup: room ID -> workspace ID
  2. If no mapping is found, the service checks if the room is the configured control room (MATRIX_CONTROL_ROOM_ID) and uses MATRIX_WORKSPACE_ID as fallback
  3. If still unmapped, the message is ignored

This ensures commands only execute within a valid workspace context.

Streaming Responses

MatrixStreamingService enables real-time AI response streaming in Matrix rooms.

How It Works

  1. An initial placeholder message ("Thinking...") is sent to the room
  2. The bot's typing indicator is activated
  3. Tokens from the LLM arrive via an AsyncIterable<string>
  4. Tokens are buffered and the message is edited via m.replace events
  5. Edits are rate-limited to a maximum of once every 500ms to avoid flooding the homeserver
  6. When streaming completes, a final clean edit is sent and the typing indicator clears
  7. On error, the message is edited to include an error notice

Message Edit Format (m.replace)

{
  "m.new_content": {
    "msgtype": "m.text",
    "body": "Updated response text"
  },
  "m.relates_to": {
    "rel_type": "m.replace",
    "event_id": "$original_event_id"
  },
  "msgtype": "m.text",
  "body": "* Updated response text"
}

The top-level body prefixed with * serves as a fallback for clients that do not support message edits.

Thread Support

Streaming responses can target a specific thread by passing threadId in the options. The initial message and all edits will include the m.thread relation.

Development

Running Tests

# All bridge tests
pnpm test -- --filter @mosaic/api -- matrix

# Individual service tests
pnpm test -- --filter @mosaic/api -- matrix.service
pnpm test -- --filter @mosaic/api -- matrix-room.service
pnpm test -- --filter @mosaic/api -- matrix-streaming.service
pnpm test -- --filter @mosaic/api -- command-parser
pnpm test -- --filter @mosaic/api -- bridge.module

Adding a New Command

  1. Add the action to the CommandAction enum in apps/api/src/bridge/parser/command.interface.ts

  2. Add parsing logic in CommandParserService.parseActionArguments() (apps/api/src/bridge/parser/command-parser.service.ts)

  3. Add the handler case in MatrixService.handleParsedCommand() (apps/api/src/bridge/matrix/matrix.service.ts)

  4. Implement the handler method (e.g., handleNewCommand())

  5. Update the help text in MatrixService.handleHelpCommand()

  6. Add tests for the new command in both the parser and service spec files

Extending the Bridge

The IChatProvider interface (apps/api/src/bridge/interfaces/chat-provider.interface.ts) defines the contract all chat bridges implement:

interface IChatProvider {
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  sendMessage(channelId: string, content: string): Promise<void>;
  createThread(options: ThreadCreateOptions): Promise<string>;
  sendThreadMessage(options: ThreadMessageOptions): Promise<void>;
  parseCommand(message: ChatMessage): ChatCommand | null;
  editMessage?(channelId: string, messageId: string, content: string): Promise<void>;
}

To add a new chat platform:

  1. Create a new service implementing IChatProvider
  2. Register it in BridgeModule with a conditional check on its environment variable
  3. Add it to the CHAT_PROVIDERS factory
  4. HeraldService will automatically broadcast to it with no further changes

File Layout

apps/api/src/
  bridge/
    bridge.module.ts              # Conditional module loader
    bridge.constants.ts           # CHAT_PROVIDERS injection token
    interfaces/
      chat-provider.interface.ts  # IChatProvider contract
      index.ts
    parser/
      command-parser.service.ts   # Shared command parser
      command-parser.spec.ts
      command.interface.ts        # Command types and enums
    matrix/
      matrix.service.ts           # Core Matrix integration
      matrix.service.spec.ts
      matrix-room.service.ts      # Workspace-room mapping
      matrix-room.service.spec.ts
      matrix-streaming.service.ts # Streaming AI responses
      matrix-streaming.service.spec.ts
    discord/
      discord.service.ts          # Discord integration (parallel)
  herald/
    herald.module.ts
    herald.service.ts             # Status broadcasting
    herald.service.spec.ts

docker/
  docker-compose.matrix.yml        # Dev overlay (Synapse + Element)
  docker-compose.sample.matrix.yml # Production sample (Swarm)
  matrix/
    synapse/
      homeserver.yaml             # Dev Synapse config
    element/
      config.json                 # Dev Element Web config
    scripts/
      setup-bot.sh                # Bot account setup

Deployment

Production Considerations

The dev environment uses relaxed settings that are not suitable for production. Review and address the following before deploying:

Synapse Configuration

  • Set a proper server_name (this is permanent and cannot change after first run)
  • Disable open registration (enable_registration: false)
  • Replace dev secrets (macaroon_secret_key, form_secret) with strong random values
  • Configure proper rate limiting (dev config allows 100 msg/sec)
  • Set up TLS termination (via reverse proxy or Synapse directly)
  • Consider a dedicated PostgreSQL instance rather than the shared Mosaic database

Bot Security

  • Generate a strong bot password (not the dev default)
  • Store the access token securely (use a secrets manager or encrypted .env)
  • The bot auto-joins rooms when invited -- consider restricting this in production by removing AutojoinRoomsMixin and implementing allow-list logic

Environment Variables

  • MATRIX_WORKSPACE_ID should be a valid workspace UUID from your database; all commands from the control room execute within this workspace context

Network

  • If Synapse runs on a separate host, ensure MATRIX_HOMESERVER_URL points to the correct endpoint
  • For federation, configure DNS SRV records and .well-known delegation

Sample Production Stack

A production-ready Docker Swarm compose file is provided at docker/docker-compose.sample.matrix.yml. It includes:

  • Synapse with Traefik labels for automatic TLS
  • Element Web with its own domain
  • Dedicated PostgreSQL instance for Synapse
  • Optional coturn (TURN/STUN) for voice/video

Deploy via Portainer or Docker Swarm CLI:

docker stack deploy -c docker/docker-compose.sample.matrix.yml matrix

After deploying, follow the post-deploy steps in the compose file comments to create accounts and configure the Mosaic Stack connection.

Makefile Targets

Target Description
make matrix-up Start Synapse + Element Web (dev overlay)
make matrix-down Stop Matrix services
make matrix-logs Follow Synapse and Element logs
make matrix-setup-bot Run bot account setup script