Frostyh

SmartWhitelist

Whitelist applications in game, on the web or on Discord, with automatic checks, a score, staff review and join waves.

Price
Coming soon
Runs on
Paper, Java 21
Version
v1.0.0

Read the docs

SmartWhitelist coverComing soon

In game

See it on a real server

5 screenshots and a short clip, all taken on a real server. Click a screenshot to see it full size.

Clip
SmartWhitelist: limbo form
SmartWhitelist: staff desk
SmartWhitelist: review banned
SmartWhitelist: deny screen
SmartWhitelist: web form

01 / 06

Overview

Run a whitelisted server without drowning in applications: players apply, the plugin checks them, staff decide with one click.

A whitelisted SMP lives on its applications, and most servers still collect them in a Discord channel, copy names by hand and type /whitelist add one by one. SmartWhitelist turns that into one flow. A player who is not on the whitelist is told exactly how to apply (or applies right there in a dialog), every application gets automatic checks and a score from 0 to 100, and staff accept or deny in game or with Discord buttons. Accepting adds the player to the whitelist, at once or with the next wave.

The vanilla whitelist stays on and stays your real list, so the server is still closed if you ever remove the plugin.

In short

  • Three ways to apply from one question list: an in-game dialog, a small web page the plugin serves itself, or a Discord form
  • Automatic checks with a score, so the obvious yes and no decide themselves and staff look at the rest
  • Review in game or on Discord with Accept and Deny buttons, with notes and a full audit log
  • Join waves for a season launch: accepted players get in ten at a time instead of all at once
  • Honest checks: it says what it can and cannot know about an account
Latest version
1.0.0
Runs on
Paper or Purpur, Java 21
BuiltByBit
Coming soon

