Bootstrap now enables each bridge's provisioning API (persisted shared secret, matrix-token auth) and publishes /shared/bridges.json. The TUI bridge screen shows per-bridge login state and launches a step-driven wizard: flow picker, sequential input prompts (phone/cookies/password), in-terminal QR rendering with long-poll wait, cancel support. Discord (legacy bridge, no v3 API) keeps the bot-DM path via 'm'. verify.sh gains a provisioning API smoke check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vortex — unified messaging terminal client
All your chat networks (WhatsApp, Telegram, Google Messages, X/Twitter, LinkedIn, Discord, Instagram) in one terminal UI, built on Matrix + mautrix bridges, with an MCP server that lets AI agents read your messages and stage drafts — but never send without a human.
┌────────────┐ ┌─────────┐ ┌──────────────────────────────┐
│ Textual TUI│──▶│ Synapse │◀──│ mautrix bridges (7 networks) │
└─────┬──────┘ │ +Postgres│ └──────────────────────────────┘
│ drafts └────┬────┘
▼ SQLite │
┌────────────┐ │
│ MCP server │────────┘ read tools + draft staging for AI agents
└────────────┘
Quickstart
cp .env.example .env # edit MATRIX_PASSWORD / POSTGRES_PASSWORD at minimum
make up # builds + provisions + starts everything
make tui # open the terminal client (already logged in)
make verify # smoke-test the whole stack
That is the entire setup. Bootstrap renders the Synapse config, creates
databases, generates bridge configs + appservice registrations, registers the
admin user, and writes credentials to a shared volume. Re-running make up is
a no-op. No YAML editing, no token copying.
Make targets
| target | what |
|---|---|
make up |
build + start the full stack, wait for healthy |
make tui |
run the Textual TUI (interactive) |
make verify |
smoke tests: health, appservice round-trip, MCP draft gating |
make logs |
follow all logs |
make down |
stop containers |
make nuke |
stop and delete all data (volumes included) |
Logging into bridges
Press b in the TUI for the bridge screen. It shows each bridge's login state
(via the bridge provisioning API). Enter starts a guided login wizard:
- Pick a login method (the bridge advertises its flows: QR, pairing code, phone number, browser cookies, ...).
- The wizard walks each step — QR codes render right in the terminal, text fields become prompts, and the wizard waits while you scan/confirm.
- On success the bridge starts syncing your chats into the room list.
The wizard talks to each bridge's /_matrix/provision/v3 API, authenticated
with your own Matrix access token — no extra secrets to configure.
| network | typical flows |
|---|---|
| QR scan or phone pairing code | |
| Google Messages | Google account / QR from the Messages app |
| Telegram | phone number + code. Requires TELEGRAM_API_ID/TELEGRAM_API_HASH in .env (my.telegram.org); without them the bridge is skipped. |
| X/Twitter | browser cookies (wizard prompts for each value) |
| browser cookies | |
| Discord | manual only — legacy bridge without the v3 provisioning API. Press m to open the bot DM and use login qr or login token. |
browser cookies (bridge runs in instagram mode) |
m on any bridge opens the bot DM for manual commands (help, logout,
relay settings, ...). Passkey/WebAuthn flows aren't supported in a terminal —
use the corresponding cookie or QR flow instead. Double puppeting is
pre-configured for all bridges — your own messages sent from other devices
appear as you.
The TUI
Three panes: room list grouped by network (with [wa] [tg] [gm] [tw] [li] [dc] [ig] [mx] labels and unread counts), message timeline, input bar.
Keys: q quit · b bridge manager · d drafts panel · tab cycle focus ·
enter (in room list) open room. Type /reply <text> to reply to the last
message. Media shows as download links.
The drafts panel lists drafts staged by AI agents via MCP (live, 2s poll):
e edit · s send · x discard. This is the human approval step.
Native run (no Docker): pip install -e ./tui, then
MATRIX_HOMESERVER=http://localhost:8008 vortex-tui (needs the shared volume
mounted or CREDENTIALS_FILE/DRAFTS_DB/STORE_DIR pointed somewhere useful).
MCP server for AI agents
Runs at http://localhost:8765/mcp (streamable HTTP) and over stdio.
Tools: list_rooms, read_messages, search_messages, create_draft,
list_drafts, update_draft, discard_draft, send_draft.
create_draft never sends. send_draft is refused while MCP_ALLOW_SEND=false
(the default) — the agent is told a human must send from the TUI. Drafts appear
in the TUI drafts panel live.
Claude Code:
claude mcp add --transport http vortex http://localhost:8765/mcp
Claude Desktop (claude_desktop_config.json), HTTP transport:
{ "mcpServers": { "vortex": { "url": "http://localhost:8765/mcp" } } }
stdio transport (spawns inside the running container):
{ "mcpServers": { "vortex": {
"command": "docker",
"args": ["compose", "-f", "/absolute/path/to/vortex/docker-compose.yml",
"exec", "-i", "mcp", "vortex-mcp", "--stdio"] } } }
Environment variables
See .env.example — every value is documented inline. Highlights:
BRIDGES_ENABLED— comma-separated bridge list; compose starts exactly these. After changing it, runmake down && make up.REGISTRATION_SHARED_SECRET/DOUBLEPUPPET_SHARED_SECRET— leave asauto; generated once and persisted in the data volume.MCP_ALLOW_SEND— settrueonly if you want AI agents to send without you.
How provisioning works
bootstrap (one-shot, runs before Synapse) renders homeserver.yaml from
synapse/homeserver.template.yaml, generates a signing key and secrets, creates
one Postgres DB per bridge, patches each bridge's config from the pristine
upstream example in bridges/templates/ (homeserver address, appservice
address, Postgres URI, admin permissions, double-puppet secret; Telegram API
creds; Instagram mode), and writes matching registration.yaml files for
Synapse and each bridge. provision (one-shot, after Synapse is healthy)
registers the admin user via the shared-secret admin API and writes
credentials.json to the shared volume; the TUI and MCP server log in from it
with their own devices. Everything is keyed on "file already exists" — re-runs
change nothing.
Troubleshooting
The as_token was not acceptedin a bridge log — registration drift; runmake down && make up(bootstrap rewrites registrations from persisted tokens). If it persists:make nuke && make up.- Telegram container prints "skipped" — expected without
TELEGRAM_API_ID/TELEGRAM_API_HASH. Set them, thenmake down && make up. - Bridge bot doesn't answer —
docker compose logs <bridge>; the bridge must show a successful websocket/appservice connection to Synapse. - TUI can't log in — check
docker compose logs provision; deletetui-store/session.jsonin the shared volume to force a fresh login. - Changed
MATRIX_SERVER_NAMEafter first boot — Synapse server names are permanent;make nuke && make up. - Ports busy — Synapse uses host
8008, MCP usesMCP_HTTP_PORT(8765).