# esx_whitelist (/docs/core/esx_whitelist)

`esx_whitelist` controls who can connect to an ESX Legacy server. It checks players during `playerConnecting` against a database identifier list, Discord guild roles, or both, and adds an admin-only maintenance mode, an emergency bypass list, Discord webhook logging and an in-game admin panel. It ships disabled, so installing it changes nothing until you enable it.

## Overview [#overview]

| Fact | Value |
| --- | --- |
| Resource | `esx_whitelist` |
| Side | Server (connection check) + client (admin panel NUI) |
| Dependencies | `oxmysql`, `esx_lib`, `es_extended` |
| Database | `esx_whitelist_config`, `esx_whitelist_identifiers` (created automatically) |
| Version | 1.16.3 (shipped with ESX Legacy 1.16.4) |

## Installation [#installation]

<Steps>
<Step>
### Import the SQL (optional) [#import-the-sql-optional]

Import `whitelist.sql`. The resource also creates missing tables and indexes on start, so this step is optional.
</Step>
<Step>
### Add convars and ensure the resource [#add-convars-and-ensure-the-resource]

Secrets must use `set` (server only). UI convars must use `setr` so the client can read them.

```bash title="server.cfg"
set discord:botToken "YOUR_DISCORD_BOT_TOKEN"            # only for discord/both mode
set whitelist:webhook "https://discord.com/api/webhooks/..." # optional logging
setr esx:ui:logoUrl "https://example.com/logo.png"         # optional panel logo

ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_whitelist
```
</Step>
<Step>
### Configure and enable [#configure-and-enable]

Edit `config/main.lua`: pick a `Mode`, add identifiers or Discord roles, then set `Config.Whitelist.Enabled = true`. You can also do all of this later in the `/whitelist` panel.
</Step>
</Steps>

<Callout type="warn" title="Never put the bot token in config or client files">
The token is read only from the `discord:botToken` convar. Keep it out of `config/main.lua` (it is a shared script and is sent to clients) and out of public repositories.
</Callout>

## How a connection is checked [#how-a-connection-is-checked]

Every connection runs through the same order. The first matching rule decides:

```txt
0. Whitelist disabled            -> allow
1. Bypass identifiers            -> allow        (method: bypass)
2. Admin-only mode enabled       -> allow/deny   (skips everything below)
3. Cached admin group            -> allow        (method: admin_group)
4. Mode-specific check           -> allow/deny
5. Default                       -> deny         (fail closed)
```

| Mode | Behavior |
| --- | --- |
| `identifier` | One of the player's identifiers is in the `whitelist` list. |
| `discord` | The player has a linked Discord identifier, is a member of `GuildId`, and has at least one role from `AllowedRoles`. |
| `both` + `CombinationMode = 'or'` | Identifier match **or** Discord verification. |
| `both` + `CombinationMode = 'and'` | Identifier match **and** Discord verification. |

Admin groups are read from the `group` column of the ESX `users` table (matched via the `esx:identifier` convar, default `license`) and updated live on `esx:playerLoaded` and `esx:setGroup`. If the database is not ready within 15 seconds, the player is rejected with a verification error.

## Configuration [#configuration]

All keys live in `config/main.lua`. Values changed in the panel are saved to `esx_whitelist_config` and **override** the file.

### General [#general]

