Framework Integration

NAC ID system and integration examples for QBCore, ESX, and Qbox.

QBCoreESXQbox

NAC ID System

NAC IDs (Network Access Codes) gate per-department radio access. getUserNacId assigns an ID to each player; zones and channels match that ID against nacIds, allowedNacs, and scanAllowedNacs.

Access order:

  1. radioAccessCheck — can the player open the radio at all?
  2. Zone.nacIds — is this zone visible?
  3. Channel.allowedNacs — can they connect / transmit?
  4. Channel.scanAllowedNacs — can they scan (listen-only)?

Example: Police get "141", EMS get "200". A channel with allowedNacs = { "141" } + scanAllowedNacs = { "200" } lets police talk and EMS listen. Returning "0" (or any unused string) = no match, no access.

Players whose NAC ID matches dispatchNacId also get the SGN button. Defaults are both "141" so everyone can trigger alerts — change dispatchNacId to restrict.

For full access resolution (including ACE entries), see How Access Resolves.

Core Functions

Three callbacks in config.lua:

config.lua
-- Who can use the radio at all (server-side)
Config.radioAccessCheck = function(playerId)
    return true  -- Default: allow everyone
end

-- Assign NAC ID based on job/role (server-side)
Config.getUserNacId = function(serverId)
    return "141"  -- Default: everyone gets "141"
end

-- Display name for radio and dispatch panel (server-side)
Config.getPlayerName = function(serverId)
    return GetPlayerName(serverId)
end

Refreshing NAC IDs

When a player's job changes, call this server-side so their channel access updates immediately without a reconnect:

-- Single player (call this on job change events)
exports['tRadio']:refreshNacId(serverId)

-- All players
exports['tRadio']:refreshPlayerInfo()

Hook the call into your framework's job-change event:

RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)
AddEventHandler('esx:setJob', function(source)
    exports['tRadio']:refreshNacId(source)
end)
RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)

Framework Setup

Pick your framework. The choice persists across the page and across visits.

Radio Item (Optional)

Add to qb-core/shared/items.lua if you want to require a radio item:

qb-core/shared/items.lua
['radio'] = {
    ['name'] = 'radio',
    ['label'] = 'Radio',
    ['weight'] = 1000,
    ['type'] = 'item',
    ['image'] = 'radio.png',
    ['unique'] = false,
    ['useable'] = true,
    ['shouldClose'] = true,
    ['combinable'] = nil,
    ['description'] = 'Handheld radio transceiver'
},
config.lua — QBCore
-- Require radio item
Config.radioAccessCheck = function(playerId)
    local Player = exports['qb-core']:GetPlayer(playerId)
    if not Player then return false end
    local radioItem = Player.Functions.GetItemByName("radio")
    return radioItem and radioItem.amount > 0 or false
end

-- Job-based access (alternative — no item required)
-- Config.radioAccessCheck = function(playerId)
--     local player = exports['qb-core']:GetPlayer(playerId)
--     if player and player.PlayerData.job then
--         local job = player.PlayerData.job.name
--         return job == "police" or job == "ambulance" or job == "ems"
--     end
--     return false
-- end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local player = exports['qb-core']:GetPlayer(serverId)
    if player and player.PlayerData.job then
        local job = player.PlayerData.job.name
        if job == "police" then return "141"
        elseif job == "ambulance" or job == "ems" then return "200"
        elseif job == "firefighter" then return "300"
        end
    end
    return "0"
end

-- Display name: callsign > lastname > FiveM name
Config.getPlayerName = function(serverId)
    local player = exports['qb-core']:GetPlayer(serverId)
    if player and player.PlayerData then
        if player.PlayerData.metadata.callsign then
            return player.PlayerData.metadata.callsign
        end
        if player.PlayerData.charinfo.lastname then
            return player.PlayerData.charinfo.lastname
        end
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    exports['qb-core']:CreateUseableItem('radio', function(source)
        TriggerClientEvent('radio:use', source)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

Radio Item (Optional)

Add to your database if you want to require a radio item:

Database
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('radio', 'Radio', 1, 0, 1);
config.lua — ESX
ESX = exports['es_extended']:getSharedObject()

-- Require radio item
Config.radioAccessCheck = function(playerId)
    local xPlayer = ESX.GetPlayerFromId(playerId)
    if not xPlayer then return false end
    local radioItem = xPlayer.getInventoryItem('radio')
    return radioItem and radioItem.count > 0 or false
end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return "0" end
    local job = xPlayer.getJob()
    if job.name == "police" then return "141"
    elseif job.name == "ambulance" then return "200"
    elseif job.name == "fire" or job.name == "firefighter" then return "300"
    end
    return "0"
end

-- Display name
Config.getPlayerName = function(serverId)
    if not serverId or serverId <= 0 then return "DISPATCH" end
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return GetPlayerName(serverId) end

    local callsign = xPlayer.getMeta("callsign")
    if callsign and callsign ~= "" then return callsign end

    local characterName = xPlayer.getName()
    if characterName and characterName ~= "" then
        local lastName = string.match(characterName, "%s+(%S+)$")
        if lastName then return lastName end
        return characterName
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    ESX.RegisterUsableItem('radio', function(playerId)
        TriggerClientEvent('radio:use', playerId)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

Radio Item (Optional)

Add to ox_inventory/data/items.lua:

ox_inventory/data/items.lua
['radio'] = {
    label = 'Radio',
    weight = 500,
    stack = false,
    close = true,
    client = {
        event = 'radio:use',
    },
},
config.lua — Qbox
-- Require radio item (ox_inventory)
Config.radioAccessCheck = function(playerId)
    local count = exports.ox_inventory:Search(playerId, 'count', 'radio')
    return count and count > 0 or false
end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local player = exports.qbx_core:GetPlayer(serverId)
    if player and player.PlayerData.job then
        local job = player.PlayerData.job.name
        if job == "police" then return "141"
        elseif job == "ambulance" or job == "ems" then return "200"
        elseif job == "firefighter" then return "300"
        end
    end
    return "0"
end

-- Display name: callsign > lastname > FiveM name
Config.getPlayerName = function(serverId)
    if not serverId or serverId <= 0 then return "DISPATCH" end
    local player = exports.qbx_core:GetPlayer(serverId)
    if player and player.PlayerData then
        if player.PlayerData.metadata and player.PlayerData.metadata.callsign
           and player.PlayerData.metadata.callsign ~= "NO CALLSIGN"
           and player.PlayerData.metadata.callsign ~= "" then
            return player.PlayerData.metadata.callsign
        end
        if player.PlayerData.charinfo and player.PlayerData.charinfo.lastname
           and player.PlayerData.charinfo.lastname ~= "" then
            return player.PlayerData.charinfo.lastname
        end
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    exports.qbx_core:CreateUseableItem('radio', function(source)
        TriggerClientEvent('radio:use', source)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

On this page

Need help?

Ask on Discord