-
Bot (
services/bot) — discord.js gateway client(s). Receives WS events, filters crosspost-eligible messages, forwards crosspost intents to the Proxy, then forgets. The bot's discord.jsClientis configured withrest.api: 'http://proxy:8080/api'andglobalRequestsPerSecond: Infinity— all Discord REST traffic flows through the Proxy. -
Proxy (
services/proxy) — single Node process. Sole purpose: rate-limit synchronization across shards via a shared in-memory@discordjs/restREST instance. All other concerns (crosspost queue, gate, error classification) live here because the Proxy is the natural home for any code wanting to make Discord REST calls with the shared bucket state.
-
Crosspost — the Discord operation
POST /channels/:id/messages/:id/crosspost. The thing we exist to automate. -
Passthrough — any non-crosspost Discord REST request from the bot's discord.js Client. Forwarded by the Proxy unchanged with all
x-ratelimit-*headers preserved, since the bot's Client uses them to maintain its handler/hash collections. -
Invalid requests — the rolling 10-minute count of invalid requests Cloudflare uses to ban an egress IP. Counts 401, 403, and 429 except those with
X-RateLimit-Scope: shared. Tracked in-memory viaRESTEvents.Response. Single-replica only. -
Sublimit — Discord's per-channel 10/hour shared 429 on crossposts. Surfaces as
RateLimitErrorwithscope: 'shared'. -
Blocked cache — negative TTL'd record marking a channel as un-crosspostable (401/403). Cleared by the bot via
DELETE /internal/blocked/:idon permission updates.
-
Gateway — owns the
RESTinstance, therejectOnRateLimitpredicate, REST event listeners, the invalid-requests tracker, and the passthrough Express route. Pure rate-limit-sync concern. -
Crosspost — owns the BullMQ queue + worker, the gate (invalid-requests + blocked + sublimit), the Discord-error classifier, the Redis-backed caches, and the enqueue Express route.
The Crosspost module calls Gateway.rest.post(...) directly (in-process function call, not HTTP).
-
Single process. The v7 three-tier split (
bot → crosspost-worker → proxy) was speculative and untested in production. The proven shape is one process — the Proxy's only reason to exist is shared rate-limit state, which doesn't benefit from another network hop. -
Explicit RPC bot↔proxy for crossposts. Bot calls
POST /crosspost/:channelId/:messageId(empty body) instead of letting the Proxy intercept Discord-shaped URLs. Keeps the async-queue semantics visible at the call site. -
Invalid-requests in-memory. Relies on single-replica deployment. Multi-replica deployment requires moving this back to Redis.