Features

  1. 01What makes it different

    • A real waiting room (limbo mode): non-whitelisted players join, stand frozen and invisible at one spot, and fill in the form in a native dialog. No chat, no building, no commands except /apply. Accepting releases them straight into the world
    • A kick message that does the work (kick mode): "Apply here: your-link, your code: K7M2QX". The code ties the web form or Discord to the right Minecraft account, so nobody applies for someone else
    • Checks you can trust: shared ban lists (your own file or URLs your network shares), your own bans, other accounts seen on the same connection (stored only as a salted hash), paid account and own skin, how much the player wrote, and for a linked Discord account its exact age and whether it is in your server
    • No fake checks: Mojang does not publish when a Minecraft account was made or its old names, so SmartWhitelist does not pretend to check them
  2. 02What you get

    • Your own questions: up to 10, as one-line text, longer text, a choice or a yes/no box, with required, minimum and maximum length. The same list drives all three forms
    • Kick mode or limbo mode, or off when you only take applications through Discord
    • Web form: a clean page served by the plugin on a port you choose (put it behind your website's proxy for HTTPS), with rate limits and size limits
    • Discord, optional: webhook mode posts every application and decision to a channel; bot mode uses your own bot to post applications with Accept and Deny buttons and lets players link their Discord account with /apply and their code. No privileged intents needed
    • Score and checks: every check and its points are in config.yml. Optional automatic accept above a score, automatic deny below one, and automatic deny for anyone on a ban list
    • Staff desk (/swl): waiting applications with their score, a review screen with every check and answer, deny with a reason the player sees, notes only staff see, the queue and the audit log
    • Cooldown: a denied player sees the reason and when they may apply again
    • Join waves: accepted players queue and get in N at a time on a timer, or when you press the button, with a welcome message for each group
    • Every word is yours: all messages, labels, the web page text and the Discord text in config.yml, in MiniMessage, with one colour theme

Installation

Requirements

Server
Paper, or a Paper fork such as Purpur. Spigot and CraftBukkit won't work, because it uses Paper-only APIs.
Minecraft
plugin.yml api-version 1.21; built against Paper API 1.21.11
Java
21 or newer
Needs
No other plugin
Works with
Nothing optional
Libraries
org.xerial:sqlite-jdbc:3.53.2.1, net.dv8tion:JDA:6.7.0, downloaded by the server on first start

Steps

  1. Download SmartWhitelist.jar from BuiltByBit. There's no licence key to enter.
  2. Stop the server and put the jar in the plugins folder.
  3. Start the server. The plugin writes plugins/SmartWhitelist/config.yml with the defaults listed under Configuration.
  4. Change config.yml to fit your server.
  5. Run /swl reload (read config.yml again) to apply your changes, or restart the server.

Setup guide

  1. Drop SmartWhitelist.jar into /plugins and restart. The server downloads the SQLite and Discord libraries on the first start.
  2. Run /whitelist on.
  3. Pick kick or limbo mode in config.yml, write your questions, and turn on the web form or Discord if you want them.
  4. Run /swl reload and open /swl.

Commands

Straight from plugin.yml. Where the permission column says "not set", plugin.yml puts no permission on the command itself; the nodes the plugin checks are under Permissions.

CommandAliasesUsageWhat it doesPermission
/swl/smartwhitelist /applications/swl [pending|review|accept|deny|note|queue|wave|log|banlist|reload|help]The application desk for staff.not set
/applynone/applyFill in your application (while you wait to be let in).not set

Every subcommand

/swl                              The staff desk (a dialog).
/swl pending                      Applications waiting for a decision.
/swl review <name|#id>            One application with its checks and answers.
/swl accept <name|#id>            Accept (whitelist now, or queue when waves are on).
/swl deny <name|#id> [reason]     Deny with a reason the player sees.
/swl note <name|#id> <text>       A note only staff see.
/swl queue                        Accepted players waiting for the next wave.
/swl wave                         Let the next wave in now.
/swl log                          The audit log.
/swl banlist                      Read the ban list file and URLs again.
/swl reload                       Read config.yml again.
/apply                            For players in the waiting room: open the form.

Everything except the dialogs works from the console.

Permissions

From plugin.yml.

NodeDefaultWhat it allowsIncludes
smartwhitelist.reviewoperatorsReview, accept, deny and add notes to applications.none
smartwhitelist.adminoperatorsEverything, plus waves, the ban list and reload.smartwhitelist.review

Placeholders

This plugin doesn't add any PlaceholderAPI placeholders.

Configuration

The default files exactly as they ship in the jar, comments included. The plugin writes them to plugins/SmartWhitelist/ the first time it starts.

config.yml

KeyDefaultThe comment above it
gatesection, 3 keysnone
applysection, 6 keysnone
checkssection, 8 keysnone
banlistsection, 3 keys███ ██ █ █ █ ███ ███ ███ █ █ █ █ ██ █ █ █ █ █ ███ ████ █ ██ █ █ ██ █ █ █ █ █ █ █ █ █ █ █ ███ █ █ █ █ ████ ███ ███ █ BAN LIST: players already banned elsewhere. One per line: a UUID or name, optionally " | reason".
wavessection, 4 keys█ █ ██ █ █ ████ ███ █ █ █ █ █ █ █ █ █ █ █ ████ █ █ ███ ██ ██ ██ █ █ ██ █ █ █ █ █ █ █ ████ ███ WAVES: accepted players join in groups instead of all at once (good for a season launch).
websection, 7 keys█ █ ████ ███ ████ ██ ███ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ ███ ███ ███ █ █ ███ █ █ █ ██ ██ █ █ █ █ █ █ █ █ █ █ █ █ ████ ███ █ ██ █ █ █ █ WEB FORM: a small application page served by the plugin (plain HTTP). For HTTPS put it behind the reverse proxy of...
discordsection, 8 keys███ ███ ███ ███ ██ ███ ███ █ █ █ █ █ █ █ █ █ █ █ █ █ █ ██ █ █ █ ███ █ █ █ █ █ █ █ █ █ █ █ █ █ ███ ███ ███ ███ ██ █ █ ███ DISCORD: optional. off, webhook (posts only) or bot (posts with Accept/Deny buttons, /apply).
themesection, 12 keys███ █ █ ████ █ █ ████
menusection, 2 keysnone
messagessection, 196 keys█ █ ████ ███ ███ ██ ███ ████ ███ ██ ██ █ █ █ █ █ █ █ █ █ █ █ ███ ██ ██ ████ █ ██ ███ ██ █ █ █ █ █ █ █ █ █ █ █ █ █ ████ ███ ███ █ █ ███ ████ ███ MESSAGES: MiniMessage with the theme tags <accent> <text> <muted> <value> <good> <warn> <bad>. An empty message...
plugins/SmartWhitelist/config.yml465 lines
# ------------------------------------------------------------------------------------------------
#  SmartWhitelist by Frostyh
#  Applications for whitelisted servers. Players who are not on the whitelist apply in game, on a
#  small web page or through Discord; every application gets automatic checks and a score; staff
#  accept or deny with /swl or Discord buttons, with notes and a full audit log.
#
#  The vanilla whitelist stays ON and stays the real list: accepting adds the player to it, so the
#  server is still closed if this plugin is ever removed. Run /whitelist on before you start.
# ------------------------------------------------------------------------------------------------

# ============================================================================
#   ███  ██  ███ ████
#  █    █  █  █  █
#  █ ██ ████  █  ███
#  █  █ █  █  █  █
#   ███ █  █  █  ████
#  GATE: what happens to someone who is not on the whitelist.
gate:
  # kick:  turned away with a message that says how to apply (web link and code, or the Discord
  #        command), or where their application stands.
  # limbo: let in to a frozen waiting room where they fill in the form in a dialog. They cannot
  #        move, chat, build, take damage, see anyone or use other commands until accepted.
  # off:   SmartWhitelist does not touch joins (applications can still come in from Discord).
  # Keep the quotes on "off": unquoted, YAML reads it as false.
  mode: "kick"
  # true: only act while the vanilla whitelist is on. With it off the server is open on purpose.
  require-vanilla-whitelist: true
  limbo:
    # world,x,y,z[,yaw,pitch]. Empty: the main world's spawn.
    location: ""
    hide-players: true
    # Where an accepted player goes: spawn (the main world's spawn) or stay (where they waited).
    release-to: spawn
    # The only commands a waiting player can use.
    allowed-commands: [apply]

# ============================================================================
#   ██  ███  ███  █    █ █
#  █  █ █  █ █  █ █    █ █
#  ████ ███  ███  █     █
#  █  █ █    █    █     █
#  █  █ █    █    ████  █
#  APPLY: the form, the cooldown after a denial and the apply codes.
apply:
  # Hours a denied player waits before they may apply again. 0: straight away.
  cooldown-after-deny-hours: 72
  # How long a code from the kick message stays valid, in minutes.
  code-minutes: 60
  # The reason filled in on the deny screen; staff can change it.
  default-deny-reason: "Thanks for applying. We are not taking you on right now."
  # Lines shown above the form in game.
  intro:
    - "<title>Welcome, {player}.</title>"
    - "<text>This server is whitelisted. Answer a few questions and staff will look at your application.</text>"
  # Shown as a short list on the form. Leave empty to hide.
  rules:
    - "Be respectful to everyone."
    - "No cheats, x-ray or duplication."
    - "No griefing or stealing."
  # Up to 10 questions. type: text (one line), paragraph (several lines), choice (pick one of
  # options) or toggle (a yes/no box). min and max are characters. The same list drives the in-game
  # form, the web page and the Discord form (Discord fits 5 questions; with more, players answer
  # in game or on the web).
  questions:
    - id: age
      label: "How old are you?"
      type: text
      required: true
      max: 3
    - id: why
      label: "Why do you want to join?"
      type: paragraph
      required: true
      min: 30
      max: 600
    - id: found
      label: "How did you find us?"
      type: choice
      options: ["A friend", "YouTube or TikTok", "A server list", "Somewhere else"]
    - id: rules
      label: "I have read and accept the rules"
      type: toggle
      required: true

# ============================================================================
#   ███ █  █ ████  ███ █  █  ███
#  █    █  █ █    █    █ █  █
#  █    ████ ███  █    ██    ██
#  █    █  █ █    █    █ █     █
#   ███ █  █ ████  ███ █  █ ███
#  CHECKS: the automatic checks and the score (0 to 100).
#
#  What is checked: shared ban lists and your own bans, other accounts on the same address (stored
#  hashed with a salt that never leaves the server), whether the name is a paid account (on
#  offline-mode servers), whether the account has its own skin, how much the player wrote, and for
#  a linked Discord account its age and membership.
#  What is NOT checked: Minecraft account age and old names. Mojang does not publish either.
checks:
  base: 50
  points:
    on-ban-list: -100
    banned-here: -100
    address-banned: -50
    address-denied: -15
    address-shared: -5
    paid-account: 10
    not-paid: -20
    own-skin: 5
    default-skin: -5
    good-answers: 10
    poor-answers: -10
    discord-linked: 10
    discord-old: 10
    discord-new: -15
    discord-member: 5
    discord-not-member: -5
    discord-missing: -20
  answers:
    # Characters written across the text questions.
    good-length: 120
    poor-length: 40
  discord:
    # A Discord account at least this many days old counts as established...
    min-days: 30
    # ...and one younger than this counts as new.
    new-days: 7
  address:
    # Needs the players' real addresses: behind Velocity or BungeeCord, turn on IP forwarding.
    # Loopback and LAN addresses (a proxy without forwarding) are never compared.
    enabled: true
  # Deny anyone on a ban list straight away (they can still be accepted by hand later).
  auto-deny-ban-list: true
  # Accept automatically at or above this score. 0: never, staff decide.
  auto-accept-at: 0
  # Deny automatically below this score. 0: never.
  auto-deny-below: 0

# ============================================================================
#  ███   ██  █  █    █    ███  ███ ███
#  █  █ █  █ ██ █    █     █  █     █
#  ███  ████ █ ██    █     █   ██   █
#  █  █ █  █ █  █    █     █     █  █
#  ███  █  █ █  █    ████ ███ ███   █
#  BAN LIST: players already banned elsewhere. One per line: a UUID or name, optionally " | reason".
banlist:
  # In plugins/SmartWhitelist/.
  file: banlist.txt
  # Plain-text lists in the same format, fetched in the background (for example a list your
  # network shares between servers).
  urls: []
  refresh-minutes: 60

# ============================================================================
#  █   █  ██  █  █ ████  ███
#  █   █ █  █ █  █ █    █
#  █ █ █ ████ █  █ ███   ██
#  ██ ██ █  █  ██  █       █
#  █   █ █  █  █   ████ ███
#  WAVES: accepted players join in groups instead of all at once (good for a season launch).
waves:
  enabled: false
  # How many players each wave lets in.
  size: 10
  # Minutes between waves. /swl wave runs one now.
  every-minutes: 60
  announce: true

# ============================================================================
#  █   █ ████ ███     ████  ██  ███  █   █
#  █   █ █    █  █    █    █  █ █  █ ██ ██
#  █ █ █ ███  ███     ███  █  █ ███  █ █ █
#  ██ ██ █    █  █    █    █  █ █ █  █   █
#  █   █ ████ ███     █     ██  █  █ █   █
#  WEB FORM: a small application page served by the plugin (plain HTTP). For HTTPS put it behind
#  the reverse proxy of your website and set public-url to that address.
web:
  enabled: false
  bind: "0.0.0.0"
  port: 8765
  # The address players open, without a trailing slash. Shown in the kick message.
  public-url: "http://play.example.com:8765"
  server-name: "our server"
  max-forms-per-hour: 6
  # Read the visitor's address from X-Forwarded-For (only behind your own reverse proxy).
  trust-proxy-header: false

# ============================================================================
#  ███  ███  ███  ███  ██  ███  ███
#  █  █  █  █    █    █  █ █  █ █  █
#  █  █  █   ██  █    █  █ ███  █  █
#  █  █  █     █ █    █  █ █ █  █  █
#  ███  ███ ███   ███  ██  █  █ ███
#  DISCORD: optional. off, webhook (posts only) or bot (posts with Accept/Deny buttons, /apply).
discord:
  # Quoted on purpose: an unquoted off is read by YAML as false.
  mode: "off"
  # Shown in the kick message so players know where to apply.
  invite: "https://discord.gg/your-invite"
  webhook-url: ""
  webhook-name: "SmartWhitelist"
  # A picture of the player in the post. {uuid} and {name} are filled in. Empty: no picture.
  avatar-url: "https://mc-heads.net/avatar/{uuid}/64"
  # true: an application without a linked Discord account loses points (bot mode only).
  require-link: false
  default-deny-reason: "Denied on Discord."
  bot:
    # Your own bot's token (discord.com/developers). It needs no privileged intents.
    token: ""
    guild-id: ""
    # The channel applications are posted in.
    channel-id: ""
    # Members with this role (or Manage Server) can press Accept and Deny.
    staff-role-id: ""

# ============================================================================
#  ███ █  █ ████ █   █ ████
#   █  █  █ █    ██ ██ █
#   █  ████ ███  █ █ █ ███
#   █  █  █ █    █   █ █
#   █  █  █ ████ █   █ ████
#  THEME: the colours every message and screen uses.
theme:
  accent: "#4fd1a5"
  accent2: "#a7f0d6"
  title: "#f2fbf7"
  text: "#c4d6cf"
  muted: "#6e8a80"
  value: "#ffffff"
  good: "#57e08a"
  warn: "#ffc857"
  bad: "#ff6b6b"
  sounds: true
  item-icons: true
  small-caps: false

menu:
  width: 320
  body-width: 360

# ============================================================================
#  █   █ ████  ███  ███  ██   ███ ████  ███
#  ██ ██ █    █    █    █  █ █    █    █
#  █ █ █ ███   ██   ██  ████ █ ██ ███   ██
#  █   █ █       █    █ █  █ █  █ █       █
#  █   █ ████ ███  ███  █  █  ███ ████ ███
#  MESSAGES: MiniMessage with the theme tags <accent> <text> <muted> <value> <good> <warn> <bad>.
#  An empty message is not sent at all.
messages:
  prefix: "<accent>Whitelist</accent> <muted>»</muted> "

  # ---- turned away at the door (kick mode) and the waiting room (limbo mode)
  kick-apply-web: "<title>This server is whitelisted.</title><newline><newline><text>Apply here:</text> <accent>{url}</accent><newline><text>Your code:</text> <value>{code}</value><newline><newline><muted>The code works for one hour.</muted>"
  kick-apply-discord: "<title>This server is whitelisted.</title><newline><newline><text>Join our Discord and type</text> <value>/apply {code}</value><newline><accent>{invite}</accent><newline><newline><muted>The code works for one hour.</muted>"
  kick-apply-other: "<title>This server is whitelisted.</title><newline><newline><text>Ask staff on our Discord to be let in:</text><newline><accent>{invite}</accent>"
  kick-pending: "<title>Your application is being reviewed.</title><newline><newline><text>Application</text> <value>#{id}</value><text>. You will be able to join as soon as staff accept it.</text>"
  kick-queued: "<title>You are accepted.</title><newline><newline><text>You are</text> <value>#{position}</value> <text>in the queue. Next group joins in</text> <value>{next}</value><text>.</text>"
  kick-denied: "<title>Your application was not accepted.</title><newline><newline><text>{reason}</text><newline><newline><muted>You can apply again in {again}.</muted>"
  limbo-welcome: "<text>Fill in the form to apply. Closed it? Type</text> <accent>/apply</accent><text>.</text>"
  limbo-no-chat: "<warn>Chat opens once your application is accepted.</warn>"
  limbo-no-commands: "<warn>Only</warn> <accent>/apply</accent> <warn>works while you wait.</warn>"

  # ---- the form
  form-title: "<title>Apply to {server}</title>"
  form-rules-head: "<accent>Rules</accent>"
  form-rules-row: "<muted>•</muted> <text>{rule}</text>"
  form-error: "<bad>{error}</bad>"
  btn-send: "<white><icon:apply></white><accent>Send application</accent>"
  btn-send-tip: "<text>Staff will see your answers.</text>"
  btn-later: "<text>Later</text>"
  invalid-required: "Please answer: {question}"
  invalid-required-toggle: "Please tick: {question}"
  invalid-short: "That answer is too short: {question}"
  invalid-long: "That answer is too long: {question}"
  invalid-choice: "Pick one of the options: {question}"
  form-sent: "<good>Application #{id} sent.</good> <text>You will hear back here.</text>"
  already-in: "<text>You are already on the whitelist.</text>"
  cooldown: "<warn>You can apply again in {again}.</warn>"
  status-title: "<title>Your application</title>"
  status-pending: "<text>Application</text> <value>#{id}</value> <text>is waiting for staff (sent {when} ago).</text>"
  status-queued: "<good>You are accepted.</good> <text>You are</text> <value>#{position}</value> <text>in the queue; the next group joins in</text> <value>{next}</value><text>.</text>"
  status-hint: "<muted>You can close this. Your place is kept.</muted>"
  you-accepted: "<good>You are accepted. Welcome!</good>"
  you-queued: "<good>You are accepted.</good> <text>You will be let in with the next group.</text>"
  waves-manual: "when staff open it"

  # ---- staff: notices
  staff-new: "<text>New application from</text> <value>{name}</value> <muted>(#{id}, score {score})</muted><text>.</text> <accent><click:run_command:'/swl review #{id}'>[review]</click></accent>"
  staff-accepted: "<value>{name}</value> <text>was accepted by</text> <value>{by}</value><text>.</text>"
  staff-queued: "<value>{name}</value> <text>was accepted by</text> <value>{by}</value> <text>and waits for the next wave.</text>"
  staff-denied: "<value>{name}</value> <text>was denied by</text> <value>{by}</value><text>.</text>"
  wave-broadcast: "<good>Welcome to the server:</good> <value>{names}</value> <muted>({count})</muted>"

  # ---- staff: the desk
  home-title: "<title>Applications</title>"
  home-counts: "<value>{pending}</value> <text>waiting</text> <muted>·</muted> <value>{queued}</value> <text>queued</text> <muted>·</muted> <good>{accepted}</good> <text>accepted and</text> <bad>{denied}</bad> <text>denied today</text>"
  home-gate: "<muted>Gate</muted> <value>{mode}</value> <muted>· web form</muted> <value>{web}</value> <muted>· Discord</muted> <value>{discord}</value> <muted>· ban list</muted> <value>{bans}</value> <muted>· waiting room</muted> <value>{waiting}</value>"
  home-waves: "<muted>Waves of</muted> <value>{size}</value><muted>, next in</muted> <value>{next}</value>"
  word-on: "on"
  word-off: "off"
  word-yes: "yes"
  word-no: "no"
  btn-pending: "<white><icon:pending></white><accent>Waiting</accent> <value>{count}</value>"
  btn-pending-tip: "<text>Applications staff have not decided yet.</text>"
  btn-queue: "<white><icon:queue></white><accent>Queue</accent> <value>{count}</value>"
  btn-queue-tip: "<text>Accepted players waiting for the next wave.</text>"
  btn-log: "<white><icon:log></white><accent>Audit log</accent>"
  btn-log-tip: "<text>Every application, decision and note, newest first.</text>"
  btn-banlist: "<white><icon:ban></white><accent>Reload ban list</accent>"
  btn-banlist-tip: "<text>Reads banlist.txt and every URL again.</text>"
  btn-help: "<white><icon:help></white><accent>Help</accent>"
  btn-help-tip: "<text>Commands and how the gate works.</text>"
  btn-prev: "<white><icon:back></white><text>Previous</text>"
  btn-prev-tip: "<text>The page before.</text>"
  btn-next: "<white><icon:next></white><text>Next</text>"
  btn-next-tip: "<text>The page after.</text>"
  btn-back: "<white><icon:back></white><text>Back</text>"
  btn-back-tip: "<text>One screen back.</text>"
  btn-close: "<text>Close</text>"
  banlist-reloaded: "<text>Ban list reloaded:</text> <value>{count}</value> <text>entries.</text>"
  pending-title: "<title>Waiting for a decision</title>"
  pending-empty: "<text>Nothing is waiting. New applications show up here and in chat.</text>"
  pending-head: "<value>{count}</value> <text>waiting, oldest first</text> <muted>(page {page} of {pages})</muted>"
  pending-row: "<value>{name}</value> {bar} <muted>{score}</muted>{flag}"
  pending-flag-ban: " <bad>ban</bad>"
  pending-row-tip: "<text>Sent {when} ago via {source}.</text>"
  review-title: "<title>{name}</title>"
  review-head: "<value>{name}</value> <muted>#{id} · via {source} · {when} ago ·</muted> {status}"
  status-word-pending: "<warn>waiting</warn>"
  status-word-accepted: "<good>accepted</good>"
  status-word-queued: "<good>queued</good>"
  status-word-denied: "<bad>denied</bad>"
  review-score: "<text>Score</text> <value>{score}</value> {bar}"
  review-checks-head: "<accent>Checks</accent>"
  review-check-row: "<{tag}>{delta}</{tag}> <text>{label}</text>"
  review-answer: "<accent>{question}</accent><newline><text>{answer}</text>"
  review-notes-head: "<accent>Staff notes</accent>"
  review-note-row: "<value>{author}</value> <muted>{when} ago:</muted> <text>{text}</text>"
  review-decided: "<muted>Decided by</muted> <value>{by}</value><muted>.</muted> <text>{reason}</text>"
  btn-accept: "<white><icon:accept></white><good>Accept</good>"
  btn-accept-tip: "<text>Adds them to the whitelist now.</text>"
  btn-accept-tip-waves: "<text>Puts them in the queue for the next wave.</text>"
  btn-deny: "<white><icon:deny></white><bad>Deny</bad>"
  btn-deny-tip: "<text>Asks for a reason first.</text>"
  btn-note: "<white><icon:note></white><text>Add a note</text>"
  btn-note-tip: "<text>Only staff see notes.</text>"
  already-decided: "<warn>{name} was already decided by someone else.</warn>"
  you-accepted-staff: "<good>Accepted</good> <value>{name}</value><text>.</text>"
  you-queued-staff: "<good>Accepted</good> <value>{name}</value><text>; they join with the next wave.</text>"
  you-denied-staff: "<bad>Denied</bad> <value>{name}</value><text>.</text>"
  deny-title: "<title>Deny {name}</title>"
  deny-body: "<text>The reason is shown to the player. They can apply again after</text> <value>{cooldown}</value> <text>hours.</text>"
  deny-reason-label: "<text>Reason</text>"
  btn-deny-confirm: "<white><icon:deny></white><bad>Deny</bad>"
  btn-deny-confirm-tip: "<text>Sends the reason and kicks them from the waiting room.</text>"
  note-title: "<title>Note on {name}</title>"
  note-label: "<text>Note (only staff see it)</text>"
  btn-note-save: "<white><icon:note></white><accent>Save note</accent>"
  btn-note-save-tip: "<text>Added to the application and the audit log.</text>"
  note-saved: "<text>Note saved on</text> <value>{name}</value><text>.</text>"
  queue-title: "<title>Queue</title>"
  queue-waves-off: "<muted>Waves are off, so accepted players get in straight away. Turn waves.enabled on to queue them.</muted>"
  queue-empty: "<text>Nobody is queued.</text>"
  queue-head: "<value>{count}</value> <text>queued; next wave in</text> <value>{next}</value>"
  queue-row: "<muted>{pos}.</muted> <value>{name}</value> <muted>accepted {when} ago</muted>"
  queue-more: "<muted>and {count} more</muted>"
  btn-wave-now: "<white><icon:wave></white><accent>Let in the next {size}</accent>"
  btn-wave-now-tip: "<text>Runs the next wave right now.</text>"
  wave-done: "<text>Wave done:</text> <value>{count}</value> <text>players whitelisted.</text>"
  log-title: "<title>Audit log</title>"
  log-head: "<accent>Newest first</accent>"
  log-empty: "<text>Nothing yet.</text>"
  log-row: "<muted>{when} ago</muted> <value>{actor}</value> <accent>{action}</accent> <value>{target}</value> <muted>{detail}</muted>"
  help-title: "<title>SmartWhitelist</title>"
  help-lines:
    - "<accent>/swl</accent> <text>opens this desk.</text>"
    - "<accent>/swl review</accent> <value>[name|#id]</value><text>, </text><accent>accept</accent> <value>[name]</value><text>, </text><accent>deny</accent> <value>[name] [reason]</value><text>, </text><accent>note</accent> <value>[name] [text]</value>"
    - "<accent>/swl queue</accent><text>,</text> <accent>wave</accent><text>,</text> <accent>log</accent><text>,</text> <accent>banlist</accent><text>,</text> <accent>reload</accent>"
    - "<text>Players apply with</text> <accent>/apply</accent> <text>in the waiting room (limbo mode), on the web page from their kick message, or with /apply on Discord.</text>"
    - "<text>Accepting adds the player to the vanilla whitelist, so keep</text> <value>/whitelist on</value><text>.</text>"

  # ---- staff: errors and the console
  unknown-application: "<warn>No application found.</warn>"
  no-permission: "<bad>You do not have permission for that.</bad>"
  players-only: "<warn>Only players can do that.</warn>"
  console-usage: "<text>Use /swl pending, review, accept, deny, note, queue, wave, log, banlist or reload.</text>"
  console-pending: "<value>{count}</value> <text>waiting:</text>"
  console-row: "<muted>#{id}</muted> <value>{name}</value> <text>score</text> <value>{score}</value> <muted>{flags}</muted>"
  console-queue: "<value>{count}</value> <text>queued, next wave in</text> <value>{next}</value>"
  usage-review: "<warn>/swl review [name|#id]</warn>"
  usage-accept: "<warn>/swl accept [name|#id]</warn>"
  usage-deny: "<warn>/swl deny [name|#id] [reason]</warn>"
  usage-note: "<warn>/swl note [name|#id] [text]</warn>"
  reloaded: "<good>Reloaded.</good>"
  unknown-sub: "<warn>Unknown. Try: {subs}</warn>"

  # ---- the checks (shown to staff)
  check-banlist: "On a shared ban list ({where})"
  check-local-ban: "Banned on this server"
  check-address-banned: "Same connection as {name}, who is banned"
  check-address-denied: "Same connection as {name}, who was denied"
  check-address-shared: "Shares a connection with {count} other account(s)"
  check-paid-online: "Paid Minecraft account (online mode)"
  check-paid-unknown: "Could not reach Mojang to check the account"
  check-paid-yes: "Paid Minecraft account"
  check-paid-no: "Not a paid Minecraft account"
  check-skin-yes: "Has their own skin"
  check-skin-no: "Default skin"
  check-answers-good: "Wrote a proper application ({chars} characters)"
  check-answers-poor: "Very short answers ({chars} characters)"
  check-discord-linked: "Discord account linked"
  check-discord-old: "Discord account is {days} days old"
  check-discord-new: "Discord account is only {days} days old"
  check-discord-age: "Discord account is {days} days old"
  check-discord-member: "In your Discord server"
  check-discord-not-member: "Not in your Discord server"
  check-discord-missing: "No Discord account linked"
  reason-ban-list: "You are on a ban list this server uses."
  reason-low-score: "Your application did not meet our requirements."

  # ---- Discord (plain text: Discord shows no colours)
  discord-new-title: "New application: {name} (#{id})"
  discord-new-score: "Score {score} of 100, sent via {source}"
  discord-checks: "Checks"
  discord-review-in-game: "Review in game: /swl review {name}"
  discord-review-buttons: "Accept or deny below, or in game with /swl"
  discord-accepted: "{name} (#{id}) was accepted by {by}."
  discord-queued: "{name} (#{id}) was accepted by {by} and joins with the next wave."
  discord-denied: "{name} (#{id}) was denied by {by}. {reason}"
  discord-cmd-description: "Apply to the Minecraft server with the code from your kick message"
  discord-cmd-code: "The code from your kick message"
  discord-btn-accept: "Accept"
  discord-btn-deny: "Deny"
  discord-btn-accepted: "Accepted by {by}"
  discord-btn-denied: "Denied by {by}"
  discord-code-unknown: "That code is unknown or has run out. Join the Minecraft server once more for a new one."
  discord-linked-pending: "Your Discord account is now linked to {name}. Your application is already with staff."
  discord-error: "Something went wrong on our side. Please try again in a minute."
  discord-linked-web: "Linked to {name}. Fill in the application here: {url}"
  discord-linked-game: "Linked to {name}. Join the Minecraft server and type /apply to fill in the form."
  discord-form-title: "Application"
  discord-toggle-hint: "Type yes"
  discord-sent: "Application #{id} for {name} is sent. You will hear back here and in game."
  discord-already-in: "{name} is already on the whitelist."
  discord-cooldown: "You can apply again in {again}."
  discord-invalid: "{detail}"
  discord-not-staff: "Only staff can decide applications."
  discord-already-decided: "Someone already decided this one."

  # ---- the web page (plain text)
  web-title: "Apply to {server}"
  web-intro: "Enter the code from your kick message and answer a few questions. Staff will look at your application."
  web-code-label: "Your code"
  web-send: "Send application"
  web-code-unknown: "That code is unknown or has run out. Join the server once more for a new one."
  web-sent-title: "Application sent"
  web-sent-body: "Thanks, {name}. Application #{id} is with staff. Try joining again later."
  web-already-title: "Already sent"
  web-already-body: "{name}, your application is already with staff."
  web-in-title: "You are in"
  web-in-body: "{name} is already on the whitelist. Just join."
  web-cooldown-title: "Not yet"
  web-cooldown-body: "You can apply again in {again}."
  web-not-found: "Nothing here."
  web-too-many: "Too many forms from your connection. Try again in an hour."
  web-too-big: "That was too much text."

FAQ

Which servers and versions does it run on?

It needs Paper or a Paper fork like Purpur, because it uses Paper-only APIs (the dialog API for its menus, Adventure text components, Paper player profiles). Spigot and CraftBukkit won't work. plugin.yml declares api-version 1.21, the jar is built against Paper API 1.21.11, and it needs Java 21 or newer.

Does it need other plugins?

No, it runs without any other plugin (plugin.yml has no hard dependencies). On first start the server downloads org.xerial:sqlite-jdbc:3.53.2.1, net.dv8tion:JDA:6.7.0, so it needs internet access then.

How do I apply config changes?

Edit plugins/SmartWhitelist/config.yml and run /swl reload (read config.yml again). Restarting works too.

Does it run on Folia, or for Bedrock players?

I haven't tested it on Folia, and plugin.yml doesn't declare folia-supported. I haven't tested Bedrock players through Geyser either.

What if I remove the plugin?

Your server stays closed. Accepted players are on the vanilla whitelist, which never gets switched off.

Does it work behind Velocity or BungeeCord?

Yes, on the backend server. For the same-connection check, turn on IP forwarding; without it every player shares the proxy's address, so that check switches itself off.

Do I need a Discord bot?

No. Discord is optional. Webhook mode needs only a webhook link; bot mode needs your own bot token.

Is there DRM or a licence key?

No. There is nothing to activate and no licence server to reach, and the code is not obfuscated.

Get it

Not on sale yet

SmartWhitelist is finished and waiting for its BuiltByBit listing. This page gets a buy button the day it goes up.

Ask about it on Discord

Also mine

Run a Discord too?

ZeroBot is my Discord bot: moderation, tickets, welcome messages, logs, backups and raid protection, all set up from a web dashboard. It's free to add; Premium plans unlock more.

Its Minecraft Link connects your server to your Discord through the free ZeroBot Link plugin (coming to Modrinth soon): a live status card, a chat bridge, join and death lines, and account linking. Outgoing HTTPS only, no port to open.

See ZeroBot

Custom work

Need something that isn't here? I make custom Paper plugins, change these ones to fit your server, and do server setup and config work. Small plugins start at 50 EUR; bigger jobs get a quote.

How commissions work