Skip to content

Discord bridge

armada-discord-bridge connects a Discord channel and an Armada channel in both directions, with each person’s own name on both sides. It exists so a community can talk across the gap before anyone has to migrate.

Anything mirrored into Discord is plaintext to Discord. That is not a bug in the bridge — it is what bridging to a hosted service means, and it is why linking a channel needs an in-channel confirmation from someone holding Manage Channels.

Both ways

Text, long-message splitting, replies, edits, deletes, reactions and typing indicators, plus catch-up backfill after downtime.

Per-user identity

Discord messages arrive in Armada signed by a puppet key derived per Discord user, so each person keeps one stable identity. Armada messages arrive in Discord through a webhook wearing their kind-0 name and avatar.

Attachments

Re-hosted to a Blossom server when one is configured, with a CDN link as the fallback.

Server import

Mint a whole Armada community from a Discord server — channels, roles and grants — with genesis signed by the owner’s own key in their browser, then backfill the history.

The portal is the interesting deployment: Discord OAuth, a setup wizard, the confirmation flow and a dashboard, running bridges in-process for however many communities use it. The runtime is Bun; Postgres is the only other dependency.

Terminal window
git clone nostr://soapbox.pub/relay.ngit.dev/armada-discord-bridge
cd armada-discord-bridge
npm install
docker compose up -d postgres
DATABASE_URL=postgres://bridge:bridge@localhost:5432/bridge \
PORTAL_SEAL_KEY=$(openssl rand -hex 32) \
bun run packages/portal/src/index.ts

Build the SPA once with vite build packages/portal/web; the API then serves web/dist itself. It binds loopback and expects a TLS-terminating proxy in front of it.

Variable
DATABASE_URL Required. Postgres connection string.
PORTAL_SEAL_KEY Required. 64 hex chars. Seals key material at rest.
PORTAL_BASE_URL Public origin. Decides OAuth redirects and whether cookies are Secure.
DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET The OAuth application.
DISCORD_TOKEN Bot token, with the MESSAGE_CONTENT intent enabled. Without it no bridge can go live.
BLOSSOM_SERVER Where attachments are re-hosted. Empty leaves CDN links.
IMPORT_DEFAULT_RELAYS Relays for communities minted by an import.
PORTAL_DEV_LOGIN Fabricates a session for local work. It refuses to boot in production.

There is also a standalone single-link bridge (packages/bridge), provisioned entirely from environment variables and started with docker compose --profile bridge up. It links one Discord channel to one Concord channel, with no portal and no web UI.

Build the client with VITE_BRIDGE_PORTAL_URL set to the portal’s origin — see the web app’s configuration. It is empty by default, which hides every Discord affordance in the app; set it and the import buttons appear on the Add dialog, the welcome page, the Discover grid and community settings.

The client never dials it. The URL is only the target of links a user clicks, and the import runs on the portal, which signs the resulting community with the user’s own key and hands back an ordinary invite.

The bridge is scaffolding: it lowers the cost of trying Armada while half your people are still on Discord. The point is to take it down afterwards.

$ hold the door open