Permissions
F5 Board uses logical permission names. When the script needs to check access (/board admin gate, /badmin, force-delete), it asks for a logical name like admin. The permission block in framework_config.lua then decides which real identifier in which permission system gets checked.
This page covers all four mechanisms and how they compose.
TL;DR — Stock Behaviour
Out of the box, with the default configuration, "admin" maps to:
| Framework | Check |
|---|---|
| QBCore | QBCore.Functions.HasPermission(src, 'admin') |
| QBox | IsPlayerAceAllowed(src, 'admin') |
| ESX | xPlayer.getGroup() ∈ { 'admin', 'superadmin' } |
| ox_core | IsPlayerAceAllowed(src, 'admin') |
So if you already manage admins via your framework's normal mechanism, you don't need to touch this config — just make sure the admin permission/group exists.
The Permissions Config Block
Config.Permissions = {
mode = 'auto', -- 'auto' | 'aceOnly'
perFramework = {
esx = nil, -- nil | 'auto' | 'aceOnly'
qb = nil,
qbx = nil,
ox = nil,
},
aceFallback = false, -- true → fall back to ACE when native returns false
nodes = {
admin = {
ace = 'admin',
esx = { 'admin', 'superadmin' },
qb = 'admin',
qbx = 'admin',
ox = 'admin',
},
},
}
| Field | Purpose |
|---|---|
mode | Global mode — 'auto' (framework-native, optionally with ACE fallback) or 'aceOnly' (ACE only) |
perFramework.<key> | Per-framework override of mode. nil = use the global mode |
aceFallback | In 'auto' mode, if the native check returns false, also try ACE. OR semantics |
nodes | Maps each logical permission name to its real identifier in each system |
Mode 1 — Framework-Native (default)
Config.Permissions = {
mode = 'auto',
aceFallback = false,
}
Each framework uses its own native mechanism. This is the recommended setup if you already manage admins through your framework's own system.
| Framework | Native mechanism |
|---|---|
| ESX | xPlayer.getGroup() compared against nodes.<logical>.esx (single string or list) |
| QBCore | QBCore.Functions.HasPermission(src, nodes.<logical>.qb) |
| QBX | IsPlayerAceAllowed(src, nodes.<logical>.qbx) (QBX is ACE-based by design) |
| ox_core | IsPlayerAceAllowed(src, nodes.<logical>.ox) (ox_core is ACE-based) |
For ESX, nodes.admin.esx = { 'admin', 'superadmin' } means OR — any of those groups passes the check.
Mode 2 — ACE-Only
Config.Permissions = {
mode = 'aceOnly',
}
The framework's native check is bypassed entirely. Only IsPlayerAceAllowed(src, nodes.<logical>.ace) is consulted.
Use this when you manage permissions purely through add_ace / add_principal in server.cfg and don't want to use the framework's internal system (e.g. the users.group column on ESX).
add_ace group.admin admin allow
add_ace group.moderator moderator allow
add_principal identifier.steam:11000010abcdef12 group.admin
add_principal identifier.discord:123456789012345678 group.moderator
Mode 3 — ACE Fallback
Config.Permissions = {
mode = 'auto',
aceFallback = true,
}
In 'auto' mode, if the framework-native check returns false, the bridge also tries IsPlayerAceAllowed. OR semantics — either passing grants access.
This is useful when you have a mix of admins:
- Some admins are flagged through the framework (
users.group = 'admin'on ESX, or QBCore's permissions table) - Some staff have
add_principal identifier.X group.admininserver.cfg
Both work without changes.
falseBy default aceFallback = false, so ESX uses its native groups as the single source of truth. Flip it to true if you also want add_principal identifier.X group.admin to pass.
aceFallback has no effect under mode = 'aceOnly' — that mode already uses ACE exclusively.
Mode 4 — Per-Framework Override
Config.Permissions = {
mode = 'auto',
perFramework = {
esx = 'aceOnly', -- on ESX, use ACE only
qb = 'auto', -- on QBCore, use the native QBCore check
qbx = nil, -- on QBX, inherit global ('auto')
ox = nil,
},
}
Per-framework value takes precedence over mode. Useful when you have multiple servers sharing one config or when you migrate between mechanisms gradually.
Custom Logical Permissions
The script ships with a single logical permission, admin. You can define your own and use them in your own integrations (the script itself only asks for admin, but the bridge exposes Bridge.HasPermission(src, name) for any name).
Config.Permissions = {
nodes = {
admin = {
ace = 'admin',
esx = { 'admin', 'superadmin' },
qb = 'admin',
qbx = 'admin',
ox = 'admin',
},
-- A new logical permission "moderator":
moderator = {
ace = 'moderator',
esx = { 'mod', 'admin', 'superadmin' },
qb = 'admin', -- QBCore has no built-in 'mod'; reuse admin
qbx = 'admin',
ox = 'admin',
},
-- A logical permission scoped to your dev team:
['f5board.dev'] = {
ace = 'f5board.dev',
esx = { 'developer' },
qb = 'admin',
qbx = 'f5board.dev',
ox = 'f5board.dev',
},
},
}
Then to use the custom name, change the relevant gate:
Config.AdminCommand.permission = 'moderator'
If the script asks for a permission name not listed in nodes, the logical name is used as-is for every system. So Bridge.HasPermission(src, 'xyz') resolves to: xPlayer.getGroup() == 'xyz' on ESX, HasPermission(src, 'xyz') on QBCore, IsPlayerAceAllowed(src, 'xyz') on QBX/ox.
ACE Quick Setup Examples
Grant admin to a specific Steam ID
add_ace group.admin admin allow
add_principal identifier.steam:110000110abcdef group.admin
Grant a custom permission to a Discord role group
add_ace group.f5board_dev f5board.dev allow
add_principal identifier.discord:123456789012345678 group.f5board_dev
Direct ACE grant (no group)
add_ace identifier.steam:110000110abcdef admin allow
Common Recipes
Pure ACE (framework-independent)
Config.Permissions = {
mode = 'aceOnly',
aceFallback = false,
nodes = {
admin = { ace = 'admin', esx = nil, qb = nil, qbx = 'admin', ox = 'admin' },
},
}
add_ace group.admin admin allow
add_principal identifier.steam:XXX group.admin
ESX with users.group only (default)
Config.Permissions = {
mode = 'auto',
aceFallback = false,
nodes = {
admin = { esx = { 'admin', 'superadmin' } },
},
}
Set users.group = 'admin' in the DB or use /setgroup <id> admin (depending on your admin menu).
ESX with both users.group AND ACE
Config.Permissions = {
mode = 'auto',
aceFallback = true, -- key flip
nodes = {
admin = {
ace = 'admin',
esx = { 'admin', 'superadmin' },
},
},
}
add_ace group.admin admin allow
add_principal identifier.discord:XXX group.admin -- ACE admins
-- ESX admins: set users.group = 'admin' as usual
QBCore with permissions table only (default)
No config change needed. Use QBCore's normal permission flow:
qb permissions add <serverId> admin
or set the permissions JSON column in the players table.
Different gates for /board and /badmin
Config.Command.permission = 'admin'
Config.AdminCommand.permission = 'admin.super'
Config.Permissions.nodes = {
admin = {
ace = 'admin',
esx = { 'admin', 'superadmin' },
qb = 'admin',
qbx = 'admin',
ox = 'admin',
},
['admin.super'] = {
ace = 'admin.super',
esx = { 'superadmin' },
qb = 'admin',
qbx = 'admin.super',
ox = 'admin.super',
},
}
/board opens for any admin; /badmin only for super-admins.
Verifying Permissions
Enable the bridge startup log to confirm which framework and mode were detected:
Config.Bridge.debug = true
On boot you'll see:
[f5_board] bridge: framework=qb
[f5_board] bridge: inventory=qb
If a /board or /badmin call fails with notify_no_perm, enable the SECURITY debug category to see the rejection:
Config.Debug.enabled = true
Config.Debug.categories.SECURITY = true
Then the server console shows rejections like:
[SERVER][SECURITY] [WARN] catalog_request_reject src=12 reason=no_perm
[SERVER][SECURITY] [WARN] admin requestList denied src=12
That tells you the permission check ran but returned false. From there, double-check your nodes.<logical>.<framework> mapping in framework_config.lua and the player's actual group / ACE membership.
See Also
- Commands — which commands use which logical permission
- Configuration —
Config.Framework(forcing an adapter, custom frameworks) - Troubleshooting — when permissions fail