One-command local stack: cp .env.example .env && make up. Idempotent bootstrap renders Synapse + bridge configs from pristine upstream examples, provisions databases, registrations, double puppeting, and the admin user. Textual TUI with bridge manager, in-terminal QR login, and drafts panel. MCP server exposes read tools and a human-gated draft send workflow. 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
Open the TUI, press b for the bridge screen, pick a network — it opens a DM
with that bridge's bot. Send help to list commands and login to start the
login flow. QR codes sent by bots render in-terminal.
| network | bot | login flow |
|---|---|---|
@whatsappbot |
login qr (scan with phone) or login pairing-code |
|
| Google Messages | @gmessagesbot |
login → QR scan from the Messages app |
| Telegram | @telegrambot |
login → phone number + code. Requires TELEGRAM_API_ID/TELEGRAM_API_HASH in .env (get them at my.telegram.org). Without them the bridge is skipped with a log line. |
| X/Twitter | @twitterbot |
login cookies — paste browser cookies |
@linkedinbot |
login cookies |
|
| Discord | @discordbot |
login qr (scan with Discord app) or login token |
@metabot |
login cookies (bridge runs in instagram mode) |
Cookie flows: the bot explains exactly which cookies to paste. 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).