Skip to main content

Framework Compatibility

F5 Shadow Market runs on a self-detecting bridge. Everything framework-specific lives in bridge/; the rest of the resource only ever calls Bridge.*. At startup the bridge detects your framework, inventory and target system, resolves your money types, and wires up notifications — with no manual configuration in the common case.

Open bridge layer

The entire bridge layer ships as open, editable source — it is not part of the escrow-protected code. That means the custom config adapters described on this page are the no-code path, and for the most demanding setups you can also edit the bridge files directly. See Editing the Bridge Directly.

All selection lives in config.lua:

config.lua
F5Cfg.Framework = {
mode = 'auto', -- framework
inventory = 'auto', -- inventory
notify = 'framework', -- notification backend
notifyStyle = 'custom',
}
F5Cfg.Target = { system = 'auto' } -- target system
F5Cfg.Inventory = { imageUrl = 'auto' } -- item image path
Resource goes inert on failure

If no supported framework is detected — or no supported inventory for that framework — the bridge logs an error and the resource stays inert (no commands, no UI). Always confirm boot order and enable debug if the market doesn't respond.

Frameworks​

Supported​

FrameworkDetection resource
QBox Coreqbx_core
QBCoreqb-core
ESXes_extended

Auto-Detection​

With F5Cfg.Framework.mode = 'auto', the bridge checks resources in this priority order and uses the first one that is started:

  1. qbx_core → qbx
  2. qb-core → qb
  3. es_extended → esx

If none are started, the framework is nil and the resource is inert.

Forcing a Framework​

Set mode to a specific value to skip detection:

config.lua
F5Cfg.Framework.mode = 'qb'   -- 'esx' | 'qb' | 'qbx' | 'custom'

Per-Framework Behaviour​

The bridge maps each framework's native API to its internal contract:

ConcernQBCore / QBoxESX
Core objectexports['qb-core']:GetCoreObject() / exports.qbx_coreexports['es_extended']:getSharedObject()
Player by idcitizenididentifier
Player loaded eventQBCore:Server:PlayerLoadedesx:playerLoaded
Player unload eventQBCore:Server:OnPlayerUnloadesx:playerDropped
Usable itemCreateUseableItem / qbx_core:CreateUseableItemRegisterUsableItem
Admin group checkQBCore.Functions.HasPermission → else ACE group.<name>; QBox uses ACE group.<name>xPlayer.getGroup() == <name>

Custom Framework Adapter​

For a fork of QB / QBox / ESX (a renamed core with the same semantics), set mode = 'custom' and describe only what your fork changed. Everything you leave nil is inherited from the base.