| Key | Default | Description |
| --- | --- | --- |
| `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. Only `en` ships with the resource. |
| `Config.IdentifierTypes` | `{ license2 = true, license = true, fivem = true }` | Identifier prefixes accepted for access lists. `ip` is always rejected. |
| `Config.Debug` | `false` | Print extra logs to the server console. |

### Config.Whitelist [#configwhitelist]

| Key | Default | Description |
| --- | --- | --- |
| `Enabled` | `false` | Master switch. When `false`, everyone may connect and no checks run. |
| `Mode` | `'identifier'` | `'discord'`, `'identifier'` or `'both'`. |
| `CombinationMode` | `'or'` | How `both` combines the checks: `'or'` or `'and'`. |
| `AllowedIdentifiers` | `{}` | Initial identifier list, imported **once** into `esx_whitelist_identifiers` (type `whitelist`). |

### Config.Discord [#configdiscord]

| Key | Default | Description |
| --- | --- | --- |
| `Enabled` | `false` | Perform Discord verification. |
| `GuildId` | `''` | Discord server id the player must be a member of. |
| `AllowedRoles` | `{}` | Role ids; the member needs at least one. |
| `Timeout` | `5000` | Per-request HTTP timeout (ms). |
| `CacheTTL` | `60` | Seconds a verified result may be reused. `0` disables the cache. |
| `CacheMaxEntries` | `10000` | Max cached Discord results. |
| `RequestsPerSecond` | `40` | Outbound request smoothing, in addition to Discord rate-limit headers. |
| `MaxRetries` | `2` | Retries for timeouts, 5xx and network errors. |
| `RetryBaseDelay` | `750` | First retry delay (ms), doubled per attempt. |
| `RetryMaxDelay` | `4000` | Max retry delay (ms). |

### Config.Admin [#configadmin]

| Key | Default | Description |
| --- | --- | --- |
| `Groups` | `{ admin = true, superadmin = true, owner = true }` | ESX groups that may use the panel and commands, and that bypass the normal whitelist. |
| `IdentifierPriority` | `{ 'license2', 'license', 'fivem' }` | Order used to pick the stable identifier for cache keys. |
| `Cache.Enabled` | `true` | Enable the admin group cache. |
| `Cache.SaveDelay` | `5000` | Kept for panel compatibility. Admin decisions are not persisted. |

### Config.AdminOnly [#configadminonly]

| Key | Default | Description |
| --- | --- | --- |
| `Enabled` | `false` | Maintenance mode: only these groups or identifiers may connect. |
| `Groups` | `{ admin = true, superadmin = true, owner = true }` | Groups allowed in admin-only mode. |
| `AllowedIdentifiers` | `{}` | Identifiers allowed in admin-only mode (imported once, type `admin_only`). |

### Config.Bypass [#configbypass]

| Key | Default | Description |
| --- | --- | --- |
| `AllowedIdentifiers` | `{}` | Emergency list checked before everything else, even admin-only mode (imported once, type `bypass`). Keep it tiny. |

### Config.Logging [#configlogging]

The webhook URL comes from the `whitelist:webhook` convar.

| Key | Default | Description |
| --- | --- | --- |
| `Enabled` | `true` | Send events to the webhook (only if the convar is set). |
| `MinInterval` | `1200` | Minimum ms between webhook requests. |
| `BatchSize` | `5` | Events sent per cycle. |
| `QueueLimit` | `250` | Max queued events; the oldest are dropped. |
| `Levels` | `{ info = true, warning = true, error = true, security = true }` | Severities that are sent. |

### Config.Performance [#configperformance]

| Key | Default | Description |
| --- | --- | --- |
| `DiscordConcurrency` | `10` | Max simultaneous Discord API requests. |
| `DiscordQueueLimit` | `4096` | Max pending Discord verifications. |
| `VerificationTimeout` | `120000` | Hard limit (ms) for one connection check. |
| `PanelRateLimit` | `10` | Panel actions allowed per window. |
| `PanelRateWindow` | `5000` | Rate-limit window (ms). |

### Config.Panel [#configpanel]

| Key | Default | Description |
| --- | --- | --- |
| `Command` | `'whitelist'` | Command name for the panel and subcommands. |
| `Subcommands` | `Panel = 'panel'`, `Status = 'status'`, `Reload = 'reload'`, `Cache = 'cache'`, `CacheClear = 'clear'`, `Test = 'test'`, `Add = 'add'`, `Remove = 'remove'` | Names of the subcommands. |

## Commands [#commands]

All commands work in the server console. In-game, the player's ESX group must be in `Config.Admin.Groups`; unauthorized attempts are logged as security events.

| Command | Description |
| --- | --- |
| `/whitelist` or `/whitelist panel` | Open the admin panel (in-game only). |
| `whitelist status` | Mode, admin-only state, Discord readiness and decision counters. |
| `whitelist reload` | Reload runtime config and admin cache from the database, clear the Discord cache. |
| `whitelist cache` | Cache and queue statistics. |
| `whitelist cache clear` | Clear the Discord verification cache. |
| `whitelist test <serverId>` | Run the full check against an online player. |
| `whitelist add <identifier> [type]` | Add an identifier. `type` is `whitelist` (default), `admin_only` or `bypass`. |
| `whitelist remove <identifier> [type]` | Remove an identifier from that list. |

```bash title="Server console"
whitelist add license2:1234567890abcdef1234567890abcdef12345678
whitelist add license:abcdef1234567890abcdef1234567890abcdef12 bypass
whitelist status
```

## Admin panel [#admin-panel]

`/whitelist` opens a NUI panel with the tabs Dashboard, Whitelist, Discord, Admins and Logging & Performance. Changes are validated on the server, rate limited, persisted to `esx_whitelist_config` and logged. The panel refuses changes to admin groups that would remove your own current group, so you cannot lock yourself out. Theme convars: `esx:ui:logoUrl`, `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor` (all `setr`).

## Discord bot setup [#discord-bot-setup]

1. Create an application and bot at the [Discord developer portal](https://discord.com/developers/applications).
2. Invite the bot to your guild with the `bot` scope. No special permissions are required; role checks use the guild member endpoint.
3. Enable Developer Mode in Discord, then copy the guild id and role ids into `GuildId` and `AllowedRoles` (or the panel).
4. Set `Config.Discord.Enabled = true` and `Config.Whitelist.Mode = 'discord'` or `'both'`.

Players must have Discord running and linked to FiveM, otherwise they have no `discord:` identifier and are denied.

## Database schema [#database-schema]

```sql title="whitelist.sql"
CREATE TABLE IF NOT EXISTS `esx_whitelist_config` (
  `setting_key` VARCHAR(64) NOT NULL,
  `setting_value` LONGTEXT NOT NULL,
  `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `esx_whitelist_identifiers` (
  `identifier` VARCHAR(128) NOT NULL,
  `type` VARCHAR(32) NOT NULL DEFAULT 'whitelist',
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`identifier`, `type`),
  KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

`type` is one of `whitelist`, `admin_only` or `bypass`.

## Exports and events [#exports-and-events]

esx_whitelist has no public exports. Its net events (prefix `esxwl:`) are internal to the panel and command bridge. It listens to `playerConnecting`, `playerDropped`, `esx:playerLoaded` and `esx:setGroup`.

## Common issues [#common-issues]

<Accordions>
<Accordion title="Everyone can still join">
`Config.Whitelist.Enabled` is `false` by default. Enable it in the config or panel, then check `whitelist status`. A value saved in the panel wins over the file.
</Accordion>
<Accordion title="Identifiers added to AllowedIdentifiers are ignored">
The config lists are imported into the database only on the first start. After that, use `whitelist add` or the panel. Also make sure the prefix is allowed by `Config.IdentifierTypes` (default `license2`, `license`, `fivem`).
</Accordion>
<Accordion title="Discord players are always denied">
Check `whitelist status` for `Discord: NOT READY`. Common causes: missing or wrong `discord:botToken`, bot not in the guild, wrong `GuildId`, `Config.Discord.Enabled = false`, or the player has no linked Discord identifier. Discord failures fail closed.
</Accordion>
<Accordion title="Locked out in admin-only mode">
Add your identifier from the server console: `whitelist add license2:... bypass`. Bypass is checked before admin-only mode.
</Accordion>
</Accordions>

## Related [#related]

- [Core resources overview](/docs/core)
- [Admin permissions](/docs/guides/admin-permissions)
- [server.cfg](/docs/getting-started/server-cfg)
- [Database schema](/docs/guides/database-schema)
