Dice Witch is a Discord dice roller that aims to simulate the experience of rolling Dice IRL. It literally shows you the dice.
Dice Witch runs on six Cloudflare Workers. Durable Objects coordinate roll delivery and Gateway shards. D1 stores application data.
flowchart TB
Browser[Browser]
DiscordHTTP[Discord Interactions]
DiscordAPI[Discord REST API]
DiscordGateway[Discord Gateway]
subgraph Runtime
Web[Web/API Worker]
Interactions[Interactions Worker]
Roll[Roll Worker]
RollWork[RollWork Durable Objects]
Gateway[Gateway Worker]
Coordinator[GatewayCoordinator Durable Object]
Partitions[GatewayPartition Durable Objects]
Rest[Discord REST Worker]
Data[Data Worker]
D1[(D1)]
end
Browser --> Web
DiscordHTTP --> Web --> Interactions --> RollWork
Web --> Roll
RollWork --> Roll --> DiscordAPI
RollWork --> Data --> D1
Interactions --> Data
Interactions --> Gateway
Gateway --> Coordinator --> Partitions <--> DiscordGateway
Gateway --> Data
Gateway --> Rest --> DiscordAPI
Signed HTTP interactions handle commands. The Gateway handles presence, sessions, shards, and guild lifecycle events.
RollWork Durable Objects make Discord delivery idempotent. The Data Worker is the only Worker with direct access to D1. Everything else talks to it through service bindings.
The Web/API Worker serves the React application, OAuth and API routes, and the /interactions endpoint.
cloudflare/: Workers, Durable Objects, shared packages, D1 migrations, tests, and Wrangler examplesfrontend/: React and Vite application
- Node.js 24.13
- npm 11.6
- A Cloudflare account with Wrangler authenticated
- A Discord application and bot
Install dependencies and copy the example configuration files:
npm ci
cp cloudflare/wrangler.data.example.jsonc cloudflare/wrangler.data.jsonc
cp cloudflare/wrangler.discord-rest.example.jsonc cloudflare/wrangler.discord-rest.jsonc
cp cloudflare/wrangler.gateway.example.jsonc cloudflare/wrangler.gateway.jsonc
cp cloudflare/wrangler.interactions.example.jsonc cloudflare/wrangler.interactions.jsonc
cp cloudflare/wrangler.roll.example.jsonc cloudflare/wrangler.roll.jsonc
cp cloudflare/wrangler.web-api.example.jsonc cloudflare/wrangler.web-api.jsonc
cp frontend/.env.example frontend/.env.localThe copied Wrangler files and .env.local are ignored by Git.
Replace every placeholder. If you rename a Worker, update every service binding that points to it.
You will need:
- Discord application ID and public key
- Discord OAuth client ID, client secret, and callback URL
- Public frontend origin and web app URL
- Invite, support, and audit log channel URLs or IDs
- D1 database name and ID
- Gateway mode, hostname, and partition capacities
Create an empty D1 database:
npx wrangler d1 create dice-witch-dataCopy the returned database name and ID into cloudflare/wrangler.data.jsonc, then apply the migrations:
npx wrangler d1 migrations apply dice-witch-data \
--remote \
--config cloudflare/wrangler.data.jsoncThe committed migrations can initialize a fresh deployment directly. No legacy database or data import is required.
Add each secret to the Worker that uses it:
npx wrangler secret put DISCORD_BOT_TOKEN --config cloudflare/wrangler.discord-rest.jsonc
npx wrangler secret put TOPGG_KEY --config cloudflare/wrangler.discord-rest.jsonc
npx wrangler secret put DISCORD_BOT_LIST_KEY --config cloudflare/wrangler.discord-rest.jsonc
npx wrangler secret put DISCORD_BOT_TOKEN --config cloudflare/wrangler.gateway.jsonc
npx wrangler secret put GATEWAY_CONTROL_TOKEN --config cloudflare/wrangler.gateway.jsonc
npx wrangler secret put DISCORD_PUBLIC_KEY --config cloudflare/wrangler.interactions.jsonc
npx wrangler secret put DISCORD_CLIENT_SECRET --config cloudflare/wrangler.web-api.jsoncSecrets Store bindings also work. The application will refuse to run if a required secret is missing.
Build the frontend using the same public origin and Discord application ID configured in the Web/API Worker:
VITE_API_BASE=https://your-domain.example \
VITE_DISCORD_CLIENT_ID=YOUR_DISCORD_APPLICATION_ID \
npm run buildDeploy the Workers in dependency order:
npx wrangler deploy --config cloudflare/wrangler.data.jsonc
npx wrangler deploy --config cloudflare/wrangler.discord-rest.jsonc
npx wrangler deploy --config cloudflare/wrangler.roll.jsonc
npx wrangler deploy --config cloudflare/wrangler.gateway.jsonc
npx wrangler deploy --config cloudflare/wrangler.interactions.jsonc
npx wrangler deploy --config cloudflare/wrangler.web-api.jsoncWrangler creates each Worker during deployment. You do not need to create them separately.
export DISCORD_APPLICATION_ID=YOUR_DISCORD_APPLICATION_ID
read -rsp "Discord bot token: " DISCORD_BOT_TOKEN && echo
export DISCORD_BOT_TOKEN
npm run commands:register --workspace=@dice-witch/cloudflare
unset DISCORD_BOT_TOKENFor development, set DISCORD_COMMAND_GUILD_ID before running the command. This registers the commands to one guild instead of globally.
Set the Discord Interaction Endpoint URL to:
https://your-domain.example/interactions
Set the OAuth callback URL to:
https://your-domain.example/api/auth/callback/discord
Start the Gateway through its authenticated control endpoint:
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $GATEWAY_CONTROL_TOKEN" \
https://YOUR_GATEWAY_WORKER/gateway/start