BenzyBridge — Integrations
How to use the bridge from a resource, and how to add support for a system it doesn't know yet. Everything a resource asks the bridge for is provider-agnostic — wire it once and it keeps working when you swap the underlying system. The bridge is standalone-first: with no framework, no inventory and no money system running it still answers every call sensibly (see With and without a framework).
Startup order. In
server.cfg, alwaysensure BenzyBridgebefore the resources that use it (and after your SQL connector, if any).
Contents
- Adding the bridge to a resource
- What the bridge auto-detects
- What you get with and without
- Storage
- Custom notification / prompt systems
- Custom money systems (adapters)
Adding the bridge to a resource
Add the bridge as your resource's first shared_script, and (optionally) declare the dependency:
-- your resource's fxmanifest.lua
shared_script '@BenzyBridge/init.lua' -- must be the FIRST shared_script
dependency 'BenzyBridge'
init.lua runs inside your resource's Lua state: it fetches the resolved settings, loads the active framework / inventory / target adapters in-process, and builds one global — Benzy. From then on you call Benzy.* directly. It's an ordinary Lua call, no cross-resource export hop, and nothing but Benzy is added to your globals.
-- Server
Benzy.Notify(source, 'success', 'Job', 'Paid $500.')
local ok = Benzy.AddMoney(source, 'cash', 500, 'Payout')
if Benzy.HasItem(source, 'lockpick') then --[[ ... ]] end
-- Client
Benzy.Show('E', 'Open the door')
local money = Benzy.GetTotal()
-- Read the resolved stack any time
print(Benzy.Framework, Benzy.Money, Benzy.Settings.storage)
Guard startup on the bridge if your resource requires it:
if GetResourceState('BenzyBridge') ~= 'started' then
-- warn + disable: this resource depends on the bridge
end
What the bridge auto-detects
With a concern left at 'auto' (the default for all of them), the bridge picks the first supported provider that is running, in this order — else the fallback in the last column. Force any concern by naming it in config.lua instead of 'auto'.
| Concern | Detected in order | Fallback |
|---|---|---|
| Framework | es_extended → esx, qbx_core → qbox, qb-core → qbcore | standalone |
| Inventory | ox_inventory, qb-inventory, qs-inventory | none (item ops are silent no-ops) |
| Money | a framework's own money if a framework is active, else BenzyMoney | none (money ops return false, 'UNSUPPORTED') |
| Notify | BenzyNotifications | native (native toast / chat) |
| DrawText (prompts) | BenzyDrawText | native (native prompt) |
| Target | ox_target, qb-target | none (a script that wants targeting downgrades to draw-text) |
| Storage | oxmysql, mysql-async | file (JSON files) |
The stack is re-resolved whenever a resource starts or stops, so starting BenzyMoney or a targeting system mid-session is picked up. Check what resolved with the server console command debugbridge, or read it live with exports.BenzyBridge:GetSettings().
What you get with and without
Every concern degrades gracefully — a missing system is never an error, just a smaller feature set. This is what a consumer sees in each case:
Framework
- With a framework (ESX / QBCore / QBox) —
GetPlayer,GetJob,GetGang,HasJob,OnDutyreturn real character / job / gang data, and the core cash/bank money ops route to the framework's own wallet. - Without (
standalone) — you still get a normalized player (source / id / license / name, an emptyjob/gang, and money read from the money provider). Job and gang come back empty rather than failing, so job-gated code just sees "no job".
Inventory
- With an inventory (ox / qb / qs) —
HasItem,GetItemCount,AddItem,RemoveItem,CanCarryoperate on real items. The item definitions and images live in your inventory, not the bridge — a resource that grants items ships its own list and setup steps (e.g. the BenzyFishing Integrations documentation). - Without (
none) — item ops are silent no-ops:HasInventory()returnsfalse,AddItem/RemoveItemdo nothing and don't warn. Branch onBenzy.HasInventory()and keep your own database tracking when it'sfalse. This is normal standalone behavior, not a fault.
Money
- With BenzyMoney — the full money surface works: cash/bank, society + business accounts, requests, loans, fines, and economy stats.
- With a framework's money — only the core cash/bank subset works; every account / request / loan op returns
false, 'UNSUPPORTED', so richer features fall away cleanly. - Without (
Config.Money = 'none', or nothing detected) — every money op returnsfalse, 'UNSUPPORTED'. Thefalsevs. a declinedfalse, 'INSUFFICIENT_FUNDS'lets a caller tell "no money system" apart from "the transaction was refused".
Storage
The bridge owns the connection; you own your tables. Wait for storage, then run your own SQL:
AddEventHandler('BenzyBridge:StorageReady', function()
if exports.BenzyBridge:GetStorageMode() ~= 'database' then return end -- file mode: use your own files
exports.BenzyBridge:Execute([[
CREATE TABLE IF NOT EXISTS myscript_data (
license VARCHAR(60) NOT NULL PRIMARY KEY,
value BIGINT NOT NULL DEFAULT 0
)]])
end)
-- later, in a thread
local row = exports.BenzyBridge:Single('SELECT value FROM myscript_data WHERE license = ?', { license })
The storage helpers (Query, Single, Scalar, Insert, Execute, GetStorageMode, IsDatabase, IsStorageReady, WaitForStorage) are server-only and are called on the bridge resource. Storage settles asynchronously, so wait for BenzyBridge:StorageReady as above — or block on WaitForStorage(), which yields until it's ready and returns the mode — before your first query.
Custom notification / prompt systems
For a notification or draw-text prompt system the bridge has no built-in provider for, set that concern to 'custom' and fill its Config.Custom hook. Each call is resolved in order: a shared function Fn, then a Resource + Export, then an Event — the first one filled in is used.
-- BenzyBridge config.lua
Config.Notify = 'custom'
Config.Custom.Notify = { Resource = 'my_notify', Export = 'ShowNotification', Event = '' }
-- called as exports.my_notify:ShowNotification(kind, title, message, duration)
The prompt (draw-text) hook works the same way, called with (a, b) — the key/label and the message. Unlike a toast, a prompt stays up until something takes it down, so it also takes a hide side (HideFn → HideExport → HideEvent, on the same Resource), called with no arguments:
Config.DrawText = 'custom'
Config.Custom.DrawText = { Resource = 'my_prompt', Export = 'ShowText', HideExport = 'HideText', Event = '', HideEvent = '' }
-- shown as exports.my_prompt:ShowText(a, b)
-- hidden as exports.my_prompt:HideText()
Benzy.Hide() and Benzy.HideAll() both call the hide side; the hook has no per-prompt id, so it clears whatever your system is showing. Benzy.IsShown() reports whether the bridge currently has a custom prompt up.
Fill the hide side. Leave it blank and a custom prompt can be shown but never dismissed — it stays on screen.
Leave a field blank to skip it. If nothing resolves — or your hook throws — the bridge renders the built-in instead, so a message is never silently lost, and warns once in the console naming the hook, so a mistyped resource or export name shows up instead of erroring on every call.
A custom money system is not registered here — it needs an op-map, so it's added as an adapter (next section).
Custom money systems (adapters)
Money has many operations that differ per script, so a custom money system is added as a small adapter — a table that maps the bridge's ops onto that script's real exports. Register it under the name custom (the only name accepted; anything else is refused with a console warning), then point Config.Money at 'custom'.
The registry is runtime state, so a bridge restart clears it. Register from a function you call in both places — on your own start, and whenever the bridge asks:
-- server, in your money script (or a small companion resource)
local function registerAdapter()
exports.BenzyBridge:RegisterMoneyProvider('custom', {
GetCash = function(id) return exports.my_money:getBalance(id, 'cash') end,
GetBank = function(id) return exports.my_money:getBalance(id, 'bank') end,
CanAfford = function(id, account, amount) return exports.my_money:getBalance(id, account) >= amount end,
AddMoney = function(id, account, amount, reason) return exports.my_money:give(id, account, amount) end,
RemoveMoney = function(id, account, amount, reason) return exports.my_money:take(id, account, amount) end,
-- implement the ops your server actually uses; every op you leave out returns false, 'UNSUPPORTED'
})
end
AddEventHandler('BenzyBridge:RequestMoneyProvider', registerAdapter) -- the bridge (re)started
CreateThread(function() -- ...and if it is already up
if GetResourceState('BenzyBridge') == 'started' then registerAdapter() end
end)
Each side keeps its own adapter, so if your UI reads balances through the bridge, register a client one too. Client ops are self-based and read-only — money is always mutated server-side:
-- client, only if something reads balances client-side
exports.BenzyBridge:RegisterMoneyProvider('custom', {
GetCash = function() return exports.my_money:getMyCash() end,
GetBank = function() return exports.my_money:getMyBank() end,
})
-- BenzyBridge config.lua
Config.Money = 'custom'
Now every Benzy.AddMoney(...) across every resource flows into your money script — with no consumer changes. You only implement the ops you need; the rest report UNSUPPORTED automatically, and an op that throws is caught and reported the same way — but warns once naming the op, so a broken adapter doesn't read as a missing feature. Selecting 'custom' with nothing registered prints a startup error rather than failing silently; debugbridge also shows whether an adapter is registered.