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.
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:
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
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
| Framework | Detection resource |
|---|---|
| QBox Core | qbx_core |
| QBCore | qb-core |
| ESX | es_extended |
Auto-Detection
With F5Cfg.Framework.mode = 'auto', the bridge checks resources in this priority order and uses the first one that is started:
qbx_core→qbxqb-core→qbes_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:
F5Cfg.Framework.mode = 'qb' -- 'esx' | 'qb' | 'qbx' | 'custom'
Per-Framework Behaviour
The bridge maps each framework's native API to its internal contract:
| Concern | QBCore / QBox | ESX |
|---|---|---|
| Core object | exports['qb-core']:GetCoreObject() / exports.qbx_core | exports['es_extended']:getSharedObject() |
| Player by id | citizenid | identifier |
| Player loaded event | QBCore:Server:PlayerLoaded | esx:playerLoaded |
| Player unload event | QBCore:Server:OnPlayerUnload | esx:playerDropped |
| Usable item | CreateUseableItem / qbx_core:CreateUseableItem | RegisterUsableItem |
| Admin group check | QBCore.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.
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)
},
}
| Field | Required | Purpose |
|---|---|---|
base | Yes | The framework whose semantics your fork inherits — 'qb', 'qbx' or 'esx'. An invalid value makes the resource inert |
getCore | No | Returns your core object if the export name differs |
playerLoadedEvent / playerUnloadEvent | No | Renamed lifecycle event names (same arguments as the base) |
onPlayerLoaded(emit) / onPlayerUnload(emit) | No | Hook 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 / identifierOf | No | Override the player data layer when your fork's player struct differs |
getAccount / addAccount / removeAccount / offlineSQL | No | Override the money primitives |
hasGroup / registerUsableItem | No | Override the admin-group check and usable-item registration |
notify / getPlayerData | No | Client-side overrides |
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 value | Resource | Item image path | Pickup popup (ItemBox) | CanCarry check |
|---|---|---|---|---|
ox | ox_inventory | nui://ox_inventory/web/images/%s.png | Automatic (none fired) | Yes |
qb | qb-inventory | nui://qb-inventory/html/images/%s.png | qb-inventory:client:ItemBox + legacy inventory:client:ItemBox | Yes |
qs | qs-inventory | nui://qs-inventory/html/images/%s.png | Automatic (none fired) | Yes |
ps | ps-inventory | nui://ps-inventory/html/images/%s.png | ps-inventory:client:ItemBox2 | No — always allows |
codem | codem-inventory | nui://codem-inventory/html/itemimages/%s.png | Automatic (none fired) | No — always allows |
tgiann | tgiann-inventory | nui://inventory_images/images/%s.webp | Automatic (none fired) | Yes |
esx_native | ESX built-in | (none — no images) | Notify on add | Yes |
custom | yours | from your getItemImage callback | optional itemBox callback | from your canCarryItem |
Auto-Detection
With F5Cfg.Framework.inventory = 'auto', the bridge checks resources in this order and uses the first one started:
ox_inventory→oxqs-inventory→qsps-inventory→pscodem-inventory→codemtgiann-inventory→tgiannqb-inventory→qb- (ESX only, fallback) →
esx_native
Inventory detection requires a valid framework first. Force a specific inventory with:
F5Cfg.Framework.inventory = 'ox' -- 'ox'|'qb'|'qs'|'ps'|'codem'|'tgiann'|'esx_native'|'custom'
Known Limitations
| Inventory | Limitation |
|---|---|
ps | The ps-inventory adapter requires a QB/QBox base and always allows carrying (no weight pre-check) |
codem | Requires a QB/QBox base. On ESX it cannot read the item list and the market item grid comes back empty. Also always allows carrying |
tgiann | Item images live in a separate inventory_images resource as .webp files, not in tgiann-inventory |
esx_native | No 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:
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:
| Callback | Signature | Notes |
|---|---|---|
addItem | (src, name, amount, metadata) → boolean | |
removeItem | (src, name, amount, slot) → boolean | |
hasItem | (src, name, amount) → boolean | |
getItemCount | (src, name) → number | Used for validation |
getItems | (src) → table | List of item objects (name, amount/count, label?, info/metadata?, slot?, image?) |
canCarryItem | (src, name, amount) → boolean | Defaults 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.
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 id | QBCore | QBox | ESX |
|---|---|---|---|
cash | cash | cash | money |
bank | bank | bank | bank |
crypto | crypto | crypto | (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:
- The
banktype (if active) - 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.
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,
},
}
system | Behaviour |
|---|---|
'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) |
| Option | Default | Description |
|---|---|---|
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.iconColor | nil | Icon tint, e.g. '#e6b800'. ox_target only — ignored by qb-target. nil = theme default |
pickup.distance | 2.5 | Max pickup interaction distance (m) |
Custom Target Adapter
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
},
}
| Field | Purpose |
|---|---|
base | The API your target mirrors — 'ox' or 'qb'. Validated; an invalid base disables targeting |
resource | Your target resource/export name. If the resource isn't running and no add/remove overrides are given, targeting is disabled (no target interaction) |
add / remove | Optional callbacks to fully override how an entity option is added/removed |
Notifications
F5Cfg.Framework.notify = 'framework' -- 'auto' | 'framework' | 'oxlib'
F5Cfg.Framework.notifyStyle = 'custom' -- 'custom' | 'native'
| Option | Value | Behaviour |
|---|---|---|
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:
- Config
customadapters (no code) — for a fork of QB/QBox/ESX, a custom inventory, or a custom target, describe it inconfig.lua(F5Cfg.Framework.custom,F5Cfg.Inventory.custom,F5Cfg.Target.custom). Covered above. This is enough for the large majority of setups. - 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 / folder | Responsibility |
|---|---|
bridge/init.lua | Detection, the shared Bridge.* table, money resolution, the item filter, the net-event callback transport |
bridge/server/{esx,qb,qbx,custom}.lua | Server framework adapters (player data, money, groups, usable items, lifecycle) |
bridge/client/{esx,qb,qbx,custom}.lua | Client framework adapters (notify, player data) |
bridge/inventory/{ox,qb,qs,ps,codem,tgiann,esx_native,custom}.lua | Inventory adapters (the 8-function contract) |
bridge/target.lua | Target adapter (ox_target / qb-target / custom) |
server/sql_bridge.lua | The oxmysql / mysql-async / ghmattimysql auto-detecting SQL layer |
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:
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
- Configuration → Framework & Compatibility — the config block in context
- Items — per-inventory icon folders
- Admin Panel → Access Control — how framework groups grant admin access
- Troubleshooting — when detection or money/target picks the wrong path