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>
This commit is contained in:
537
docs/MATRIX-BRIDGE.md
Normal file
537
docs/MATRIX-BRIDGE.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# 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.
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
MATRIX_HOMESERVER_URL=http://synapse:8008
|
||||
```
|
||||
|
||||
### 4. Restart the API
|
||||
|
||||
```bash
|
||||
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](https://github.com/matrix-org/matrix-spec-proposals/pull/3440):
|
||||
|
||||
- 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:
|
||||
|
||||
```typescript
|
||||
await matrixRoomService.linkWorkspaceToRoom(workspaceId, "!roomid:localhost");
|
||||
```
|
||||
|
||||
And unlinked:
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```bash
|
||||
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 |
|
||||
Reference in New Issue
Block a user