Skip to main content
If your framework is not es_extended, qb-core, or qbx_core, you can still run every Atlas resource by filling in the custom framework adapter. An adapter normalizes your framework to one fixed shape, so consumer scripts call Bridge.GetJob(src) and Bridge.AddMoney(src, 'bank', 500) without ever branching on which framework you run. This is the most important page for unsupported setups. Work through the contract once and every Atlas resource works.
Consumers always use Bridge.Framework (and its shortcuts like Bridge.GetJob) — never the raw Fw* exports the bridge generates internally. You only implement the adapter; the bridge wires the rest.

How it works

The bridge selects a framework provider with the atlas:framework convar and loads the matching adapter. When you set it to custom, the bridge loads your editable stub:
setr atlas:framework "custom"
  • Server → editable/framework/server.lua
  • Client → editable/framework/client.lua
Both files live outside the escrow (they are meant to be edited). If your custom adapter fails to load — a syntax error or a runtime error at load time — the bridge falls back to the _default adapter so the server still boots. That fallback is why a half-finished adapter can look like “money silently fails”: the default adapter’s no-op methods are running, not yours.

The single-return rule

Every adapter method returns exactly one value. The bridge re-exposes each method as an export, and exports cross a Lua VM boundary where multi-return is lost. Returning value, err will silently drop the second value. Pick one return and stick to it.

Normalized shapes

Your methods must return these exact shapes so consumer scripts keep working unchanged:
-- job
{ name = 'police', label = 'Police', grade = 2, gradeLabel = 'Sergeant', isBoss = false, onDuty = true }

-- name
{ first = 'John', last = 'Doe' }

-- money accounts are normalized to these three keys:
'cash' | 'bank' | 'dirty'
Map your framework’s account names to cash / bank / dirty inside the adapter. If your framework has no dirty-money concept, return 0 for it.

Server contract

Implement these in editable/framework/server.lua. Required methods must return correct data for Atlas resources to function; optional methods can return a safe no-op (0 / false / {}) if your framework lacks the feature.
GetIdentifier(src)
→ string
required
The player’s unique identifier (license, char id, etc.).
GetPlayers()
→ int[]
required
Source ids of all currently loaded players.
GetName(src)
→ { first, last }
required
The player’s character name.
GetJob(src)
→ job
required
The normalized job shape { name, label, grade, gradeLabel, isBoss, onDuty }.
SetJob(src, name, grade)
→ bool
required
Set the player’s job. Return true on success.
GetMoney(src, account)
→ number
required
Balance of 'cash' | 'bank' | 'dirty'.
AddMoney(src, account, amount, reason?)
→ bool
required
Add money to an account. reason is an optional audit string. Return true on success.
RemoveMoney(src, account, amount, reason?)
→ bool
required
Remove money. Return false if the player has insufficient funds.
IsAdmin(src)
→ bool
required
Whether the player is a server admin.
GetPlayersByJob(jobName)
→ int[]
required
Source ids of online players with that job.
SetDuty(src, onDuty)
→ bool
required
Set the player’s duty state. Return true on success.
GetSociety(job)
→ number
Society (job) account balance. Return 0 if you have no society banking.
AddSociety(job, amount)
→ bool
Add to a society account. Return false if unsupported.
RemoveSociety(job, amount)
→ bool
Remove from a society account. Return false if unsupported.
RegisterUsable(item, onUse)
→ bool
Register item as a usable item, calling onUse(src) when used. See Usable items.

Client contract

Implement these in editable/framework/client.lua:
GetPlayerData()
→ { identifier, name, job, money? }
required
The local player’s data. name is { first, last }; job is the normalized job shape; money is optional.
GetJob()
→ job
required
The local player’s normalized job shape.
IsOnDuty()
→ bool
required
Whether the local player is on duty.

Lifecycle events you must fire

