PiTyUs.Hire me

Frameworks

Supporting several frameworks with a bridge

Write FiveM resources that run on ESX, QBCore, Qbox or standalone: detecting the framework with GetResourceState, a bridge layer with one interface (player, money, job, notify, items), file layout, config overrides, testing each framework, and the common pitfalls.

Updated 11 min readBy PiTyUs · FiveM developer

Overview

If you sell or share scripts, supporting only one framework cuts your audience in half. The answer used by most successful resources is a bridge: your script talks to a small interface you define, and a thin file per framework implements it. The script’s logic never mentions ESX or QBCore.

Detecting the framework

shared/framework.lualua
local function detect()
    if Config.Framework and Config.Framework ~= 'auto' then return Config.Framework end
    if GetResourceState('qbx_core') == 'started' then return 'qbx' end
    if GetResourceState('qb-core') == 'started' then return 'qb' end
    if GetResourceState('es_extended') == 'started' then return 'esx' end
    return 'standalone'
end

Framework = detect()

Check Qbox before QBCore: Qbox ships a QBCore compatibility layer, so testing for qb-core first could pick the bridge path you did not intend. Make sure your resource starts after the framework (list it as a dependency or order it in server.cfg).

Define one interface

FunctionReturns / does
Bridge.getIdentifier(src)Character ID (citizenid / identifier / charId)
Bridge.getJob(src){ name, grade }
Bridge.addMoney(src, account, amount)true/false
Bridge.removeMoney(src, account, amount)true/false
Bridge.notify(src, text, type)Shows a notification
Bridge.addItem / Bridge.removeItemThrough the active inventory

Two bridge implementations

bridge/esx/server.lualua
if Framework ~= 'esx' then return end
local ESX = exports.es_extended:getSharedObject()

Bridge = {}

function Bridge.getIdentifier(src)
    local x = ESX.GetPlayerFromId(src)
    return x and x.identifier
end

function Bridge.getJob(src)
    local x = ESX.GetPlayerFromId(src)
    return x and { name = x.job.name, grade = x.job.grade }
end

function Bridge.addMoney(src, account, amount)
    local x = ESX.GetPlayerFromId(src)
    if not x then return false end
    if account == 'cash' then x.addMoney(amount) else x.addAccountMoney(account, amount) end
    return true
end

function Bridge.notify(src, text)
    TriggerClientEvent('esx:showNotification', src, text)
end
bridge/qbx/server.lualua
if Framework ~= 'qbx' then return end

Bridge = {}

function Bridge.getIdentifier(src)
    local p = exports.qbx_core:GetPlayer(src)
    return p and p.PlayerData.citizenid
end

function Bridge.getJob(src)
    local p = exports.qbx_core:GetPlayer(src)
    return p and { name = p.PlayerData.job.name, grade = p.PlayerData.job.grade.level }
end

function Bridge.addMoney(src, account, amount)
    return exports.qbx_core:AddMoney(src, account, amount)
end

function Bridge.notify(src, text, kind)
    exports.qbx_core:Notify(src, text, kind or 'inform')
end
fxmanifest.lua — load orderlua
shared_scripts {
    'config.lua',
    'shared/framework.lua',
}
server_scripts {
    'bridge/esx/server.lua',
    'bridge/qb/server.lua',
    'bridge/qbx/server.lua',
    'bridge/standalone/server.lua',
    'server/main.lua',
}

Logic that never mentions a framework

server/main.lualua
RegisterNetEvent('fishmarket:sell', function()
    local src = source
    local job = Bridge.getJob(src)
    if job and job.name == 'police' then
        return Bridge.notify(src, 'Police cannot sell fish', 'error')
    end
    -- count and remove fish through the inventory bridge, then:
    Bridge.addMoney(src, 'cash', 45)
end)

Inventories are a second axis

Framework and inventory vary independently: an ESX server may run ox_inventory, a QBCore server qb-inventory or a community ox_inventory fork (official ox_inventory supports ESX, Qbox, ox_core and ND_Core). Bridge items separately from the framework — see inventory integration for developers.

Pitfalls

  • Grades: ESX uses a number, QBCore/Qbox a table with level — normalise in the bridge.
  • Account names differ (money vs cash on ESX) — map them.
  • Player-loaded events differ — expose one Bridge.onPlayerLoaded on the client.
  • Never claim support you have not tested.

Frequently asked questions

How do I make a FiveM script work on ESX and QBCore?

Write your logic against a small bridge interface and implement that interface once per framework, choosing the bridge at runtime with GetResourceState.

How do I detect which framework is running?

GetResourceState('es_extended') == 'started', and the same for qb-core and qbx_core.

Why check Qbox before QBCore?

Qbox provides a QBCore compatibility layer, so a QBCore check alone can match a Qbox server.

What is a standalone script?

One that works without any framework, often with an optional bridge to use framework features when present.

Need this built, not just explained?

Ten years of FiveM work, from Lua to NUI

Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.

Related guides