Composable Discord bot built with Next.js App Router and TypeScript.
Slash commands, guild settings, FAQ storage, and automatic preview cards for X / Twitter, Pixiv, and Bluesky.
Quick Start · Deployment · Environment · Runbooks · Commands
- Slash commands:
/ping,/help,/faq,/settings - Verified Discord interaction flow on
POST /api/discord-bot/interactions - Automatic preview cards for X / Twitter, Pixiv, and Bluesky
- Optional translate and GIF actions through the media pipeline
- Prisma + Postgres as the default storage path, with Redis storage variables supported
- Deployment model that can start simple and split into
web,listener,media, andgif-worker
flowchart LR
User["Discord User"] --> Discord["Discord API / Gateway"]
Discord --> Web["web\nNext.js app"]
Discord --> Listener["listener\nAlways-on gateway service"]
Web --> Storage["storage\nPrisma + Postgres or Redis adapter"]
Listener --> Storage
Web -. optional remote .-> Media["media\nOptional remote preview/translate/gif wrapper"]
Listener -. optional remote .-> Media
Media --> Gif["gif-worker\nOptional ffmpeg service"]
| Role | Responsibility |
|---|---|
web |
Slash commands, interaction verification, component callbacks, command registration, and debug routes |
listener |
Persistent Discord Gateway connection and auto preview replies for MESSAGE_CREATE |
media |
Optional remote wrapper for /v1/preview, /v1/translate, and /v1/gif |
gif-worker |
Optional ffmpeg-backed conversion service when GIF generation is enabled |
src/app/api/discord-bot/ Next.js routes for interactions, command registration, and debug checks
src/commands/ Slash command implementations
src/common/ Shared configs, stores, types, and media utilities
worker/gateway-listener/ Always-on Discord Gateway listener for auto preview
worker/cloudflare-media-proxy/ Optional remote media proxy
worker/render-gif-api/ Optional Render-friendly GIF worker
docs/en/runbooks/ English deployment and operations runbooks
- Node.js
20+ - pnpm
10+
cp .env.example .env.localStart from .env.example. The default local path expects:
STORAGE_DRIVER=prisma
DATABASE_URL=
MEDIA_MODE=embedded
GIF_MODE=disabled
TRANSLATE_PROVIDER=disabledpnpm install
pnpm prisma:generatepnpm prisma:pushpnpm devpnpm gateway:listenImportant
Automatic preview is not handled directly by the Next.js webhook routes. Local preview testing requires the separate gateway listener process.
In development, use the home page button or call:
POST /api/discord-bot/register-commandsIn production, include:
Authorization: Bearer <REGISTER_COMMANDS_KEY>Note
POST /api/discord-bot/register-commands is rate-limited to 5 requests per IP per minute.
Tip
Render Standard is the repo's primary deployment path: web + listener + db on one platform.
| Profile | Required services | FAQ / settings | Auto preview | Translate | GIF | Platform count |
|---|---|---|---|---|---|---|
Starter |
web + db |
Yes | No | No | No | 1 |
Render Standard |
web + listener + db |
Yes | Yes | Optional when provider is configured | Optional via remote gif worker | 1 |
Split |
web + listener + db + optional media / gif-worker |
Yes | Yes | Yes | Optional | 2+ |
Recommended defaults:
- Use
Render Standardunless you already need a split runtime - Keep
GIF_MODE=disableduntil a dedicated GIF service exists - Only expose translate when the provider is configured
Deployment notes:
- The Render button uses
render.yamlfor the recommendedRender Standardprofile - The Vercel button covers only the
webrole; auto preview still requireslistener - The Cloudflare button deploys only the optional remote
mediawrapper - Railway's official deploy button requires a published template, so this repo does not expose a Railway button yet
NEXT_PUBLIC_APPLICATION_ID=
PUBLIC_KEY=
BOT_TOKEN=
REGISTER_COMMANDS_KEY=
DISCORD_GATEWAY_TOKEN=
STORAGE_DRIVER=prisma
DATABASE_URL=
MEDIA_MODE=embedded
GIF_MODE=disabled
TRANSLATE_PROVIDER=disabledCreate a shared Postgres database for:
discord-bot-webdiscord-bot-listener
Then run:
pnpm prisma:push- Build command:
pnpm install && pnpm prisma:generate && pnpm build - Start command:
pnpm start
- Build command:
pnpm install && pnpm prisma:generate - Start command:
pnpm gateway:listen - Health check path:
/healthz
Check:
POST /api/discord-bot/register-commandshttps://<listener>/healthz/settingsand/faqin a guild- A fresh
x.com,pixiv.net, orbsky.applink in a guild channel
Start from .env.example.
Discord Core
| Variable | Required by | Notes |
|---|---|---|
NEXT_PUBLIC_APPLICATION_ID |
web |
Discord application ID |
PUBLIC_KEY |
web |
Discord interaction verification key |
BOT_TOKEN |
web, listener |
Bot token |
REGISTER_COMMANDS_KEY |
web |
Protects production command registration |
DISCORD_GATEWAY_TOKEN |
listener |
Optional dedicated token; falls back to BOT_TOKEN |
Storage
| Variable | Required by | Notes |
|---|---|---|
STORAGE_DRIVER |
web, listener |
prisma (default) or redis |
DATABASE_URL |
web, listener |
Required when STORAGE_DRIVER=prisma |
UPSTASH_REDIS_REST_URL |
web, listener |
Required when STORAGE_DRIVER=redis |
UPSTASH_REDIS_REST_TOKEN |
web, listener |
Required when STORAGE_DRIVER=redis |
REDIS_NAMESPACE |
web, listener |
Optional Redis key namespace |
Media
| Variable | Required by | Notes |
|---|---|---|
MEDIA_MODE |
web, listener |
embedded (default), remote, or disabled |
MEDIA_SERVICE_BASE_URL |
web, listener |
Required when MEDIA_MODE=remote |
MEDIA_SERVICE_TOKEN |
web, listener |
Optional bearer token for the remote media service |
MEDIA_TIMEOUT_MS |
web, listener |
Timeout for remote media requests |
MEDIA_ALLOWED_DOMAINS |
web, listener, media |
Comma-separated allowlist for supported preview domains |
TRANSLATE_PROVIDER |
web, listener |
disabled (default) or libretranslate |
TRANSLATE_API_BASE_URL |
web, listener, media |
Required for embedded LibreTranslate mode |
TRANSLATE_API_KEY |
web, listener, media |
Optional translate provider key |
GIF
| Variable | Required by | Notes |
|---|---|---|
GIF_MODE |
web, listener |
disabled (default) or remote |
GIF_SERVICE_BASE_URL |
web, listener, media |
Required when GIF_MODE=remote |
GIF_SERVICE_TOKEN |
web, listener, media |
Optional bearer token for the GIF service |
FFMPEG_TIMEOUT_SEC |
gif-worker |
gif-worker only |
MAX_GIF_DURATION_SEC |
gif-worker |
gif-worker only |
GIF_SCALE_WIDTH |
gif-worker |
gif-worker only |
GIF_FPS |
gif-worker |
gif-worker only |
Listener
| Variable | Required by | Notes |
|---|---|---|
GATEWAY_ATTACHMENT_MAX_BYTES |
listener |
Max bytes per relayed preview attachment |
GATEWAY_ATTACHMENT_MAX_ITEMS |
listener |
Max relayed media items |
GATEWAY_ATTACHMENT_TIMEOUT_MS |
listener |
Per-attachment relay timeout |
Legacy Compatibility
The project still accepts these aliases for one deprecation cycle:
MEDIA_WORKER_BASE_URL->MEDIA_SERVICE_BASE_URLMEDIA_WORKER_TOKEN->MEDIA_SERVICE_TOKENMEDIA_WORKER_TIMEOUT_MS->MEDIA_TIMEOUT_MS
- Render Gateway Listener Runbook
- Production Register-Commands Runbook
- Optional Cloudflare Media Service
- Optional Render GIF Worker
| Command | Purpose |
|---|---|
pnpm install |
Install dependencies |
pnpm dev |
Start the local development server |
pnpm build |
Build the production bundle |
pnpm start |
Start the production server |
pnpm gateway:listen |
Start the Discord Gateway listener |
pnpm prisma:generate |
Generate the Prisma client |
pnpm prisma:push |
Apply the Prisma schema to the configured database |
pnpm worker:smoke |
Smoke test a live remote media service |
pnpm lint |
Run ESLint |
pnpm typecheck |
Run tsc --noEmit |
pnpm test |
Run Vitest |
pnpm prettier |
Run Prettier |
Released under the MIT License.