Skip to content

Modules and Custom Providers

Custom identity, payment, artist access, target, TextUI, and appearance providers

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#

NeedFileDefault behavior
Character-bound persistent ownershipmodules/identity.luaQBX, QB-Core, ESX; otherwise FiveM license
Professional artist accessmodules/access.luaConfigured identifier or framework job
Charge and artist payoutmodules/payment.luaQBX, QB-Core, and ESX cash
Shop text notificationmodules/textui.luaox_lib TextUI
Player target for professional offersmodules/target/client.luaox_target, then qb-target
Appearance persistenceclient/appearance/*.luastandalone 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:

lua
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:

lua
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.

lua
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:

lua
local USE_CUSTOM_PAYMENT = true

Implement all four CustomPayment functions:

lua
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:

lua
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:

lua
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:

lua
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#

  1. Fully restart the server after module changes.
  2. Test character identity across reconnect and character switching.
  3. Test project save, purchase, and reconnect persistence.
  4. In a professional session, verify the customer charge and artist payout. Test a preparation/payment failure separately from a post-commit synchronization failure.
  5. Stop and start the target or TextUI provider and verify re-registration.
  6. 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.