This guide covers the resource provider surface.
This page describes adaptation points for servers using a different framework or script. Most installations do not need module changes: QBX, QB-Core, ESX, ox_target, qb-target, and ox_lib are detected or supported automatically.
The module files are escrow-ignored. Back up local changes before updating and test every provider change on a test server.
Module ownership#
| Need | File | Default behavior |
|---|---|---|
| Character-bound persistent ownership | modules/identity.lua | QBX, QB-Core, ESX; otherwise FiveM license |
| Professional artist access | modules/access.lua | Configured identifier or framework job |
| Charge and artist payout | modules/payment.lua | QBX, QB-Core, and ESX cash |
| Shop text notification | modules/textui.lua | ox_lib TextUI |
| Player target for professional offers | modules/target/client.lua | ox_target, then qb-target |
| Appearance persistence | client/appearance/*.lua | standalone or illenium-appearance |
Configure before writing custom code#
First check these sections in config/init.lua:
- Config.TattooShop.Professional.Access.Jobs for job, minimum grade, and on-duty requirements
- Config.TattooShop.Professional.Access.Identifiers for framework-independent access
- Config.AppearanceIntegration for the active appearance provider
For example, require the tattoo job at grade 2 and while on duty:
Config.TattooShop.Professional.Access = {
Match = 'any',
Identifiers = {},
Jobs = {
tattoo = { MinGrade = 2, RequireOnDuty = true },
},
}
Match = any means that one configured identifier or job group is sufficient. Match = all requires every configured group to pass.
1. Custom multicharacter or identity provider#
File: modules/identity.lua
Saved projects, player tattoos, and purchases are bound to a character identity. Do not return a source, ped ID, or temporary session ID. Return a stable character-specific value that remains the same after reconnecting.
Adapt getCustomCharacterIdentifier:
local function getCustomCharacterIdentifier(source)
local characterId = exports.my_multicharacter:GetCharacterId(source)
return characterId and tostring(characterId) or nil
end
If the function returns nil, the standard QBX -> QB-Core -> ESX -> license order is used. If a supported framework is running but its character data is not ready, the system fails closed instead of falling back to a license and mixing characters.
The identity export is public and documented on the Events, Exports, and Commands page. The identity implementation itself remains server-owned.
2. Custom artist authorization#
File: modules/access.lua
Use getCustomAuthorization when job and identifier rules are not sufficient. This code runs on the server; never accept an authorization value sent by the client.
local function getCustomAuthorization(source)
return exports.my_business:IsEmployee(source, 'tattoo')
end
Return true to authorize the artist, false to reject, and nil to continue with the normal configured rules. Administrators with the configured admin permission are handled by the server access policy.
3. Custom economy or payment provider#
File: modules/payment.lua
The default adapter uses the QBX, QB-Core, or ESX cash account. For another economy or bank account, enable custom payment:
local USE_CUSTOM_PAYMENT = true
Implement all four CustomPayment functions:
local CustomPayment = {
isAvailable = function()
return GetResourceState('my_economy') == 'started'
end,
canAfford = function(source, amount, account, operationContext)
return exports.my_economy:GetBalance(source, account) >= amount
end,
removeMoney = function(source, amount, account, reason, operationContext)
return exports.my_economy:RemoveMoney(source, account, amount, reason) == true
end,
addMoney = function(source, amount, account, reason, operationContext)
return exports.my_economy:AddMoney(source, account, amount, reason) == true
end,
}
When operationContext.operationId is present, use it as an idempotency key if the economy supports idempotent operations. The charge operation uses the apply-job id with a :charge suffix; professional artist payout uses the same id with an :artist-payout suffix.
The customer pays in a professional session and the artist is paid through addMoney after canonical ownership commits. There is no refund handler or refund export. If a charged operation fails after the charge, the runtime does not create a second financial state store or silently retry the charge.
Do not re-register the tattooPayment exports; the module registers them.
4. Custom TextUI provider#
File: modules/textui.lua
Set the custom flag and adapt the show/hide calls:
local USE_CUSTOM_TEXT_UI = true
local CustomTextUI = {
show = function(message)
exports.my_textui:Show(message)
return true
end,
hide = function()
exports.my_textui:Hide()
return true
end,
}
The hide function must be safe when no message is visible.
5. Custom target provider#
File: modules/target/client.lua
Professional offers use ox_target and then qb-target by default. A custom target provider must implement addGlobalPlayer and removeGlobalPlayer:
local USE_CUSTOM_TARGET = true
local CUSTOM_TARGET_RESOURCE = 'my-target'
function CustomTarget.addGlobalPlayer(options)
exports['my-target']:AddGlobalPlayer(options)
return true
end
function CustomTarget.removeGlobalPlayer(name, label)
exports['my-target']:RemoveGlobalPlayer(name)
return true
end
The options table contains name, label, icon, distance, canInteract, and onSelect. Adapt it to the target provider's API. The module re-registers target options when the target resource restarts.
Without a target provider, target-based professional offers are not available. This does not disable self-service shop access.
6. Custom appearance provider#
The built-in providers are standalone and illenium-appearance. Use Config.AppearanceIntegration to select one.
For another appearance system, create a client adapter and register it with TattooAppearance.register:
TattooAppearance.register({
id = 'my-appearance',
isAvailable = function(_, config)
return GetResourceState(config.resource) == 'started'
end,
applyOwnedTattoos = function(_, tattoos, persistAppearance, config)
-- Merge Tattoo Designer tattoos with the provider state.
-- Persist the appearance when persistAppearance is true.
end,
})
Tattoo Designer remains the canonical owner of project ownership and applied rows. The adapter must preserve other provider tattoos, remove old Tattoo Designer runtime tattoos when required, and reapply the current owned set after provider outfit or appearance changes. Otherwise a removed tattoo can remain visible or return unexpectedly. The built-in Illenium adapter receives the local illenium-appearance:client:appearanceApplied hook after customization exit; a custom provider should expose an equivalent post-appearance signal or call the public refreshOwnTattoos() export after it clears decorations.
Provider validation#
- Fully restart the server after module changes.
- Test character identity across reconnect and character switching.
- Test project save, purchase, and reconnect persistence.
- In a professional session, verify the customer charge and artist payout. Test a preparation/payment failure separately from a post-commit synchronization failure.
- Stop and start the target or TextUI provider and verify re-registration.
- Test appearance add, remove, provider reapply, and reconnect behavior. For Illenium, also test editor save, cancel, and outfit changes after applying a custom runtime tattoo.