Carbon

Gateway Forwarder

Forward Discord Gateway events to your application as webhook-style events

Want to run your bot on serverless but still need Gateway events? The Gateway Forwarder plugin is here to help! It lets you receive Gateway events through regular HTTP webhooks instead of maintaining a WebSocket connection. This means you can run your bot on platforms like Vercel, Cloudflare Workers, or AWS Lambda while still getting all those real-time events from Discord.

How Does It Work?

The Gateway Forwarder uses a clever two-part setup: a super lightweight forwarder that runs on a regular server (like a $5 VPS) and just maintains the WebSocket connection to Discord, and your main bot code that runs wherever you want (even serverless!) and receives events as HTTP webhooks. Think of the forwarder as your bot's phone operator - it stays on the line with Discord and forwards any important calls to your main application. This way, your main bot logic can scale independently and process events through your familiar HTTP infrastructure.

Technical Details

The forwarder is designed to handle all the complex parts of Gateway connections so you don't have to. It maintains a WebSocket connection to Discord's Gateway following the official lifecycle - establishing the connection, handling the Hello event, keeping up with heartbeats and ACKs, and managing session state and resuming. It takes care of receiving and decoding Gateway events (including ETF/JSON handling), signs each event with Ed25519, and forwards it to your webhook endpoint. If your endpoint happens to be down, it'll handle retries with exponential backoff, and it manages all the Gateway session state and reconnections exactly as Discord specifies.

On your main application's side, things are even simpler. Your app receives events as regular HTTP POST requests, and Carbon's client automatically handles all the security and processing for you. The client verifies the Ed25519 signatures, manages rate limits according to Discord's guidelines, and presents the events to your listeners in the same format as if they came directly from Discord.

Setting Up the Forwarder

First, you'll need to generate a key pair so your main bot can verify that events are actually coming from your forwarder. Here's what you need to run in your terminal:

Terminal
# Generate key pair and store in variables
KEYPAIR=$(openssl genpkey -algorithm ED25519)
PUBKEY=$(echo "$KEYPAIR" | openssl pkey -pubout)
 
# Extract raw public key bytes (last 32 bytes) and convert to hex
RAW_PUBKEY=$(echo "$PUBKEY" | grep -v -- "-----" | tr -d '\n' | base64 -d | tail -c 32 | xxd -p -c 64)
 
# Add public key to .env
echo "FORWARDER_PUBLIC_KEY=\"$RAW_PUBKEY\"" >> .env
 
# Add private key with escaped newlines to .env
echo -n "FORWARDER_PRIVATE_KEY=\"" >> .env
echo "$KEYPAIR" | awk '{printf "%s\\n", $0}' >> .env
echo "\"" >> .env

This will add two environment variables to your .env file: the FORWARDER_PRIVATE_KEY that the forwarder uses to sign events (keep this one secret!), and the FORWARDER_PUBLIC_KEY that your main bot uses to verify events are legitimate.

Setting Up the Forwarder Bot

Creating a new application for your forwarder is super straightforward - it just needs the Gateway Forwarder plugin:

src/index.ts
import "dotenv/config"
import { 
	Client, 
	type ListenerEventType
} from "@buape/carbon"
import {
	GatewayForwarderPlugin,
	GatewayIntents
} from "@buape/carbon/plugins/gateway-forwarder"
 
const client = new Client(
	{
		baseUrl: process.env.BASE_URL,
		deploySecret: process.env.DEPLOY_SECRET,
		clientId: process.env.DISCORD_CLIENT_ID,
		publicKey: process.env.DISCORD_PUBLIC_KEY,
		token: process.env.DISCORD_BOT_TOKEN
	},
	{},
	[
		new GatewayForwarderPlugin({
			intents: GatewayIntents.Guilds | 
					GatewayIntents.GuildMessages | 
					GatewayIntents.MessageContent,
			webhookUrl: `${process.env.BASE_URL}/events`,
			privateKey: process.env.FORWARDER_PRIVATE_KEY,
			eventFilter: (event: ListenerEventType) => {
				return event.startsWith('MESSAGE_');  // Only forward message events (create, update, delete)
			}
		})
	]
)
 
console.log(
	`Gateway forwarder ready to send events to ${process.env.BASE_URL}/events`
)

Setting Up Your Main Bot

Setting up your main bot application is even simpler - you just need to add the forwarder's public key to your client config:

src/index.ts
import { Client } from "@buape/carbon"
 
const client = new Client({
	baseUrl: process.env.BASE_URL,
	deploySecret: process.env.DEPLOY_SECRET,
	clientId: process.env.DISCORD_CLIENT_ID,
	publicKey: [
		process.env.DISCORD_PUBLIC_KEY,    // Discord's public key
		process.env.FORWARDER_PUBLIC_KEY   // Forwarder's public key
	],
	token: process.env.DISCORD_BOT_TOKEN
})

When events arrive at your /events endpoint, they'll come with all the headers you need: X-Signature-Ed25519 for the event signature (64 bytes hex), X-Signature-Timestamp for the timestamp used in the signature, Content-Type set to application/json, and the proper Discord bot user agent. The event payload itself is straightforward too - it's a simple JSON object with a type (always 0 for Gateway events) and an event object containing the Gateway event type and data, matching Discord's Gateway format exactly.

Event Verification

Event verification happens automatically in Carbon's client. Here's how it works:

  1. When the forwarder sends an event:

    • It adds a timestamp to the request
    • It concatenates the timestamp and the raw JSON body
    • It signs this data with Ed25519 using its private key
    • It sends the event with the signature in the X-Signature-Ed25519 header and timestamp in X-Signature-Timestamp
  2. When Carbon's client receives the event:

    • It extracts the timestamp and signature from the headers
    • It verifies the signature using the forwarder's public key (which you provided in the client config)
    • If verification passes, it processes the event and sends it to your listeners
    • If verification fails, it rejects the event to protect your bot

You don't need to implement any of this yourself - just make sure you've added the forwarder's public key to your client config as shown in the setup example above.

Quick Tips for Success

When setting up your Gateway Forwarder, there are a few key things to keep in mind. First and foremost, keep that private key super secret - never commit it to Git! Always use environment variables for both keys. It's also crucial to use HTTPS for your webhook endpoint to keep everything secure.

Consider using the event filter to reduce unnecessary traffic, and make sure you've got appropriate timeouts set up for your environment. The forwarder will handle retries if your endpoint is temporarily down, but you might want to monitor its connection status in production.

Quick Start with Railway

Want to get started fast? You can deploy a forwarder on Railway in just a few minutes. Start by creating a new forwarder project:

pnpm create carbon@latest

When prompted, you can pick any project name you like, but make sure to choose "Gateway Forwarder" as the runtime. Let it install the dependencies, then generate your keys using the commands we covered earlier.

Next, head over to Railway and create a new project. Choose "Deploy from GitHub" and pick your forwarder repo. You'll need to add these environment variables:

  • BASE_URL: Your Railway deployment URL (like https://your-app.railway.app)
  • DEPLOY_SECRET: Any random string to secure deployments
  • DISCORD_CLIENT_ID: Your Discord app ID
  • DISCORD_PUBLIC_KEY: Your Discord app public key
  • DISCORD_BOT_TOKEN: Your Discord bot token
  • FORWARDER_PRIVATE_KEY: The private key from earlier

Finally, set your start command in Railway to:

pnpm build && pnpm start

And that's it! Your forwarder will run happily on Railway, keeping that WebSocket connection alive while your main bot scales independently. Just remember: keep that private key safe. If someone gets their hands on it, they could send fake events to your bot!

Edit on GitHub

Last updated on

On this page