The bridge does not poll your framework. You translate your framework’s events into the normalized bridge events so consumers can react. Fire these as the matching thing happens:
TriggerEvent('atlasBridge:playerLoaded', src)        -- player finished loading
TriggerEvent('atlasBridge:jobChange', src, job)      -- job is the normalized shape
TriggerEvent('atlasBridge:playerUnload', src)        -- player dropped / logged out
Forgetting these is the most common mistake. Without atlasBridge:playerLoaded, Atlas resources never initialize the player; without atlasBridge:jobChange, job-gated features never update.

Usable items

Usable-item registration lives on the framework adapter (not inventory), because most frameworks own item usage. Implement RegisterUsable(item, onUse) to register a usable with your framework that calls onUse(src). When a consumer registers a usable through Bridge.Inventory.RegisterUsableItem, the bridge calls your RegisterUsable with an onUse that fires a per-resource bus event:
atlasBridge:useItem:<resource>:<item>
You do not fire that event yourself — just make sure onUse(src) runs when the item is used.

Worked example

A complete, copy-pasteable server adapter for a fictional MyFramework, modeled on the real es_extended adapter: an account map, a normalizeJob helper, every contract method, and the three lifecycle translations.
-- editable/framework/server.lua
-- Enable with:  setr atlas:framework "custom"

local Fw = exports['my_framework']:GetCoreObject()

-- Map normalized accounts -> your framework's account names.
local ACCOUNT = { cash = 'cash', bank = 'bank', dirty = 'dirty_money' }

-- Build the normalized job shape from your framework's job table.
local function normalizeJob(job)
    if type(job) ~= 'table' then
        return { name = 'unemployed', label = 'Unemployed', grade = 0, gradeLabel = '', isBoss = false, onDuty = true }
    end
    return {
        name       = job.name,
        label      = job.label,
        grade      = job.grade,
        gradeLabel = job.gradeLabel or '',
        isBoss     = job.isBoss == true,
        onDuty     = job.onDuty ~= false,
    }
end

local M = {}

---@param src number @return string|nil
function M.GetIdentifier(src)
    local player = Fw.GetPlayer(src)
    if not player then return nil end
    return player.identifier
end

