What I learned researching how to drive a home coding server from a phone in Signal/Telegram/Slack — and why the channel choice is the least important one.
The goal
Message a bot from my phone while commuting; get my home coding agent to do work on one of my projects; get the answer back. Simple to describe, and it sounds like the hard part is “which chat app.”
It isn’t.
The three channels, honestly
| Signal | Telegram | Slack | |
|---|---|---|---|
| New number needed | no — link as a secondary device | no (BotFather) | no (workspace app) |
| How the bot receives | polls/WS (outbound) | long-poll (outbound) | Socket Mode (outbound WS) |
| Public port needed | no | no | no |
| Self-hosted | yes (container) | no | no |
| Upstream support | community | community | official package (not on npm) |
The one that surprised me: Signal can be a linked device. You don’t need a
second phone number — you scan a QR from signal-cli-rest-api and your bot is
a device on your existing Signal account. Self-hosted, encrypted, and all the
traffic to Signal is outbound — no inbound port, so it works behind NAT and
over a VPN.
The catch: signal-cli-rest-api’s REST endpoint has no authentication of its
own. Bind it to loopback or a private network; never expose it.
Telegram is the fastest to prototype (long polling, no OAuth, no number). Slack is the most integrated if you already live there — but the official package isn’t published to npm, so you’d build it from source.
The insight: the channel is the shallow part
Every channel is just transport. The real design is the same regardless:
- An allowlist. A chat message must never be able to name an arbitrary path. The bridge passes only pre-approved project directories to the server. This is the trust boundary — the server runs with your user’s rights, and there is no sandbox between projects.
- A single-operator check. Ignore messages from anyone but you. Treat all inbound text as untrusted data, never as commands to your shell (prompt injection is real).
- An API target that’s complete. Build on the v1 REST API (
/project,/session,/event) — the stable, global surface — not the younger browser UI’s endpoints (see the v1-vs-v2 post). - A permission policy. The agent asks before destructive actions. Forward
those asks to chat (
/yes,/no) and keep a human in the loop — even from a phone. Never enable auto-approval on a remotely-reachable server.
Because all channels attach to the same v1 API, Telegram → Signal → Slack is a swap of the transport adapter. The core — allowlist router, API client, event tailer — doesn’t change. Prototype on the easiest, harden on the most private.
Security, in one place
- Prefer a VPN mesh (Tailscale/ZeroTier) over public ports; all three channels need zero inbound holes, so there’s rarely a reason to expose anything.
- Password-protect the server even on a private network (defence in depth).
- If you ever put a reverse proxy in front for public access, it must forward server-sent events and WebSocket upgrades — otherwise the UI loads but agent turns stall silently. This is the #1 operational gotcha.
- Secrets (bot tokens, the server password) live in a
600env file with the service, never in the repo.
Takeaways
- Pick the channel for convenience; design the allowlist + API + permissions carefully. That’s where the risk is.
- Signal’s “link as a secondary device” removes the biggest objection to Signal bots.
- Build the core transport-agnostic so switching channels later is a day’s work, not a rewrite.
The bridge is small. The thinking around it is not.