Framework Bridge
gcphone-next uses a bridge pattern to abstract framework-specific logic. This allows the same server modules to work with QBCore, QBox, and ESX without conditional checks scattered throughout the codebase.
How It Works
The bridge system has three layers:
- Config selection --
Config.Frameworkinshared/config.luadetermines which bridge loads. - Bridge files -- Each framework has a dedicated file that defines a set of global functions.
- Main orchestrator --
server/main.luawaits for the framework to start, then builds aBridgetable from the global functions and exposes them as server exports.
Loading Flow
When the resource starts:
server/main.lualoads and validates Config.- The bridge files (
server/bridge/qbcore.luaandserver/bridge/esx.lua) each checkConfig.Frameworkat the top andreturnimmediately if they are not the active framework. - The active bridge file waits for its framework resource to start (using
lib.waitFor), obtains the framework core object, and defines global functions. server/main.luawaits for a supported framework resource to reachstartedstate, then wraps the global functions into aBridgetable viabridgeCall().
Guard Clauses
Each bridge file starts with a guard:
-- qbcore.lua
if Config.Framework ~= 'qbcore' and Config.Framework ~= 'qbox' then return end
-- esx.lua
if Config.Framework ~= 'esx' then return endOnly one bridge ever executes its initialization logic.
Supported Frameworks
| Framework | Config Value | Resource Name | Bridge File |
|---|---|---|---|
| QBCore | 'qbcore' | qb-core | server/bridge/qbcore.lua |
| QBox | 'qbox' | qbx_core | server/bridge/qbcore.lua (same file, detects both) |
| ESX | 'esx' | es_extended | server/bridge/esx.lua |
The QBCore bridge handles both QBCore and QBox. It detects which is running at startup:
if GetResourceState('qb-core') == 'started' then
Core = exports['qb-core']:GetCoreObject()
Framework = 'qbcore'
elseif GetResourceState('qbx_core') == 'started' then
Core = exports.qbx_core
Framework = 'qbox'
endBridge Functions
Each bridge must define these global functions. All server modules call them through bridgeCall() in server/main.lua.
GetIdentifier(source)
Returns the player's unique identifier (citizenid for QBCore/QBox, license/identifier for ESX). Also checks if the player action is allowed (not dead, not cuffed).
- QBCore:
player.PlayerData.citizenid - ESX:
player.getIdentifier()orplayer.identifier
GetName(source)
Returns the player's character name.
- QBCore:
charinfo.firstname .. ' ' .. charinfo.lastname - ESX:
player.getName()orplayer.name
GetMoney(source, accountType)
Returns the player's money for the given account type (default: 'bank').
- QBCore:
player.Functions.GetMoney(accountType) - ESX:
player.getAccount(accountType).money(orplayer.getMoney()for cash)
AddMoney(source, amount, accountType, reason)
Adds money to the player's account. Returns true on success.
- QBCore:
player.Functions.AddMoney(accountType, amount, reason) - ESX:
player.addAccountMoney(accountType, amount, reason)(orplayer.addMoney()for cash)
RemoveMoney(source, amount, accountType, reason)
Removes money from the player's account. Returns true on success.
- QBCore:
player.Functions.RemoveMoney(accountType, amount, reason) - ESX:
player.removeAccountMoney(accountType, amount, reason)(orplayer.removeMoney()for cash)
GetJob(source)
Returns the player's job object.
- QBCore:
player.PlayerData.job - ESX:
player.getJob()orplayer.job
GetSourceFromIdentifier(identifier)
Finds the server source ID for an online player by their identifier. Returns nil if the player is offline.
- QBCore: Iterates
Core.Functions.GetPlayers()and matchescitizenid - ESX: Uses
ESX.GetPlayerFromIdentifier(identifier)
IsPlayerActionAllowed(source)
Checks if the player can perform phone actions. Returns true, nil if allowed, or false, reason if blocked.
Checked states:
Dead / last stand
Handcuffed / restrained
QBCore: Reads
player.PlayerData.metadataforisdead,inlaststand,ishandcuffedESX: Reads
Player(source).statefordead,isCuffed,handcuffed
GetFrameworkPhoneNumber(source, identifier)
Attempts to read the player's phone number from the framework's own data (e.g., QBCore's charinfo.phone or ESX's users.phone_number column). Falls back to nil if not available.
GetPhoneNumber(identifier)
Returns a phone number for the given identifier. Tries GetFrameworkPhoneNumber first, then falls back to the phone_numbers database table.
GetIdentifierByPhone(phoneNumber)
Reverse lookup: finds the identifier that owns a phone number. Checks online players first, then the database.
Server Exports
server/main.lua exposes the bridge functions as resource exports so other resources can use them:
exports('GetIdentifier', function(source) ... end)
exports('GetName', function(source) ... end)
exports('GetMoney', function(source, accountType) ... end)
exports('AddMoney', function(source, amount, accountType, reason) ... end)
exports('RemoveMoney', function(source, amount, accountType, reason) ... end)
exports('GetJob', function(source) ... end)
exports('GetFramework', function() ... end)
exports('GetSourceFromIdentifier', function(identifier) ... end)
exports('IsPlayerActionAllowed', function(source) ... end)
exports('GetBridge', function() ... end) -- Returns the full Bridge tableUsage from another resource
local identifier = exports['gcphone-next']:GetIdentifier(source)
local name = exports['gcphone-next']:GetName(source)
local bankMoney = exports['gcphone-next']:GetMoney(source, 'bank')Adding a New Framework
To add support for a new framework:
Create a new bridge file at
server/bridge/yourframework.lua.Add a guard clause at the top:
luaif Config.Framework ~= 'yourframework' then return endWait for your framework resource to start using
lib.waitFor.Define all the required global functions listed above (
GetIdentifier,GetName,GetMoney,AddMoney,RemoveMoney,GetJob,GetSourceFromIdentifier,IsPlayerActionAllowed,GetFrameworkPhoneNumber,GetPhoneNumber,GetIdentifierByPhone).Add the file to
fxmanifest.luain theserver_scriptssection:luaserver_scripts { -- ... existing ... 'server/bridge/yourframework.lua', }Update
server/main.luato detect your framework resource in the startuplib.waitForcallback:lualib.waitFor(function() if GetResourceState('qb-core') == 'started' or GetResourceState('qbx_core') == 'started' or GetResourceState('es_extended') == 'started' or GetResourceState('your_framework_resource') == 'started' then return true end end, 'gcphone-next failed to detect a supported framework', false)Set
Config.Framework = 'yourframework'inshared/config.lua.
No changes are needed in any server module -- they all go through bridgeCall() which resolves the global functions at runtime.