Framework Integration
NAC ID system and integration examples for QBCore, ESX, and Qbox.
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:
radioAccessCheck— can the player open the radio at all?Zone.nacIds— is this zone visible?Channel.allowedNacs— can they connect / transmit?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:
-- 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)
endRefreshing 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:
['radio'] = {
['name'] = 'radio',
['label'] = 'Radio',
['weight'] = 1000,
['type'] = 'item',
['image'] = 'radio.png',
['unique'] = false,
['useable'] = true,
['shouldClose'] = true,
['combinable'] = nil,
['description'] = 'Handheld radio transceiver'
},-- 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)
endRadio Item (Optional)
Add to your database if you want to require a radio item:
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('radio', 'Radio', 1, 0, 1);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)
endRadio Item (Optional)
Add to ox_inventory/data/items.lua:
['radio'] = {
label = 'Radio',
weight = 500,
stack = false,
close = true,
client = {
event = 'radio:use',
},
},-- 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