A modern, self-hosted Discord ticket bot built on Discord.js v14 and SQLite — no external database, no telemetry, full feature set out of the box.
| Feature | Description |
|---|---|
| 🎫 Ticket Types | Up to 25 configurable types with individual emoji, color, category & questions |
| 📋 Questionnaires | Modal forms (up to 5 questions) shown when opening a ticket |
| 🙋 Claim System | Staff can claim/unclaim — button toggles, embed & topic update automatically |
| 🔴 Priorities | Low / Medium / High / Urgent via /priority — shown in channel topic & embed |
| 📝 Staff Notes | Private notes via /note add / /note list |
| 🔀 Move Ticket | Move to a different type/category via /move or button (staff only) |
| 🛡️ Type-specific Staff Roles | Each ticket type can define its own staff roles |
| 🖼️ Panel Logo & Banner | Optional logo thumbnail and/or banner image in the panel embed |
| 🎛️ Panel Interaction Type | Choose between a Button or a direct Select Menu in the panel |
| ⭐ Rating System | 1–5 star feedback after closing, automatically posted to a configured channel |
| ⏰ Staff Reminder | Automatic ping inside the ticket if no staff responds within X hours |
| ⏰ Auto-Close | Automatically close inactive tickets with a configurable warning period |
| 📄 HTML Transcript | Full HTML transcript sent to log channel and creator via DM |
| 📊 Statistics | Server-wide stats and detailed per-user stats via /stats |
| 🚫 Blacklist | /blacklist add/remove/list to block users from opening tickets |
| 🌍 Multilingual | German and English included, easily extensible |
| 🗄️ SQLite | No external database required — file is created automatically |
discord_ticketbot/
├── index.js # Entry point
├── package.json
├── .env.example # Environment variable template
├── ticketbot.service # systemd unit file for Linux servers
├── assets/ # Static files (logo, banner images)
│ ├── logo.png # Panel logo thumbnail (place your own here)
│ └── banner.png # Panel banner image (place your own here)
├── config/
│ └── config.example.jsonc # Configuration template (with comments)
├── locales/
│ ├── de.json # German
│ └── en.json # English
├── data/
│ └── tickets.db # SQLite database (auto-created)
└── src/
├── client.js # Extended Discord Client
├── config.js # Config loader & validation
├── database.js # All DB operations (SQLite)
├── handlers/
│ ├── commandHandler.js # Loads & registers slash commands
│ ├── eventHandler.js # Loads Discord events
│ └── componentHandler.js # Loads buttons, modals, menus
├── commands/ # Slash commands
│ ├── setup.js # /setup – Send panel
│ ├── close.js # /close – Close ticket
│ ├── add.js # /add – Add user
│ ├── remove.js # /remove – Remove user
│ ├── claim.js # /claim – Claim ticket
│ ├── unclaim.js # /unclaim – Unclaim ticket
│ ├── move.js # /move – Move ticket
│ ├── rename.js # /rename – Rename channel
│ ├── transcript.js # /transcript – HTML transcript
│ ├── priority.js # /priority – Set priority (topic + embed)
│ ├── note.js # /note – Staff notes
│ ├── blacklist.js # /blacklist – Block users
│ └── stats.js # /stats – Statistics (server & user)
├── events/
│ ├── ready.js # Bot start, status, auto-close & staff reminder loop
│ ├── messageCreate.js # Track last activity
│ └── interactionCreate.js # Route all interactions
├── components/
│ ├── buttons/
│ │ ├── openTicket.js # tb_open
│ │ ├── closeTicket.js # tb_close
│ │ ├── claimTicket.js # tb_claim
│ │ ├── unclaimTicket.js # tb_unclaim
│ │ ├── moveTicket.js # tb_move (opens type selection)
│ │ ├── deleteTicket.js # tb_delete (confirmation step)
│ │ ├── deleteConfirm.js # tb_deleteConfirm
│ │ ├── deleteCancel.js # tb_deleteCancel
│ │ └── rateTicket.js # tb_rate:N
│ ├── modals/
│ │ ├── closeReason.js # tb_modalClose
│ │ └── ticketQuestions.js # tb_modalQuestions:type
│ └── menus/
│ ├── panelSelect.js # tb_panelSelect (SELECT_MENU mode)
│ ├── ticketType.js # tb_selectType (BUTTON mode, ephemeral)
│ └── moveSelect.js # tb_moveSelect
└── utils/
├── logger.js # Coloured console logger
├── embeds.js # All embed constructors
├── transcript.js # HTML transcript generator
└── ticketActions.js # Core logic: openTicket, performClose, performMove, refreshTicketMessage
- Node.js v18 or newer
- A Discord bot token (https://discord.com/developers/applications)
cd discord_ticketbot
npm installcp .env.example .envFill in .env:
TOKEN="your_bot_token"
CLIENT_ID="your_application_id"
GUILD_ID="your_server_id"cp config/config.example.jsonc config/config.jsoncEdit config/config.jsonc as needed — all fields are commented.
npm startOn first start the bot will automatically:
- Create the SQLite database at
data/tickets.db - Register all slash commands on your server
Run /setup on your Discord server (Administrator permission required). The bot will send the ticket panel to the channel configured in openTicketChannelId.
The included ticketbot.service file lets the bot start automatically after a server reboot.
# Copy project folder to /opt
sudo cp -r discord_ticketbot /opt/discord_ticketbot
# Create a dedicated system user (recommended — never run as root)
sudo useradd -r -s /bin/false discord
# Set permissions
sudo chown -R discord:discord /opt/discord_ticketbotsudo nano /opt/discord_ticketbot/.envwhich node
# Output e.g.: /usr/bin/nodeIf the path differs, adjust ExecStart in ticketbot.service accordingly.
sudo cp /opt/discord_ticketbot/ticketbot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ticketbot.service# Show current status
sudo systemctl status ticketbot.service
# Follow live logs
sudo journalctl -u ticketbot.service -f| Command | Description |
|---|---|
sudo systemctl start ticketbot.service |
Start the bot |
sudo systemctl stop ticketbot.service |
Stop the bot |
sudo systemctl restart ticketbot.service |
Restart the bot |
sudo systemctl enable ticketbot.service |
Enable autostart |
sudo systemctl disable ticketbot.service |
Disable autostart |
sudo journalctl -u ticketbot.service -f |
Follow live logs |
| Command | Permission | Description |
|---|---|---|
/setup |
Administrator | Send the ticket panel |
/close [reason] |
Configurable | Close the current ticket |
/claim |
Staff | Claim a ticket — updates topic & embed, button toggles to Unclaim |
/unclaim |
Staff | Release a claimed ticket — updates topic & embed, button toggles back |
/move |
Staff | Move ticket to a different type/category |
/add <user> |
Staff | Add a user to the ticket |
/remove <user> |
Staff | Remove a user from the ticket |
/rename <n> |
Staff | Rename the ticket channel |
/transcript |
Staff | Generate an HTML transcript |
/priority <level> |
Staff | Set ticket priority (updates channel topic & embed) |
/note add <text> |
Staff | Add a staff note |
/note list |
Staff | List all notes for this ticket |
/stats |
Staff | Server-wide ticket statistics |
/stats @user |
Staff | Detailed statistics for a specific user |
/blacklist add |
Manage Guild | Block a user |
/blacklist remove |
Manage Guild | Unblock a user |
/blacklist list |
Manage Guild | Show the blacklist |
Every ticket channel contains a button row at the top:
| Button | Visible when | Description |
|---|---|---|
| 🔒 Close Ticket | Always (configurable) | Disables all buttons, generates transcript, closes & renames channel |
| 🙋 Claim | claimButton: true, not yet claimed |
Staff claims — topic & embed update, button becomes Unclaim |
| 🙌 Unclaim | claimButton: true, already claimed |
Staff releases — topic & embed update, button becomes Claim |
| 🔀 Move | More than 1 ticket type | Staff opens type selection (staff only) |
| 🗑️ Delete Ticket | After closing | Deletes the channel after confirmation |
Controls how users open tickets from the panel.
| Mode | Behaviour |
|---|---|
"BUTTON" |
A green button is shown. Clicking it opens an ephemeral select menu — always fresh, no Discord caching issue. |
"SELECT_MENU" |
The select menu is shown directly in the panel. After every use it automatically resets to its default state, so users never need to restart Discord to open a second ticket of the same type. |
Optional images in the panel embed — place files in the assets/ folder.
"panel": {
"logo": {
"enabled": true, // Show as thumbnail in the top-right of the embed
"file": "logo.png" // Filename inside assets/
},
"banner": {
"enabled": true, // Show as image at the bottom of the embed
"file": "banner.png" // Filename inside assets/
}
}Supported formats: PNG, JPG, GIF, WEBP. Run /setup again after adding or changing images.
| State | Channel Name | Channel Topic | Opening Embed |
|---|---|---|---|
| Ticket opened | ticket-username |
🟡 Medium |
Priority: 🟡 Medium |
/priority urgent |
ticket-username |
🔴 Urgent |
Priority: 🔴 Urgent |
/claim |
ticket-username |
🟡 Medium | 🙋 Claimed by @Staff |
Priority: 🟡 Medium + Claimed by field |
/unclaim |
ticket-username |
🟡 Medium |
Priority: 🟡 Medium (field removed) |
| Ticket closed | closed-ticket-username |
unchanged | all buttons removed |
Note on rate-limits: Discord limits channel topic changes to 2 per 10 minutes per channel (same bucket as channel renames). A warning is shown in the ticket when a topic update is queued. The update will appear automatically once the limit resets.
{
"codeName": "support", // Unique identifier (lowercase)
"name": "Support", // Display name in the menu
"description": "...", // Description in the selection menu
"emoji": "💡",
"color": "#ff0000", // Hex color or "" to use mainColor
"categoryId": "123456789", // Discord category ID
"ticketNameOption": "", // Channel name template: USERNAME, USERID, TICKETCOUNT
"customDescription": "...", // Variables: REASON1, REASON2, USERNAME, USERID
"cantAccess": ["roleId"], // Roles that cannot access this type
"staffRoles": [], // Type-specific staff roles (see below)
"askQuestions": true,
"questions": [
{
"label": "Question",
"placeholder": "Example...",
"style": "SHORT", // SHORT or PARAGRAPH
"maxLength": 500
}
]
}Note on TICKETCOUNT: This is a global sequential counter across all tickets on the server — it never resets, even if tickets are closed. Each new ticket always gets a higher number than the previous one regardless of type or user.
Each ticket type can define its own staff roles, controlling who can see, manage and claim the ticket.
// Only developers can see "Bug Report" tickets:
{ "codeName": "bugreport", "staffRoles": ["ROLE_ID_DEVELOPER"] }
// Leave empty → global rolesWhoHaveAccessToTheTickets are used:
{ "codeName": "support", "staffRoles": [] }When moving a ticket (/move), permissions are automatically updated to match the new type. On ticket open, type-specific roles are pinged instead of the global roleToPingWhenOpenedId.
When more than one ticket type is configured, a 🔀 Move button appears automatically in every ticket. Only staff can use it. Both the button and the /move command open a selection menu listing all other available types. After selecting, the channel is moved to the new category, permissions (including staffRoles) are updated, and a message is posted in the ticket.
"staffReminder": {
"enabled": true,
"afterHours": 4, // Send reminder after X hours without any message
"pingRoles": true // Whether to @mention the staff roles of the ticket type
}The bot checks all open tickets every 15 minutes. Each ticket is only reminded once — no spam.
"ratingSystem": {
"enabled": true,
"dmUser": true,
"ratingsChannelId": "CHANNEL_ID_HERE" // Channel where ratings are posted automatically
}After closing, the ticket creator receives a 1–5 ⭐ rating request via DM. Once they rate, the result is automatically posted to ratingsChannelId.
"autoClose": {
"enabled": true,
"inactiveHours": 48, // Close after N hours without activity
"warnBeforeHours": 6, // Send a warning N hours beforehand
"excludeClaimed": true // Exclude claimed tickets from auto-close
}/stats shows server-wide numbers. /stats @user shows a detailed profile split into two sections — 👤 As a User (tickets opened, favourite type, average rating given) and 🛡️ As Staff (tickets closed & claimed, average rating received — only shown if applicable).
The SQLite database is created automatically at data/tickets.db. Existing databases are automatically migrated if columns are missing.
| Table | Contents |
|---|---|
tickets |
All tickets: status, type, priority, claim info, reminder, transcript |
blacklist |
Blocked users with reason and timestamp |
staff_notes |
Private staff notes per ticket |
ratings |
Ratings (1–5 ⭐) with optional comment |
- Copy
locales/de.json, e.g. aslocales/fr.json - Translate all strings
- Set
"lang": "fr"inconfig/config.jsonc
AGPL-3.0 — Source code must remain open and be published under the same license when distributed or hosted.