# VM 102 — Matrix Synapse Deployment

## Overview

Matrix (Synapse) is deployed on VM 102 (PFI-ANA-Docker, `10.250.50.70`) as the primary
human-to-agent communication channel for AIPA. The stack consists of:

- **Synapse** — Matrix homeserver (event routing, auth, persistence)
- **PostgreSQL 16** — Synapse database backend
- **Element Web** — Web client for users

Federation is disabled (internal-only deployment). Registration is disabled (admin-created accounts only).

---

## Architecture

```
User (Element client)
       │  m.room.message events
       ▼
┌──────────────────────┐
│  Synapse             │  Matrix homeserver — event routing, auth, persistence
│  (homeserver)        │  Port 8008 (Client-Server API)
└──────────┬───────────┘
           │  Appservice push  PUT /_matrix/app/v1/transactions/{txnId}
           ▼
┌──────────────────────────────────────────┐
│  AIPA Matrix Bridge  (core/matrix_bridge.py)  │
│  Port 8009                                │
└──────────────────────────────────────────┘
```

> Full appservice bridge details: [VM 102 — Matrix Appservice Configuration](vm-102-matrix-appservice.md)

---

## Components

| Component       | Image                          | Port  | Purpose                           |
|-----------------|--------------------------------|-------|-----------------------------------|
| `synapse`       | `matrixdotorg/synapse:latest`  | 8008  | Matrix homeserver (Client-Server API) |
| `synapse-db`    | `postgres:16`                  | —     | Synapse database                  |
| `element-web`   | `vectorim/element-web:latest`  | 8080  | Web client for users              |

---

## Storage Paths

| Host Path                          | Purpose                                 |
|------------------------------------|-----------------------------------------|
| `/opt/docker/compose/synapse/`     | Compose file (managed by Dockge)        |
| `/opt/docker/conf/synapse/`        | Configuration files                     |
| `/opt/docker/data/synapse/`        | Synapse data (`homeserver.yaml`, signing keys, appservice reg) |
| `/opt/docker/conf/synapse/element-config.json` | Element Web client config    |

> Follows the VM 102 convention: compose in `/opt/docker/compose/<service>/`, config in `/opt/docker/conf/<service>/`.

### Volumes

| Volume / Path                  | Container Path                | Purpose                     |
|--------------------------------|-------------------------------|-----------------------------|
| `/opt/docker/data/synapse`     | `/data`                       | Synapse config, signing keys, media store |
| `synapse-db-data` (named vol)  | `/var/lib/postgresql/data`    | PostgreSQL persistent data   |

---

## Step 1 — Create Directory Structure

```bash
mkdir -p /opt/docker/data/synapse
mkdir -p /opt/docker/conf/synapse
mkdir -p /opt/docker/compose/synapse
```

---

## Step 2 — Generate Synapse Config

```bash
docker run --rm \
  -v /opt/docker/data/synapse:/data \
  -e SYNAPSE_SERVER_NAME=matrix.pfi.local \
  -e SYNAPSE_REPORT_STATS=no \
  matrixdotorg/synapse:latest generate
```

This writes `/opt/docker/data/synapse/homeserver.yaml` and
`/opt/docker/data/synapse/matrix.pfi.local.signing.key`.

**Do not modify `server_name` after generation — it is permanent.**

---

## Step 3 — Edit homeserver.yaml

Open `/opt/docker/data/synapse/homeserver.yaml` and apply:

```yaml
# Use PostgreSQL instead of SQLite (required for production)
database:
  name: psycopg2
  args:
    user: synapse
    password: synapse_db_password   # match POSTGRES_PASSWORD in compose
    database: synapse
    host: synapse-db
    cp_min: 5
    cp_max: 10

# Disable open registration — accounts are created by admin only
enable_registration: false

# Disable federation (internal deployment only)
federation_domain_whitelist: []

# Allow the application service to be registered (add AFTER generating the AS file)
app_service_config_files:
  - /data/aipa_appservice.yaml
```

---

## Step 4 — Docker Compose

File: `/opt/docker/compose/synapse/docker-compose.yml`

