Architecture

How the Rails control plane and the Elixir runtime split the work, and the path a message takes between them.

ThreadBridge is two programs. A Rails application holds accounts, connections, and bridge configuration; an Elixir runtime holds the live connections and moves messages. They talk over an authenticated API rather than a shared private network, since the two run on different infrastructure providers, and the split is the reason a bridge can survive a deploy or a crash.

The split

Rails (control plane)Elixir (runtime)
Accounts, sign-in, billing Live webhook endpoints for both platforms
OAuth connections and encrypted tokens One supervised process tree per running bridge
Bridge configuration and channel maps Routing, loop suppression, delivery
Health checks, alerts, token refresh Microsoft Graph subscriptions and renewals

Rails is the single source of truth. The runtime keeps a derived copy of a bridge's configuration in memory and re-fetches it whenever it restarts a bridge or boots, so there's no second place where configuration can drift.

Why Elixir for the relay

Each running bridge is its own supervised tree of processes: a router plus one adapter per platform. A bridge that crashes is restarted by its supervisor without touching any other bridge: one customer's revoked token cannot take down another customer's relay. That isolation is the property the whole design is built around, and it's what the BEAM gives you almost for free.

The path of a message

  1. Someone posts in a mapped Slack channel.
  2. Slack POSTs the event to the runtime's public webhook endpoint.
  3. The signature is verified against the Slack signing secret; anything unsigned or stale is rejected.
  4. The event is routed to the right bridge by workspace, then normalized into a platform-neutral message: plain text, author, source channel.
  5. The router checks whether it's a duplicate delivery or the echo of a message it just sent. Either way, it stops here.
  6. Otherwise the source channel is mapped to its counterpart on every other platform in the bridge, and the message is sent to each one.
  7. For each delivery, the router records the echo it expects back and the mapping between the two platforms' message IDs.
  8. The echo arrives moments later from the destination platform and is dropped.

Adding platforms

Nothing in the router names Slack or Teams. Adapters translate at the boundary, inbound into a canonical message and outbound into the platform's own format, and the router only ever sees the canonical shape. A bridge's message map holds one entry per platform per channel rather than a hardcoded pair, so a third platform is a new adapter rather than a rewrite.

Network shape

The runtime exposes two ports, both reachable from the public internet: one that receives webhooks from Slack and Microsoft, and a separate one that Rails uses to start, stop, and configure bridges. Rails and the runtime run on different infrastructure providers, so there's no private network between them to rely on. Instead, every request on the management port is checked against a shared secret with a constant-time comparison, and a request without the right one is rejected outright. That check, not network placement, is what keeps the management API to Rails alone.