Conversation
…rker wiring) Implements Discord bot functionality analogous to the existing Slack integration: - New `cyrus-discord-event-transport` package with DiscordGatewayClient (WebSocket Gateway lifecycle), DiscordEventTransport (HTTP endpoint for CYHOST-forwarded events), DiscordMessageTranslator, DiscordMessageService (REST API with 2K-char message splitting), and DiscordReactionService - Core types: DiscordPlatformRef, DiscordSessionStartPlatformData, DiscordUserPromptPlatformData, isDiscordMessage type guard, "discord" in MessageSource - DiscordChatAdapter implementing ChatPlatformAdapter with Discord Markdown formatting - EdgeWorker: registerDiscordEventTransport() with /discord-webhook endpoint, status check, and shutdown integration - Updated tsconfig.base.json paths for all event-transport packages CYHOST-731
The DiscordGatewayClient was fully implemented but never instantiated. registerDiscordEventTransport() only created an HTTP endpoint, but Discord requires a persistent WebSocket connection (unlike Slack's HTTP webhooks). Now when DISCORD_BOT_TOKEN is set, the EdgeWorker connects to Discord's Gateway to receive MESSAGE_CREATE events for @mentions directly.
|
Great questions — here's a breakdown: 1. Every client uses the same bot token?Yes, currently. 2. How do we know what guild @mention matches to a client?The export interface DiscordGatewayConfig {
botToken: string;
intents: number;
guildFilter?: string[]; // exists but not yet wired up
}The filtering logic is implemented in For v1, this is analogous to Slack: one bot token per edge worker, one workspace/guild per deployment. True multi-tenant routing (one bot serving N guilds mapped to N teams) would need:
3. WebSocket limitations
|
|
Short answer: The current architecture already handles multi-tenancy — each team's edge worker is its own process with its own This is the same model as Slack today: each team has their own bot token in their edge worker's env. The Gateway connection is per-edge-worker, which is per-team. No shared state. Where it gets trickyIf we ever want a single shared Cyrus Discord app (one bot that customers invite to their servers, like a SaaS product), we'd need a centralized Gateway connection that routes events to the right edge worker. Three options:
RecommendationFor v1: Option 1 (per-team tokens) is correct. It's how Slack works today and scales fine — each edge worker is already isolated per-team. For v2 (if we want one-click Discord install): Option 2 — add a lightweight persistent service (could be a single Node.js process, a Fly.io app, or a Cloudflare Durable Object) that:
This is essentially the same role CYHOST plays for Slack (receive webhook → look up team → forward to edge worker), just with a WebSocket frontend instead of an HTTP one. |
|
Good point — Vercel can't hold WebSocket connections, so the Gateway proxy can't live in CYHOST itself. Here are the options for where to run it: Option A: Cloudflare Durable Object (recommended)We already use Cloudflare for tunnels (
Option B: Fly.io / Railway lightweight serviceA single Node.js process that:
Option C: Vercel Cron + Discord REST polling (hacky, not recommended)Poll Discord's REST API for new messages on a cron schedule (e.g., every 5s). Technically works on Vercel but:
Option D: Move the Gateway client to CYHOST's edge worker sidecarIf CYHOST ever moves off pure Vercel (e.g., to a hybrid setup with a long-running sidecar), the Gateway client could run there. But this is a bigger architectural shift. RecommendationOption A (Cloudflare Durable Object) is the cleanest path. We already have the Cloudflare dependency, Durable Objects are designed for exactly this pattern (stateful WebSocket ↔ HTTP bridge), and it keeps the architecture consistent. The For v1 though, none of this is needed — the per-team bot token on the edge worker works fine. |
|
Here's a concrete implementation plan for the Cloudflare Durable Object approach: ArchitectureThis mirrors the existing Slack flow exactly: Implementation Steps1. Create Cloudflare Worker + Durable Object packageNew package: The Durable Object does 3 things:
wrangler.toml: name = "discord-gateway-proxy"
main = "src/worker.ts"
compatibility_date = "2024-01-01"
[durable_objects]
bindings = [
{ name = "DISCORD_GATEWAY", class_name = "DiscordGatewayDO" }
]
[[migrations]]
tag = "v1"
new_classes = ["DiscordGatewayDO"]
[vars]
CYHOST_WEBHOOK_URL = "https://app.atcyrus.com/api/discord/webhook"Key design: One DO instance per bot token. The Worker entry point routes based on bot token hash → DO instance. This means if we ever have multiple shared bots (unlikely), each gets its own connection. 2. Add Discord webhook forwarding route in CYHOSTNew file: This mirrors the existing Slack webhook route (
The DB index 3. Shared bot setup (Discord Developer Portal)Create a single "Cyrus" Discord application owned by the Cyrus company account:
4. Update onboarding flowWhen a customer clicks "Add to Discord":
What stays the same
Effort Estimate
Should we build this now?For CYHOST-731 (v1), the per-team bot token on the edge worker is sufficient. The DO approach is a v2 optimization for when we want one-click Discord install without customers creating their own bot app. I'd recommend shipping v1 first, validating the Discord experience works end-to-end, then building the DO proxy as a follow-up issue. |
|
Here's what's needed to complete the V2 Discord Gateway via Rivet: Already Done
Remaining Steps1. Add the Vercel provider URL in the Rivet dashboard Go to the Rivet Dashboard → your namespace → Settings → Providers → Add Vercel provider with URL: This tells Rivet where your runner (the Vercel app) is hosted so it can route actor requests to it. 2. Set The actor reads 3. Set The actor uses this shared secret when forwarding events to openssl rand -hex 32 # generate a secret
vercel env add DISCORD_GATEWAY_WEBHOOK_SECRET preview cyhost-731Also referenced in the webhook route for auth validation. 4. Create the actor instance Once the provider is connected, create the import { createClient } from "rivetkit/client";
const client = createClient<typeof registry>();
await client.discordGateway.getOrCreate(["primary"]);Or use the Rivet dashboard to create an actor with key 5. Verify the connection Check the actor status via the Rivet inspector or call the const status = await client.discordGateway.getOrCreate(["primary"]).getStatus();
// { connected: true, botUserId: "1483958995852202074", sessionId: "...", reconnectAttempts: 0 }You should also see Gateway logs in the Rivet dashboard:
6. Test end-to-end @mention the bot in Discord → actor receives MESSAGE_CREATE → forwards to |
|
Found the issue. When I try to create the actor via the API, I get: The Vercel provider hasn't been connected yet in the Rivet dashboard. This is the step that tells Rivet where to actually run the actor. How to fix
Once the provider is connected, creating the actor should work. I can create it via the API once you've done this: curl -X POST "https://api.rivet.dev/actors?namespace=cyrus-whhd-production-zpro" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"name":"discordGateway","key":"[\"primary\"]","runner_name_selector":"default","crash_policy":"restart"}'Or use the Rivet dashboard UI to create it after the provider is registered. |
Assignee: Payton Webber
Summary
cyrus-discord-event-transportpackage with full Discord Gateway WebSocket client (HELLO → IDENTIFY → READY → heartbeat lifecycle with automatic reconnect/resume), HTTP event transport for CYHOST-forwarded webhooks, message translator, REST message service (with 2K-char message splitting), and reaction servicecyrus-core:DiscordPlatformRef,DiscordSessionStartPlatformData,DiscordUserPromptPlatformData,isDiscordMessagetype guard,"discord"added toMessageSourceunionDiscordChatAdapter(followingSlackChatAdapterpattern) with Discord-specific Markdown formatting rules, thread context fetching, reply posting with message references, and emoji acknowledgmentregisterDiscordEventTransport()in EdgeWorker with/discord-webhookendpoint, Bearer token auth, status check integration, and shutdown cleanuptsconfig.base.jsonwith path mappings for all event-transport packagesTest plan
Linear issue
CYHOST-731