---@return number[]
function M.GetPlayers()
    local out = {}
    for _, player in pairs(Fw.GetPlayers()) do
        out[#out + 1] = player.source
    end
    return out
end

---@param src number @return { first:string, last:string }
function M.GetName(src)
    local player = Fw.GetPlayer(src)
    if not player then return { first = 'Player', last = '' } end
    return { first = player.firstName or 'Player', last = player.lastName or '' }
end

---@param src number @return table job
function M.GetJob(src)
    local player = Fw.GetPlayer(src)
    if not player then return normalizeJob(nil) end
    return normalizeJob(player.getJob())
end

---@param src number @param name string @param grade number @return boolean
function M.SetJob(src, name, grade)
    if type(name) ~= 'string' then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    player.setJob(name, grade or 0)
    return true
end

---@param src number @param account string @return number
function M.GetMoney(src, account)
    local player = Fw.GetPlayer(src)
    if not player then return 0 end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return 0 end
    return player.getBalance(acc) or 0
end

---@param src number @param account string @param amount number @param reason? string @return boolean
function M.AddMoney(src, account, amount, reason)
    amount = tonumber(amount) or 0
    if amount <= 0 then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return false end
    player.addMoney(acc, amount, reason)
    return true
end

---@param src number @param account string @param amount number @param reason? string @return boolean
function M.RemoveMoney(src, account, amount, reason)
    amount = tonumber(amount) or 0
    if amount <= 0 then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return false end
    if (player.getBalance(acc) or 0) < amount then return false end   -- insufficient funds
    player.removeMoney(acc, amount, reason)
    return true
end

---@param src number @return boolean
function M.IsAdmin(src)
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local group = player.getGroup()
    return group == 'admin' or group == 'superadmin'
end

---@param jobName string @return number[]
function M.GetPlayersByJob(jobName)
    local out = {}
    if type(jobName) ~= 'string' then return out end
    for _, player in pairs(Fw.GetPlayers()) do
        local job = player.getJob()
        if job and job.name == jobName then out[#out + 1] = player.source end
    end
    return out
end

---@param src number @param onDuty boolean @return boolean
function M.SetDuty(src, onDuty)
    local player = Fw.GetPlayer(src)
    if not player then return false end
    player.setDuty(onDuty == true)
    return true
end

-- Society accounts (optional — return 0/false if you don't use them).
function M.GetSociety(job) return 0 end
function M.AddSociety(job, amount) return false end
function M.RemoveSociety(job, amount) return false end

-- Usable items (optional). Register a usable that calls onUse(src).
---@param item string @param onUse fun(src:number) @return boolean
function M.RegisterUsable(item, onUse)
    Fw.RegisterUsableItem(item, function(src)
        onUse(src)
    end)
    return true
end

-- Lifecycle translation: your framework's events -> normalized bridge events.
AddEventHandler('my_framework:playerLoaded', function(src)
    TriggerEvent('atlasBridge:playerLoaded', src)
end)

AddEventHandler('my_framework:jobChanged', function(src, job)
    TriggerEvent('atlasBridge:jobChange', src, normalizeJob(job))
end)

AddEventHandler('playerDropped', function()
    TriggerEvent('atlasBridge:playerUnload', source)
end)

return M
And the matching client adapter:
-- editable/framework/client.lua
-- Enable with:  setr atlas:framework "custom"

local Fw = exports['my_framework']:GetCoreObject()

local function normalizeJob(job)
    if type(job) ~= 'table' then
        return { name = 'unemployed', label = 'Unemployed', grade = 0, gradeLabel = '', isBoss = false, onDuty = true }
    end
    return {
        name = job.name, label = job.label, grade = job.grade,
        gradeLabel = job.gradeLabel or '', isBoss = job.isBoss == true, onDuty = job.onDuty ~= false,
    }
end

local M = {}

---@return table
function M.GetPlayerData()
    local data = Fw.GetLocalPlayer()
    return {
        identifier = data and data.identifier or nil,
        name = { first = data and data.firstName or 'Player', last = data and data.lastName or '' },
        job  = M.GetJob(),
    }
end

---@return table job
function M.GetJob()
    return normalizeJob(Fw.GetLocalPlayer() and Fw.GetLocalPlayer().getJob())
end

---@return boolean
function M.IsOnDuty()
    return M.GetJob().onDuty == true
end

-- Fire as your framework loads / changes job:
AddEventHandler('my_framework:client:playerLoaded', function()
    TriggerEvent('atlasBridge:client:playerLoaded')
end)

AddEventHandler('my_framework:client:jobChanged', function(job)
    TriggerEvent('atlasBridge:client:jobChange', normalizeJob(job))
end)

return M

Test it

1

Enable debug

Add setr atlas:debug "true" and setr atlas:framework "custom", then restart atlasBridge.
2

Confirm the adapter loaded

Look for [Framework] server adapter: custom in the console. If you instead see _default, your stub errored at load — fix the error and restart.
3

Read money and job back

From a test command, call Bridge.GetJob(src) and Bridge.GetMoney(src, 'bank') and confirm they return real values for a loaded player.
4

Buy or receive an item

Run a flow that removes money and grants an item, and confirm the notification, the balance change, and the item all land.
5

Turn debug off

Set setr atlas:debug "false" before production.

Pitfalls

Usually the custom adapter errored at load and the bridge fell back to _default, whose money methods are no-ops that return false. Check the console for server adapter: _default and fix the load error.
Adapter methods are single-return — exports drop the second value. Never return ok, err; return one value.
Consumers read job.name / job.grade / job.isBoss and the accounts cash / bank / dirty. If your framework uses different keys, normalize them inside the adapter — do not pass the raw framework table through.
If players never initialize or job-gated features never update, you likely forgot atlasBridge:playerLoaded or atlasBridge:jobChange. The bridge relies entirely on these.
https://mintcdn.com/atlasscripts/BwePy2Q7bMLnl0yQ/icons/box-filled.svg?fit=max&auto=format&n=BwePy2Q7bMLnl0yQ&q=85&s=c228a3b89453b292c0cedb29c252b3ab

Custom inventory adapter

Running an unsupported inventory too? Fill the inventory adapter next.