Stoatcord

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 GitHub

Features

⇄

Bidirectional Message Relay

Discord webhooks paired with Stoat masquerade keep avatars and usernames intact on both sides. Built-in echo prevention ensures bridged messages are never duplicated.

📦

Server Migration Wizard

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.

🔔

Push Notifications

Delivers alerts via FCM, WebPush, and UnifiedPush. Register any mobile or desktop device and receive real-time pings for mentions, DMs, and bridged messages.

⚙

HTTP API

A RESTful control plane for bridge management, diagnostics, health checks, and push-device registration. Integrate with your own tooling or monitoring stack.

📚

Archive Migration

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.

🔀

Reaction & Typing Sync

Bidirectional emoji reaction forwarding and typing indicator relay with echo prevention and debounce. Channel name and topic changes sync automatically.

Architecture

Discord
Webhooks / Gateway
Stoatcord Bot
REST API / WebSocket
Stoat (Revolt)

The bot connects to both platforms simultaneously. Inbound events on either side are transformed and forwarded to the other, keeping channels in continuous sync.

Quick Start

# 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.

Documentation

Bridge Setup

Link a Discord guild to a Stoat server, then bridge individual channels.

Step 1 — Link Servers

On Stoat, a server admin generates a one-time claim code:

!stoatcord code

On Discord, an admin claims it and runs the migration wizard:

/migrate claim_code:ABC123

This creates matching channels and roles on Stoat. Options:

OptionDescription
modemissing (default), full, roles, or categories
dry_runPreview without making changes
include_snapshotPost full server data to #migration-log
include_mediaUpload server icon, banner, emoji images

Step 2 — Link Channels

In each Discord channel you want to bridge:

/link stoat_channel_id:01ABC123DEF456

Messages, edits, deletes, reactions, typing indicators, and channel metadata changes sync bidirectionally.

# Check what's linked
/status

# Remove a channel bridge
/unlink

Archive & History Migration

Export Discord channel history and optionally import it into a Stoat channel.

Discord Slash Commands

# 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
OptionDescription
stoat_channel_idTarget Stoat channel (omit for export-only)
rehost_attachmentsRe-upload Discord files to Stoat CDN
preserve_embedsConvert Discord embeds to Stoat format

From Stoat

!stoatcord archive

Stoat Commands

CommandDescription
!stoatcord codeGenerate a one-time migration claim code (admin)
!stoatcord request <guild_id>Send a live migration approval request to Discord
!stoatcord statusShow bridge link status for this server
!stoatcord archiveShow archive job status for the linked guild
!stoatcord ping <user_id>Test push notifications for a user
!stoatcord diagRun notification diagnostics
!stoatcord helpShow command list

Discord Slash Commands

CommandPermissionDescription
/migrateAdministratorRun the server migration wizard
/linkManage ChannelsBridge a Discord channel to a Stoat channel
/unlinkManage ChannelsRemove a channel bridge
/unlink-serverAdministratorRemove server link, all channel bridges, and role links
/statusEveryoneShow bridge status for this server
/archiveAdministratorExport/import message history
/tokenAdministratorView or regenerate guild API token

HTTP API

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).

Public Endpoints

MethodPathDescription
GET/api/diagBot health & diagnostics
GET/api/push/vapidVAPID public key for WebPush

Archive Endpoints (Bearer token required)

# 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

Push Registration

MethodPathDescription
POST/api/push/registerRegister a device for push (FCM or WebPush)
DELETE/api/push/unregisterRemove a device registration
GET/api/push/status?deviceId=…Check device registration status

How Masquerade Works

When messages cross the bridge, the bot uses platform-native impersonation to preserve the original sender's identity:

  • Discord → Stoat: Messages are sent via the Stoat 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.
  • Stoat → Discord: Messages are sent via a Discord webhook, which can set an arbitrary username and avatar URL. The message appears as the Stoat user.

Neither method grants the bot access to the impersonated user's account or permissions.