A full-featured, self-hosted Discord support ticket bot β panel-based ticket creation, staff claiming with channel locking, scheduled close/delete, staff discussion threads, ratings, and gorgeous auto-generated HTML transcripts (attachments, avatars, and animated GIFs included).
Drop it into any server and make it yours: the bot's name and logo are rebrandable straight from Discord with /setup, no code or redeploy required. Every message the bot sends is built with Discord's Components V2 UI system rather than classic embeds β same information, a more flexible layout.
- ποΈ Panel-based tickets β Discord Support / Website Support buttons, customizable per-post
- π Claiming & locking β staff claim a ticket to lock it,
/unlockor/transferto hand it off - π‘οΈ Rank-aware
/add//removeβ higher-ups always bypass, basic staff can't touch a higher-up - π§΅ Staff discussion threads β private per-ticket threads, merged into the same transcript
- β° Auto-unclaim & escalation β idle claims release themselves, ignored tickets ping higher-ups
- π Scheduled close/delete β 5-second cancel window, or schedule a close for later (
in: 2h) - π Discord-authentic HTML transcripts β grouped messages, markdown, embeds, avatars (animated GIFs stay animated!), all generated from the bot's own message cache β no live channel fetch required, so deleted messages and deleted channels are both handled correctly
- β Ratings & stats β DM rating prompts,
/statsleaderboard,/staffstatsdeep-dive - π« Ticket bans, full
/history, canned/replyreplies - π¨ Rebrandable β name + logo image, both set live via
/setup, no restart needed - π§© Components V2 UI β every panel, announcement, and menu is built from Discord's modern component system instead of classic embeds
- π Single-admin gate β one Discord user ID in
config.jscontrols all of/setupand/test, not editable from Discord itself
- Node.js 22.5.0+ (uses Node's built-in SQLite driver β no external database)
- A Discord account that can create applications and manage your server
- Head to the Discord Developer Portal β New Application.
- Open the Bot tab:
- Click Reset Token and copy it β you'll need it in step 3. π Keep it secret.
- Under Privileged Gateway Intents, enable both:
- β Server Members Intent
- β Message Content Intent
- OAuth2 β URL Generator β check
botandapplications.commands. - Under Bot Permissions, check: Manage Channels, Manage Roles, View Channels, Send Messages, Read Message History, Attach Files, Embed Links, Manage Messages, Create Public Threads, Send Messages in Threads.
- Open the generated URL and add the bot to your server.
β οΈ Drag the bot's role above your staff roles in Server Settings β Roles β it locks/unlocks tickets by editing permissions on those roles, which only works on roles below its own.
npm run setupThis copies config.example.js β config.js for you (or do it manually: cp config.example.js config.js). Then open config.js and fill in the 3 fields β each is explained with a comment right above it:
| Field | What it is |
|---|---|
token |
Your bot token from step 1 |
guildId |
Your server ID (enable Developer Mode β right-click server icon β Copy Server ID) |
adminUserId |
Your Discord user ID β the only person who can run /setup and /test |
config.jsis gitignored and never committed β it's yours alone. Everything else (channels, roles, cooldowns, branding) is not file-based β you configure it live in Discord in step 5.
npm install
npm startThe bot registers its slash commands, runs a permissions health check, and logs anything wrong to the console.
As the adminUserId account, run /setup β it opens a dropdown menu covering everything:
π¨ Branding Β· π¨ Color scheme Β· ποΈ Panel & ticket types Β· π Transcript channel Β· π Log channel Β· π Stats channel Β· π Staff-stats channel Β· π§΅ Staff threads channel Β· π₯ Staff team role Β· π‘οΈ Staff roles Β· β Higher-up roles Β· β³ Panel cooldown Β· π¨ Escalation time
Then post the panel with /panel wherever you want it. π
| Command | Who | What |
|---|---|---|
/panel [title] [description] |
Manage Server | Posts the ticket panel β optionally override its text for this post |
/claim |
Staff | Claim the ticket, locking it to other staff |
/unlock |
Claimer / higher-up | Unlock so another staff member can /claim |
/transfer user: |
Claimer / higher-up | Hand a claimed ticket straight to someone else |
/add user:/role: |
Staff | Add a user or role to the ticket |
/remove user:/role: |
Staff | Remove a user/role (rank-checked β higher-ups bypass) |
/rename name: |
Staff | Renames the channel, its staff thread, and the transcript together |
/close [reason] [in] |
Staff / opener | Closes with a 5s cancel window; in: schedules it (30m, 2h, 1d) |
/delete [reason] |
Staff | Deletes with a 5s cancel window, transcript generated first |
/open |
Staff | Cancels a pending close/delete, or reopens a closed ticket |
/reply name: |
Staff (in a ticket) | Sends a canned reply β edit src/messages.js to add your own |
/stats [period] [sort] |
Anyone (stats channel) | π Leaderboard β claims, closes, response time, rating |
/staffstats user: |
Higher-ups | Full per-staff breakdown + every bad review |
/ticketban user: reason: |
Higher-ups | π« Bans a user from opening tickets |
/ticketunban user: |
Higher-ups | Lifts a ticket ban |
/history user: |
Staff | Past tickets + ban status for a user |
/setup |
π Bot admin | The dropdown-driven configuration menu |
/test |
π Bot admin | Previews every message/embed the bot can send, plus a live avatar-in-transcript demo |
ποΈ Tickets & claiming
- Pressing a panel button opens a short form asking why the ticket is being opened. Tickets are named
ticket-0001,ticket-0002, β¦ and created as top-level channels (no category). - Opening a ticket pings the opener and the staff team role.
- Claiming a ticket removes Send Messages from the staff team role in that channel β the claimer, higher-ups, the opener, and anyone
/add-ed can still chat. Another staff member can only take over after/unlock. - Staff can never claim (or be transferred) a ticket they opened themselves.
- Higher-ups are never locked out β they can override claims, unlock anything, and remove anyone.
- One open ticket per user at a time, plus a per-user anti-spam cooldown (default 60s, configurable).
π Closing, deleting & scheduling
/closearchives the ticket: renamed toclosed-XXXX, opener's access removed, staff keep it, transcript generated./deleteremoves the channel entirely (transcript generated first).- Both start a 5-second countdown any staff member can cancel with
/open./close in: 2hschedules the close for later instead. - A custom
/renamesticks through the whole close β reopen cycle βbilling-issuebecomesclosed-billing-issueon close and goes right back tobilling-issueon reopen, instead of falling back toticket-0007.
π Transcripts
- Generated automatically on close, on delete, and even if the channel is deleted manually (messages are cached live, so nothing is lost).
- Staff-only β posted to the transcript channel and saved to
data/transcripts/. The ticket opener never receives it. - Discord-authentic dark UI: grouped consecutive messages (one avatar/header per burst), full markdown, rich embed cards, and role/thread/bot/edited/deleted badges (shown once per burst, not spammed on every message).
- Strictly chronological ordering (snowflake-ID tiebreak) even when merging the ticket channel with its staff thread.
- Avatars are downloaded and embedded as base64 β animated GIF avatars stay animated. Falls back to a colored initial circle if unavailable.
- Hand-built rather than using a live-fetch transcript library, since those can't recover deleted messages or run after a channel is already gone β both cases this bot has to handle.
π§΅ Staff discussion threads
- If a staff threads channel is configured, every new ticket spawns a same-named thread there, cross-linked with the ticket both ways.
- That channel must be staff-only β threads inherit its visibility, and the health check warns if
@everyonecan see it. - Thread messages (including edits/deletions) are logged into the same transcript tagged
STAFF THREAD, but never count as a staff "response" for auto-unclaim/escalation. - The thread mirrors the ticket completely: renamed, archived, reopened, and deleted right along with it.
β Ratings & stats
- On close, the opener is DM'd a 1β5 β rating prompt; picking a rating opens an optional feedback form.
- If DMs are disabled, the prompt is posted in the closed channel instead (opener gets read-only access back).
- One rating per ticket, only the opener can submit it. Feeds
/statsand/staffstats.
π¨ Branding & customization
/setupβ Branding sets the bot's display name and an optional logo image URL, applied instantly everywhere (embed footers, welcome messages, transcripts) β no restart needed.- Defaults to "Ticket Bot" with no image if never set.
/setupβ Color scheme picks the accent color used across every message (purple/blue/teal/monochrome, defined insrc/messages.js) β applies instantly, no restart. Success/error/warning colors never change, so a warning can never visually read as a success regardless of scheme./setupβ Panel & ticket types adds, edits, or removes the ticket-type buttons shown on/panel(label, emoji, and short description per type) β applies instantly to the panel, ticket titles, channel topics, transcripts, and/history, no restart needed. At least one type must always remain./panelacceptstitle:/description:options to override the panel text per-post without touching code.- All other bot text lives in
src/messages.jsβ edit it freely, including the/replycanned-reply library.
π Admin gate & permissions
adminUserIdinconfig.jsis the sole gate for/setupand/testβ no Discord permission, not even Administrator, grants access on its own. It's intentionally not editable through any bot command, only by editing the file directly, so it can't be self-granted. Since it's one person, they're also the only one who can change staff/higher-up roles.- Mention policy: the bot may ping users and staff roles only. It can never ping
@everyone/@here, even if one ends up in a canned reply β that's enforced at the client level. - On startup and every minute, the bot verifies its own permissions and that configured channels/roles still exist, logging any problems.
βοΈ Reliability
- SQLite storage (
data/bot.db, Node's built-in driver β no extra install). - Every state change (claim, transfer, close, delete, open, add/remove) runs through a per-channel mutex, and ticket creation through a per-user one β two staff can't win a race on the same click, and double-submitting the open form can't create two tickets.
- Attachments are downloaded to disk as they arrive (Discord kills the URLs once a channel is deleted); images β€2MB are embedded directly in the transcript, everything is archived under
data/transcripts/files/<ticket>/when the ticket ends. - Requires the Server Members and Message Content privileged intents β the bot exits at startup with clear instructions if they're missing, instead of failing silently.
index.js β slash command definitions, event routing, bot login
config.example.js β committed config template (copy to config.js)
config.js β your real config (gitignored, created by you)
src/
config.js β merges config.js with live /setup overrides from the DB
db.js β SQLite schema + one-time JSON migration
store.js β all database reads/writes
stats.js β leaderboard/rating aggregation
messages.js β every piece of user-facing bot text
tickets.js β panel, ticket lifecycle, claiming, closing, threads
extras.js β /setup, /test, /stats, /staffstats, /history, bans, ratings
transcript.js β hand-built Discord-authentic HTML transcript renderer
attachments.js β attachment/avatar download + archival engine
data/ β SQLite DB + generated transcripts/attachments/avatars (gitignored)
Issues and PRs are welcome! Adding a new setting is easy and follows one pattern throughout the codebase:
- Add a default in
src/config.js - Add a menu entry to
MENU_OPTIONSinsrc/extras.js - Read it live as
config.<yourKey>wherever it's needed
No caching to worry about β /setup changes apply instantly everywhere.
MIT β do whatever you want with it.