config.lua
F5Cfg.Framework = {
mode = 'custom',
custom = {
base = 'qb', -- REQUIRED: 'qb' | 'qbx' | 'esx'
getCore = nil, -- function() return exports['my-core']:GetCoreObject() end

playerLoadedEvent = nil, -- renamed load event (same args as base)
playerUnloadEvent = nil, -- renamed unload event (same args as base)

-- Advanced overrides (set only if your fork's API differs):
-- normalize(raw) getPlayer(src) getPlayerByUid(uid) getPlayers() identifierOf(raw)
-- getAccount(raw,key) addAccount(raw,key,amt,reason) removeAccount(raw,key,amt,reason)
-- offlineSQL(uid,key,amt) hasGroup(src,group) registerUsableItem(name,cb)
-- onPlayerLoaded(emit) onPlayerUnload(emit)
-- notify(msg,kind,duration) getPlayerData() (client side)
},
}
FieldRequiredPurpose
baseYesThe framework whose semantics your fork inherits — 'qb', 'qbx' or 'esx'. An invalid value makes the resource inert
getCoreNoReturns your core object if the export name differs
playerLoadedEvent / playerUnloadEventNoRenamed lifecycle event names (same arguments as the base)
onPlayerLoaded(emit) / onPlayerUnload(emit)NoHook variant for when the event arguments differ — call emit(src, uid) yourself. Don't set both the string and the function for the same lifecycle event
normalize / getPlayer / getPlayerByUid / getPlayers / identifierOfNoOverride the player data layer when your fork's player struct differs
getAccount / addAccount / removeAccount / offlineSQLNoOverride the money primitives
hasGroup / registerUsableItemNoOverride the admin-group check and usable-item registration
notify / getPlayerDataNoClient-side overrides
Most forks need just two fields

For a typical QB/QBox/ESX fork that only renamed the core resource, base + getCore is enough. Setting a custom getPlayer without normalize assumes the base player struct; a custom notify bypasses the F5Cfg.Framework.notify ox_lib switch.

Inventories​

Supported​

inventory valueResourceItem image pathPickup popup (ItemBox)CanCarry check
oxox_inventorynui://ox_inventory/web/images/%s.pngAutomatic (none fired)Yes
qbqb-inventorynui://qb-inventory/html/images/%s.pngqb-inventory:client:ItemBox + legacy inventory:client:ItemBoxYes
qsqs-inventorynui://qs-inventory/html/images/%s.pngAutomatic (none fired)Yes
psps-inventorynui://ps-inventory/html/images/%s.pngps-inventory:client:ItemBox2No — always allows
codemcodem-inventorynui://codem-inventory/html/itemimages/%s.pngAutomatic (none fired)No — always allows
tgianntgiann-inventorynui://inventory_images/images/%s.webpAutomatic (none fired)Yes
esx_nativeESX built-in(none — no images)Notify on addYes
customyoursfrom your getItemImage callbackoptional itemBox callbackfrom your canCarryItem

Auto-Detection​

With F5Cfg.Framework.inventory = 'auto', the bridge checks resources in this order and uses the first one started:

  1. ox_inventory → ox
  2. qs-inventory → qs
  3. ps-inventory → ps
  4. codem-inventory → codem
  5. tgiann-inventory → tgiann
  6. qb-inventory → qb
  7. (ESX only, fallback) → esx_native

Inventory detection requires a valid framework first. Force a specific inventory with:

config.lua
F5Cfg.Framework.inventory = 'ox'   -- 'ox'|'qb'|'qs'|'ps'|'codem'|'tgiann'|'esx_native'|'custom'

Known Limitations​

InventoryLimitation
psThe ps-inventory adapter requires a QB/QBox base and always allows carrying (no weight pre-check)
codemRequires a QB/QBox base. On ESX it cannot read the item list and the market item grid comes back empty. Also always allows carrying
tgiannItem images live in a separate inventory_images resource as .webp files, not in tgiann-inventory
esx_nativeNo item images (a placeholder is shown), no native pickup popup (a notify is used instead), and item metadata is ignored on add

Item Image Path Override​

F5Cfg.Inventory.imageUrl = 'auto' uses the detected inventory's path from the table above. To override (e.g. a custom inventory folder), set a template string with a single %s placeholder for the item name:

config.lua
F5Cfg.Inventory.imageUrl = 'nui://my-inventory/images/%s.png'

Custom Inventory Adapter​

Set F5Cfg.Framework.inventory = 'custom' and provide callbacks in F5Cfg.Inventory.custom. The adapter expects these functions:

CallbackSignatureNotes
addItem(src, name, amount, metadata) → boolean
removeItem(src, name, amount, slot) → boolean
hasItem(src, name, amount) → boolean
getItemCount(src, name) → numberUsed for validation
getItems(src) → tableList of item objects (name, amount/count, label?, info/metadata?, slot?, image?)
canCarryItem(src, name, amount) → booleanDefaults to true if omitted
getItemLabel(name) → string
getItemImage(name) → string
itemBox(src, name, action, amount)Optional pickup popup — no-op if omitted

Money & Currencies​

The whole market uses a single wallet — F5Cfg.Money.payoutType — for balances, spending and payouts. The types table maps each currency id to a native account per framework.

config.lua
F5Cfg.Money = {
payoutType = 'bank',
types = {
{ id = 'cash', label = 'Cash', icon = 'fa-solid fa-money-bill-wave', accounts = { qb = 'cash', qbx = 'cash', esx = 'money' } },
{ id = 'bank', label = 'Bank', icon = 'fa-solid fa-building-columns', accounts = { qb = 'bank', qbx = 'bank', esx = 'bank' } },
{ id = 'crypto', label = 'Crypto', icon = 'fa-solid fa-coins', accounts = { qb = 'crypto', qbx = 'crypto' } },
{ id = 'blackmoney', label = 'Black Money', icon = 'fa-solid fa-sack-dollar', accounts = { esx = 'black_money' } },
},
}

Account Mapping​

Currency idQBCoreQBoxESX
cashcashcashmoney
bankbankbankbank
cryptocryptocrypto(none — skipped)
blackmoney(none — skipped)(none — skipped)black_money

Auto-Skip & Fallback​

A currency type with no account for the active framework is skipped automatically — that's why crypto is dropped on ESX and blackmoney is dropped on QB/QBox. Types are also skipped if their id or account key has an invalid format.

If payoutType resolves to a skipped or missing type, the bridge falls back, in order, to:

  1. The bank type (if active)
  2. The first active type in declaration order

A warning is logged when the fallback kicks in. Pick a payoutType that exists on your framework to avoid this.

Offline Payouts​

When a seller is offline at payout time (e.g. an auction they won concluded), the bridge credits them directly via SQL into the same payout account:

  • QB / QBox → UPDATE players SET money = JSON_SET(...) WHERE citizenid = ?
  • ESX → UPDATE users SET accounts = JSON_SET(...) WHERE identifier = ?

When the seller is online, the framework's native add-money API is used and their balance refreshes live.

Target Systems​

The package pickup interaction uses your target system.

config.lua
F5Cfg.Target = {
system = 'auto', -- 'auto' | 'ox' | 'qb' | 'custom' | 'none'

pickup = {
label = 'target_pickup_package',
icon = 'fas fa-box-open',
iconColor = nil, -- ox_target only
distance = 2.5,
},
}
systemBehaviour
'auto'Detect ox_target (→ ox) else qb-target (→ qb); if neither is running, no target interaction is created
'ox'Force ox_target (addLocalEntity / removeLocalEntity)
'qb'Force qb-target (AddTargetEntity / RemoveTargetEntity)
'none'No target interaction — buyers collect the package from the Orders panel
'custom'A fork of ox/qb-target (see below)
OptionDefaultDescription
pickup.label'target_pickup_package'Locale key for the pickup option's label
pickup.icon'fas fa-box-open'Font Awesome icon for the pickup option
pickup.iconColornilIcon tint, e.g. '#e6b800'. ox_target only — ignored by qb-target. nil = theme default
pickup.distance2.5Max pickup interaction distance (m)

Custom Target Adapter​

config.lua
F5Cfg.Target = {
system = 'custom',
custom = {
base = 'ox', -- 'ox' (addLocalEntity/removeLocalEntity) | 'qb' (AddTargetEntity/RemoveTargetEntity)
resource = 'my-target', -- your target export name
-- add(entity, name, label, icon, dist, onSelect, iconColor) -- optional full override
-- remove(entity, name) -- optional full override
},
}
FieldPurpose
baseThe API your target mirrors — 'ox' or 'qb'. Validated; an invalid base disables targeting
resourceYour target resource/export name. If the resource isn't running and no add/remove overrides are given, targeting is disabled (no target interaction)
add / removeOptional callbacks to fully override how an entity option is added/removed

Notifications​

config.lua
F5Cfg.Framework.notify      = 'framework' -- 'auto' | 'framework' | 'oxlib'
F5Cfg.Framework.notifyStyle = 'custom' -- 'custom' | 'native'
OptionValueBehaviour
notify'auto'Use ox_lib if it's started, otherwise the framework's native notify
notify'framework'Always use the framework's native notify
notify'oxlib'Always use ox_lib (ox_lib:notify)
notifyStyle'custom'The market and admin panel render their own styled top-right toasts
notifyStyle'native'Market/admin alerts are handed off to the notify backend above

Editing the Bridge Directly​

The two integration paths in order of effort:

  1. Config custom adapters (no code) — for a fork of QB/QBox/ESX, a custom inventory, or a custom target, describe it in config.lua (F5Cfg.Framework.custom, F5Cfg.Inventory.custom, F5Cfg.Target.custom). Covered above. This is enough for the large majority of setups.
  2. Direct source edits (for the most demanding) — the whole bridge layer ships as open, un-escrowed source, so you can patch or extend it for anything the config path can't express: an entirely unsupported framework, a bespoke inventory export, a non-standard target API, or a database driver beyond the three the SQL bridge already handles.

The open, editable files:

File / folderResponsibility
bridge/init.luaDetection, the shared Bridge.* table, money resolution, the item filter, the net-event callback transport
bridge/server/{esx,qb,qbx,custom}.luaServer framework adapters (player data, money, groups, usable items, lifecycle)
bridge/client/{esx,qb,qbx,custom}.luaClient framework adapters (notify, player data)
bridge/inventory/{ox,qb,qs,ps,codem,tgiann,esx_native,custom}.luaInventory adapters (the 8-function contract)
bridge/target.luaTarget adapter (ox_target / qb-target / custom)
server/sql_bridge.luaThe oxmysql / mysql-async / ghmattimysql auto-detecting SQL layer
Keep edits in the bridge

The rest of the resource only ever calls Bridge.* and the standard MySQL.* layer — so to stay forward-compatible, keep your changes inside the bridge files and preserve those public contracts. When you update the script, re-apply your bridge edits (or diff them in) rather than editing resource logic elsewhere.

Verifying Detection​

Enable debug and watch the server console on startup:

config.lua
F5Cfg.Debug.enabled            = true
F5Cfg.Debug.categories.BRIDGE = true

On success:

[SERVER][BRIDGE] detected framework=qb inventory=qb

On a custom framework:

[SERVER][BRIDGE] custom framework active (base=qb)

On failure (resource inert):

[SERVER][ERROR] [ERR]  [BRIDGE] no supported framework detected (esx/qb/qbx) — resource inert
[SERVER][ERROR] [ERR] [BRIDGE] no supported inventory detected for framework esx

See Also​