A TypeScript Discord app for seedbible.org that receives slash commands over HTTP (interactions endpoint). It runs on Cloudflare Workers with Hono, stores per-server settings in D1, and gets Bible data from the Free Use Bible API.
- Create an application at the Discord Developer Portal, and install it to your server from the Installation page with the scopes
applications.commandsandbot, and the bot permissions View Channels, Send Messages and Read Message History. Thebotscope is needed for inline verses (replying to references like "John 3:16"): Discord only sends messages to a bot that's a member of the server. For the same feature, turn on Bot → Privileged Gateway Intents → Message Content Intent. - Install dependencies:
pnpm install. - Copy
.env.exampleto.envand fill inDISCORD_TOKEN(Bot), andDISCORD_CLIENT_IDandDISCORD_PUBLIC_KEY(General Information)..envstays on your machine:pnpm deploy-commandsandpnpm devread it; the deployed Worker gets its key from a Cloudflare secret instead (below). - Register the slash commands:
pnpm deploy-commands.
Pushing to main deploys automatically (see CI/CD). To deploy by hand:
pnpm wrangler login # once
pnpm db:migrate:remote # create the tables in the D1 database
pnpm wrangler secret put DISCORD_PUBLIC_KEY # once: paste the Public Key when asked
pnpm wrangler secret put DISCORD_TOKEN # once: paste the bot token (only inline verses use it)
pnpm deploy-worker # prints the Worker's URL, e.g. https://sb-discord-app.<you>.workers.devThen in the portal under General Information → Interactions Endpoint URL, enter https://<worker-url>/interactions and save. Discord sends a signed test request; saving only succeeds if the Worker verifies it.
GitHub Actions runs two workflows:
- CI (
.github/workflows/ci.yml): on every pull request and branch push, type-checks, runs the tests, and checks the Worker bundles. - Deploy (
.github/workflows/deploy.yml): on every push tomain, runs CI, then applies D1 migrations, deploys the Worker, registers the slash commands, and smoke-tests the live Worker. You can also start it from the Actions tab.
One-time setup:
- Set the Worker's secrets once (if not done):
pnpm wrangler secret put DISCORD_PUBLIC_KEYandpnpm wrangler secret put DISCORD_TOKEN. - Create a Cloudflare API token (dashboard → My Profile → API Tokens → Create Token, "Edit Cloudflare Workers" template, plus Account → D1 → Edit).
- In GitHub, Settings → Secrets and variables → Actions, add
CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID(dashboard → Workers & Pages, right sidebar),DISCORD_TOKENandDISCORD_CLIENT_ID. - Optional: in Settings → Branches, require the "Type-check, test, build" check before merging into
main.
pnpm db:migrate:local # once, and after adding a migration
pnpm dev # http://localhost:8787Discord can't reach localhost; to try local changes from Discord, expose it with a tunnel (cloudflared tunnel --url http://localhost:8787 or ngrok http 8787) and point the Interactions Endpoint URL at https://<tunnel>/interactions.
Use a separate Discord application for development (its own token, public key and client ID in .env, invited only to a test server). Otherwise your local copy and the deployed bot share one bot: both receive every message and reply twice, and pointing the endpoint at your tunnel takes slash commands away from the live bot.
Inline verses work locally too: the Gateway connection is outgoing, so it needs no tunnel. It starts once a server runs /setinlineverses state: on. Wrangler restarts it whenever you save a file; it resumes its session, so that's cheap. Local cron triggers don't fire on their own; run curl "http://127.0.0.1:8787/cdn-cgi/local/scheduled" to trigger one by hand (rarely needed: the connection keeps itself running with its own alarm).
| Script | Description |
|---|---|
pnpm dev |
Run the Worker locally with wrangler dev |
pnpm deploy-worker |
Deploy the Worker to Cloudflare |
pnpm build |
Bundle the Worker into dist/ without deploying |
pnpm typecheck |
Type-check without emitting |
pnpm test |
Run the tests |
pnpm deploy-commands |
Register slash commands with Discord |
pnpm db:migrate:local |
Apply migrations/ to the local D1 copy |
pnpm db:migrate:remote |
Apply migrations/ to the real D1 database |
.github/workflows/ # CI (checks) and Deploy (main → Cloudflare)
wrangler.jsonc # Worker config: D1 binding, Gateway Durable Object, cron
migrations/ # D1 schema
src/
worker.ts # Workers entry point (fetch, scheduled, Durable Object export)
server/app.ts # Hono app: verifies Discord's signature, routes interactions
interactions/ # router, slash commands, buttons and menus
gateway/ # Durable Object holding the Gateway connection (receives messages)
inline/ # inline verses: replies to references found in messages
bible/ # Free Use Bible API helpers
seedbible/ # seedbible.org links and interface languages
storage/ # D1 access and per-server settings
utils/ # config, errors, text helpers
scripts/deploy-commands.ts # registers slash commands via the REST API (runs locally)
- Create
src/interactions/commands/<name>.tsexporting aCommand(definition +executereturning a response). - Add it to
commandListinsrc/interactions/commands/index.ts. - Run
pnpm deploy-commands. Commands are registered globally and can take up to an hour to appear.
Discord requires a response within 3 seconds. For slower work, return deferReply(interaction, work) from src/interactions/deferred.ts; see CLAUDE.md for details.