Discord ↔ Stoat Bridge & Migration Bot
A self-hosted bridge that keeps Discord and Stoat (Revolt) communities in sync. Relay messages bidirectionally, migrate entire servers, and deliver push notifications to any device.
View on GitHubDiscord webhooks paired with Stoat masquerade keep avatars and usernames intact on both sides. Built-in echo prevention ensures bridged messages are never duplicated.
Clone channels, roles, emoji, and permissions from Discord to Stoat in one pass. Supports dry-run mode so you can preview every change before committing.
Delivers alerts via FCM, WebPush, and UnifiedPush. Register any mobile or desktop device and receive real-time pings for mentions, DMs, and bridged messages.
A RESTful control plane for bridge management, diagnostics, health checks, and push-device registration. Integrate with your own tooling or monitoring stack.
Full message-history export from Discord and import to Stoat, preserving timestamps, attachments, reply chains, and embeds. Pause, resume, and monitor via slash commands or the HTTP API.
Bidirectional emoji reaction forwarding and typing indicator relay with echo prevention and debounce. Channel name and topic changes sync automatically.
The bot connects to both platforms simultaneously. Inbound events on either side are transformed and forwarded to the other, keeping channels in continuous sync.
# Clone the repository
git clone https://github.com/tribixbite/stoatcord-bot.git
cd stoatcord-bot
# Install dependencies
bun install
# Copy the example env and fill in your tokens
cp .env.example .env
# Start the bridge
bun run start Requires Bun v1.1+. See the README for full configuration options.
Link a Discord guild to a Stoat server, then bridge individual channels.
On Stoat, a server admin generates a one-time claim code:
!stoatcord codeOn Discord, an admin claims it and runs the migration wizard:
/migrate claim_code:ABC123This creates matching channels and roles on Stoat. Options:
| Option | Description |
|---|---|
mode | missing (default), full, roles, or categories |
dry_run | Preview without making changes |
include_snapshot | Post full server data to #migration-log |
include_media | Upload server icon, banner, emoji images |
In each Discord channel you want to bridge:
/link stoat_channel_id:01ABC123DEF456Messages, edits, deletes, reactions, typing indicators, and channel metadata changes sync bidirectionally.
# Check what's linked
/status
# Remove a channel bridge
/unlinkExport Discord channel history and optionally import it into a Stoat channel.
# Export + import in one step
/archive start stoat_channel_id:01ABC123DEF456
# Export only (no import)
/archive start
# Check job status
/archive status
# Pause / resume a running job
/archive pause job_id:abc123
/archive resume job_id:abc123| Option | Description |
|---|---|
stoat_channel_id | Target Stoat channel (omit for export-only) |
rehost_attachments | Re-upload Discord files to Stoat CDN |
preserve_embeds | Convert Discord embeds to Stoat format |
!stoatcord archive| Command | Description |
|---|---|
!stoatcord code | Generate a one-time migration claim code (admin) |
!stoatcord request <guild_id> | Send a live migration approval request to Discord |
!stoatcord status | Show bridge link status for this server |
!stoatcord archive | Show archive job status for the linked guild |
!stoatcord ping <user_id> | Test push notifications for a user |
!stoatcord diag | Run notification diagnostics |
!stoatcord help | Show command list |
| Command | Permission | Description |
|---|---|---|
/migrate | Administrator | Run the server migration wizard |
/link | Manage Channels | Bridge a Discord channel to a Stoat channel |
/unlink | Manage Channels | Remove a channel bridge |
/unlink-server | Administrator | Remove server link, all channel bridges, and role links |
/status | Everyone | Show bridge status for this server |
/archive | Administrator | Export/import message history |
/token | Administrator | View or regenerate guild API token |
The bot exposes a REST API at api.stoatcord.com.
Public endpoints like /api/diag require no auth.
Archive endpoints require a per-guild Bearer token (get it with /token in Discord).
| Method | Path | Description |
|---|---|---|
GET | /api/diag | Bot health & diagnostics |
GET | /api/push/vapid | VAPID public key for WebPush |
# Get your token from Discord
/token
# Check archive status
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://api.stoatcord.com/api/archive/status
# Start an export
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"discordChannelId":"123456789"}' \
https://api.stoatcord.com/api/archive/start
# Pause / Resume
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jobId":"abc12345"}' \
https://api.stoatcord.com/api/archive/pause| Method | Path | Description |
|---|---|---|
POST | /api/push/register | Register a device for push (FCM or WebPush) |
DELETE | /api/push/unregister | Remove a device registration |
GET | /api/push/status?deviceId=… | Check device registration status |
When messages cross the bridge, the bot uses platform-native impersonation to preserve the original sender's identity:
masquerade API field, which displays the Discord user's name and avatar on the Stoat message. The message is owned by the bot but renders as the original user.Neither method grants the bot access to the impersonated user's account or permissions.