Frameworks
How the ESX, QBCore and standalone bridges work, and how to add your own.
Every bridge lives in server/bridge/ and is listed in escrow_ignore, so you can read and edit all of it.
How detection works#
Config.Framework = "auto"On "auto", the resource checks which core resource is started:
DetectFramework = function()
if Config.Framework ~= "auto" then return end
if GetResourceState("es_extended") == "started" then
Config.Framework = "esx"
elseif GetResourceState("qb-core") == "started" then
Config.Framework = "qbcore"
else
Config.Framework = "standalone"
end
endEach bridge then guards itself on the first line, so only one ever loads:
local ESX = Config.Framework == "esx" and exports['es_extended']:getSharedObject()
if not ESX then return endWith Config.Debug on you will see which one loaded:
[DEBUG] Framework: ESX
[DEBUG] ESX file loaded successfully!What a bridge provides#
Each bridge fills the same framework table:
| Function | Returns | Used for |
|---|---|---|
GetPlayer(src) | player object | Every other function |
GetPlayerName(player) | string | Character name in the list |
GetPlayerIdentifier(player) | string | Logging |
GetPlayerGroup(player) | string | Permission check |
GetPlayerJob(player) | string | Available for your own use |
GetPlayers() | table | Available for your own use |
HasPermission(src) | boolean | Gating every entry point |
GetCharacterName(src) | string or nil | The name shown in the panel |
GetCharacterName is the one the interface uses. When it returns nil - always, on standalone - the panel falls back to the Steam/FiveM account name.
Adding your own framework#
Create the bridge file#
Copy server/bridge/standalone.lua to server/bridge/myframework.lua and guard it on your framework name:
local Core = Config.Framework == "myframework" and exports['my-core']:GetCore()
if not Core then return end
Debug("MyFramework file loaded successfully!")
framework = {}
framework.GetPlayer = function(src)
return Core.GetPlayer(src)
end
framework.GetCharacterName = function(src)
local player = framework.GetPlayer(src)
if not player then return nil end
return player.name
end
framework.HasPermission = function(src)
local player = framework.GetPlayer(src)
if not player then return false end
for _, allowed in ipairs(Config.Groups.myframework) do
if player.rank == allowed then return true end
end
return false
endserver/bridge/*.lua is a wildcard in the manifest, so the new file loads with no manifest change.
Add the detection#
DetectFramework = function()
if Config.Framework ~= "auto" then return end
if GetResourceState("my-core") == "started" then
Config.Framework = "myframework"
Debug("Framework: MyFramework")
elseif GetResourceState("es_extended") == "started" then
Config.Framework = "esx"
-- ...
end
endAdd your groups#
Config.Groups = {
esx = { "admin", "superadmin" },
qbcore = { "admin", "god" },
myframework = { "staff", "owner" },
}Skipping the bridge entirely#
If your permissions come from somewhere else - a database, a Discord role, your own admin resource - you do not need a bridge at all. Edit HasPermission in server/function.lua:
HasPermission = function(src)
if not src or src == 0 then return true end
return exports['my-admin']:isStaff(src)
endHasPermission gates every command, every NUI callback and the player list. If it returns true for everyone, every player can watch every other player.
Keep Config.Framework = "standalone" in that case, so no bridge loads and the panel falls back to account names.