F5 StudioF5 Studio
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_coreqbx
  2. qb-coreqb
  3. es_extendedesx

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_inventoryox
  2. qs-inventoryqs
  3. ps-inventoryps
  4. codem-inventorycodem
  5. tgiann-inventorytgiann
  6. qb-inventoryqb
  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 walletF5Cfg.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 / QBoxUPDATE players SET money = JSON_SET(...) WHERE citizenid = ?
  • ESXUPDATE 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