```yaml
---
# =============================================================================
# Synapse Matrix Homeserver — AIPA internal deployment on VM 102
# =============================================================================
#
# Conventions:
#   - Config:  /opt/docker/conf/synapse/
#   - Data:    /opt/docker/data/synapse/  (bind mount) + synapse-db-data (named vol)
#   - Compose: /opt/docker/compose/synapse/
#   - Network: synapse-net (dedicated, not on traefik-net)
#
# Notes:
#   - Federation disabled (internal only)
#   - Port 8448 (federation) commented out
#   - Resource limits set for VM 102 (8 vCPU, 16 GB RAM)

services:
  synapse-db:
    image: postgres:16
    container_name: synapse-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: synapse_db_password
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
    volumes:
      - synapse-db-data:/var/lib/postgresql/data
    networks:
      - synapse-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U synapse"]
      interval: 10s
      timeout: 5s
      retries: 5

  synapse:
    image: matrixdotorg/synapse:latest
    container_name: synapse
    restart: unless-stopped
    depends_on:
      synapse-db:
        condition: service_healthy
    ports:
      - "8008:8008"   # Client-Server API (HTTP)
      # - "8448:8448" # Server-Server API (federation) — disabled for internal use
    volumes:
      - /opt/docker/data/synapse:/data
    networks:
      - synapse-net
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "2.0"
        reservations:
          memory: 256M
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8008/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  element-web:
    image: vectorim/element-web:latest
    container_name: element-web
    restart: unless-stopped
    volumes:
      - /opt/docker/conf/synapse/element-config.json:/app/config.json:ro
    ports:
      - "8080:80"
    networks:
      - synapse-net

volumes:
  synapse-db-data:

networks:
  synapse-net:
    name: synapse-net
```

### Start

```bash
cd /opt/docker/compose/synapse
docker compose up -d
```

### Verify

```bash
docker compose ps
curl http://localhost:8008/health
```

---

## Step 5 — Element Web Configuration

File: `/opt/docker/conf/synapse/element-config.json`

```json
{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "http://10.250.50.70:8008",
            "server_name": "matrix.pfi.local"
        }
    },
    "brand": "AIPA",
    "default_theme": "dark",
    "disable_guests": true,
    "disable_login_language_selector": true
}
```

---

## Step 6 — Create Admin User

```bash
docker exec -it synapse register_new_matrix_user \
  -u admin \
  -p 'yourpassword' \
  -a \
  http://localhost:8008
```

> The `-a` flag makes the user an admin. Omit for regular users.

**Agent users** (`@atlas`, `@linus`, `@hermione`) are **virtual** — managed by the
appservice. Do **not** register them as real accounts.

---

## Port Allocation

| Port | Service       | Purpose                    | Protocol |
|------|---------------|----------------------------|----------|
| 8008 | Synapse       | Client-Server API          | HTTP     |
| 8080 | Element Web   | Web client                 | HTTP     |
| 8009 | AIPA Bridge   | Appservice endpoint        | HTTP     |

> See [docker-stack.md](docker-stack.md) for the full VM 102 port allocation table.

---

## Network

This deployment uses a **dedicated `synapse-net` network** (not `traefik-net`), because:

- Synapse is accessed directly by IP (no public domain routing needed)
- The AIPA bridge connects to Synapse at `http://localhost:8008` from the host
- Element Web connects at the VM IP:8008 from the browser

If TLS/reverse proxy is added later, join `traefik-net` and add Traefik labels.

---

## Security Notes

- Synapse is exposed on port 8008 (HTTP). For any externally accessible deployment,
  put it behind a TLS-terminating reverse proxy (Traefik/nginx) and restrict 8009
  to internal access only.
- Registration is disabled (`enable_registration: false`) — accounts created by admin only.
- Federation is disabled (`federation_domain_whitelist: []`) — internal use only.
- The `as_token` and `hs_token` in the appservice registration are secrets equivalent to
  admin credentials. Store in `env.sh` (gitignored), never commit.

---

## Troubleshooting

| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| Bridge starts but Synapse doesn't push events | Appservice URL wrong in registration YAML | Verify `url:` is reachable from the Synapse container; check `docker inspect synapse` network |
| 401 errors from Synapse | `as_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_AS_TOKEN` env var |
| Element can't connect to homeserver | Wrong `base_url` in element-config.json | Must be the IP/hostname Element's browser can reach, not the Docker container name |
| Synapse won't start | Database connection failure | Verify `synapse-db` is healthy first; check password matches in `homeserver.yaml` and compose |

---

## Next Steps

After Synapse is running and healthy, configure the AIPA appservice bridge:
→ [VM 102 — Matrix Appservice Configuration](vm-102-matrix-appservice.md)

---

## Sources

- Source deployment guide: `projects/matrix/matrix-deployment.md` (2026-04-11)
- VM 102 Proxmox config: `configs/pfi-ana/proxmox/vm-102.conf`
- Docker Stack conventions: `docs/pfi-ana/docker-stack.md`
- Matrix Protocol Reference: `infrastructure/matrix-docker-deployment.md` (KB)
