DirkScripts Logo

Login With FiveM

Login

📚 Library
â€ē
Modules
â€ē

scriptConfig

Script Config

â„šī¸ Client & Server Module

The scriptConfig module provides a full configuration system for resources — JSON Schema-driven defaults, database persistence, admin UI, live watchers, change history, and optimistic concurrency.

For the React/NUI admin panel side of this system, see dirk-cfx-react → Script Config.


Full Setup Guide

Setting up scriptConfig in a new resource requires files on three layers: schema, Lua, and React. This section covers the schema and Lua layers. The React layer is covered in the dirk-cfx-react docs.

1. Create Your Schema

Create a schema.json in your resource root. This JSON Schema defines every configurable setting, its type, and default value.

json
{
  "type": "object",
  "properties": {
    "basic": {
      "type": "object",
      "properties": {
        "debug": { "type": "boolean", "default": false },
        "interactionType": { "type": "string", "default": "target" },
        "maxPlayers": { "type": "number", "default": 32 },
        "secretKey": {
          "type": "string",
          "default": "changeme",
          "x-serverOnly": true
        }
      }
    },
    "rewards": {
      "type": "array",
      "x-arrayKey": "name",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "label": { "type": "string" },
          "amount": { "type": "number", "default": 1 }
        }
      }
    }
  }
}

You can optionally create a defaults.json with initial default values. If omitted, defaults are derived from the "default" fields in the schema.

2. Set Up Your fxmanifest.lua

lua
fx_version 'cerulean'
game 'gta5'

shared_scripts {
  '@dirk_lib/init.lua',
  'src/shared/*.lua',
}

client_scripts {
  'src/client/*.lua',
}

server_scripts {
  '@oxmysql/lib/MySQL.lua',
  'src/server/*.lua',
}

ui_page 'web/build/index.html'

files {
  'web/build/**',
  'schema.json',
  'defaults.json',
  'locales/*.json',
}

3. Register on the Server

Create src/server/scriptConfig.lua:

lua
CreateThread(function()
  lib.scriptConfig(schema, function(src)
    return IsPlayerAceAllowed(src, 'admin')
  end)
end)

That single call handles everything:

  1. Waits for MySQL to be ready
  2. Creates the dirk_scriptConfig DB table if needed
  3. Loads stored settings from the database
  4. Smart-merges with schema defaults (schema is always source-of-truth)
  5. Computes a content-hash version for cache invalidation
  6. Fires all registered watchers with source = "initial"
  7. Registers all NUI callbacks for the admin panel

4. Set Up the Client

Create src/client/scriptConfig.lua:

lua
local _ = lib.scriptConfig  -- triggers lazy-load + KVP cache

The client:

  1. Checks the resource KVP cache for a fast-path load
  2. Calls the server with its cached version number
  3. If the server has a newer version, receives the delta and merges locally
  4. Caches updated settings to KVP for next startup
  5. Fires any registered client-side watchers

5. Add Watchers (Server)

Register watchers in your server files to react to settings changes in real-time:

lua
-- src/server/init.lua
lib.scriptConfig.on('rewards', function(rewards)
  registerRewardItems(rewards)
end)

lib.scriptConfig.on('basic', function(basic)
  if basic.debug then
    RegisterCommand('debugMyResource', function(src)
      -- debug command logic
    end, true)
  end
end)

-- Wildcard: react to ALL changes
lib.scriptConfig.on('*', function()
  regenerateConfigFiles()
end)

6. Add Watchers (Client — Optional)

lua
-- src/client/controls.lua
lib.scriptConfig.on('basic', function(basic)
  local controls = basic.defaultControls or {}
  RegisterKeyMapping('+myAction', 'Do Something',
    controls.main?._type or 'keyboard',
    controls.main?._key or 'E')
end)

7. Access Settings Anywhere

Read settings directly in any server or client file:

lua
-- Direct access (available after init)
local debug = scriptConfig.?.basic?.debug
local rewards = scriptConfig.?.rewards or {}

-- Via getter
local value = lib.scriptConfig.get('basic.maxPlayers')

â„šī¸ Optional helper pattern: Some resources define getLive* convenience functions in shared code for null-safe access:

lua
-- src/shared/utils.lua
function getLiveBasic()
  return scriptConfig.?.basic or {}
end

This is entirely optional — you can access scriptConfig. directly.

8. Build the Admin Panel (React)

See dirk-cfx-react → Script Config for the full React setup guide covering createScriptConfig, SettingsPanel, form sections, and the NUI callback flow.


API Reference

Client

scriptConfig.get

Returns the current settings. Also callable via scriptConfig.().

lua
local settings = scriptConfig.get()
-- or
local settings = scriptConfig.()

scriptConfig.on

Watch for settings changes. Returns an unsubscribe function.

lua
local unsubscribe = scriptConfig.on('basic', function(newValue, oldValue, meta)
    print('basic settings changed', meta.path)
end, { immediate = true })

-- Stop watching
unsubscribe()
ParameterTypeRequiredDescription
pathstringyesDot-separated path to watch, or '*' for all changes
cbfunctionyesfunction(newValue, oldValue, meta)
optionstableno{ once?: boolean, immediate?: boolean }

immediate defaults to true — the callback fires immediately with the current value if settings are already loaded. once auto-unsubscribes after the first fire.

Path matching rules:

  • Exact match: Watch "basic", triggered by "basic" change
  • Parent match: Watch "basic", triggered by "basic.debug" change
  • Child match: Watch "basic.skillSettings", triggered by "basic" change
  • Wildcard: "*" catches all changes

Watcher meta table:

FieldTypeDescription
pathstringWatched path
changedPathsstring[]Which leaf paths actually changed
sourcestring'load', 'refresh', 'update', or 'initial'
currenttableFull current settings
previoustableFull previous settings

scriptConfig.set

Update settings (admin only). Sends the change to the server.

lua
scriptConfig.set({ basic = { debug = true } }, expectedVersion)
ParameterTypeRequiredDescription
datatableyesPartial settings to merge
expectedVersionnumbernoOptimistic concurrency version

scriptConfig.getAll

Get the full unfiltered settings (admin only).

lua
local allSettings = scriptConfig.getAll(source)

Server

scriptConfig.(schema, canEditFn?, rules?)

Initialise the settings system. Call once during resource start. Creates the DB table, loads persisted data, and smart-merges with schema defaults.

lua
lib.scriptConfig(schema, function(src)
    return IsPlayerAceAllowed(src, 'admin')
end, {
    migrations = {
        ['1.2.0'] = function(data)
            data.basic.newField = data.basic.oldField
            data.basic.oldField = nil
            return data
        end,
    },
    ui = {
        command = 'myres:settings',
        help = 'Open settings panel',
        restricted = 'group.admin',
    },
})
ParameterTypeRequiredDescription
schematableyesJSON Schema definition with defaults
canEditFnfunctionnofunction(src) → boolean — additional permission grant on top of the master ACE + overrides (see Access Control). Purely additive: cannot lock out the master.
rulestablenoMigration and UI rules (see below)

Save permission is gated by the master ACE list (dirk_lib_master_group convar) and per-resource overrides first. Your canEditFn only fires as a fallback when the master/override check denies. If you want to let a non-master role edit just your resource, prefer adding a per-resource override in dirk_lib's Script Config tab — that survives in DB and doesn't require code changes.

exports.dirk_lib:canEditScriptConfig(src, resourceName)

Authoritative server-side permission check. Returns true if the player can edit the named resource's scriptConfig (master ACE list + overrides + the consumer's own canEditFn). Useful when you want to gate a custom admin command or feature with the same access model dirk_lib uses internally.

lua
if exports.dirk_lib:canEditScriptConfig(source, GetCurrentResourceName()) then
    -- show admin-only menu, run admin command, etc.
end

Rules

lua
rules = {
    migrations = {
        ['1.2.0'] = function(data) return data end,
    },
    ui = {                             -- or false to disable UI entirely
        command = 'myres:settings',    -- default: '{scriptName}:settings'
        help = 'Open settings',
        restricted = 'group.admin',
        openEvent = 'myres:openSettings',
        allowTargetArg = true,         -- allow /command [playerId]
    },
}

Schema Extensions

ExtensionTypeDescription
x-serverOnlybooleanMarks a path as server-only — filtered from client responses and the admin panel
x-renamedFromstringDeclarative key migration — old path is automatically moved to new path on load
x-arrayKeystringIdentity key for array-level smart merge (merge by unique field instead of by index)

â„šī¸ Smart merge behaviour: When settings load from the database, the schema is the source of truth. New keys from the schema get their defaults, removed keys are pruned, type mismatches revert to defaults, and arrays with x-arrayKey merge by identity field rather than position.

scriptConfig.get

Get settings at a path.

lua
local val = scriptConfig.get('basic.maxPlayers')
local all = scriptConfig.get() -- returns everything

scriptConfig.set

Apply a partial settings update. Merges changes, persists to DB, broadcasts to clients, and fires watchers.

lua
scriptConfig.set({ basic = { maxPlayers = 64 } })

scriptConfig.on

Same watcher API as the client side.

lua
lib.scriptConfig.on('basic', function(newVal, oldVal, meta)
    print('basic changed, source:', meta.source)
end)

scriptConfig.reset

Reset all settings to schema defaults.

lua
scriptConfig.reset()

NUI Callbacks

These callbacks are registered automatically when you call lib.scriptConfig(). They power the admin panel:

CallbackPurpose
<resource>:getScriptConfigClient fetch with version check — returns delta if changed
<resource>:getFullScriptConfigAdmin panel full fetch (includes x-serverOnly paths)
<resource>:updateScriptConfigSave from admin panel with expectedVersion for conflict detection
<resource>:resetScriptConfigReset to defaults
<resource>:getScriptConfigHistoryPaginated audit log with search/filter
<resource>:giveScriptConfigItemAdmin give-item action (for testing)

Database

Settings are stored in the dirk_scriptConfig table (created automatically):

ColumnTypeDescription
scriptvarchar (PK)Resource name
datalongtextJSON settings data
client_versionintContent-based hash (31-bit) for cache invalidation
resource_versionvarcharResource version from manifest at last save
change_loglongtextJSON array of audit entries (max 250)
last_editorlongtextJSON { source, name, identifier } of last admin
lastupdatedtimestampAutomatic update timestamp

Change log entry format:

lua
{
  at_unix = 1710454800,
  at_utc = "2024-03-14T15:20:00Z",
  script = "my_resource",
  admin = { source = 5, name = "Admin", identifier = "license:abc123" },
  expected_version = 12345,
  applied_version = 67890,
  changes = {
    { path = "basic.debug", old = false, new = true },
    { path = "basic.maxPlayers", old = 32, new = 64 },
  },
}

Version Conflict Detection

The system uses content-based hashing for optimistic concurrency:

  1. When settings are saved, a 31-bit hash is computed from the canonical JSON
  2. The admin panel sends expectedVersion with every update
  3. If the server's current version doesn't match, the update is rejected
  4. The server returns the latest data so the admin can review and re-save

This prevents two admins from silently overwriting each other's changes.


Watcher Patterns

Common server-side patterns

lua
-- Re-register useables when equipment config changes
lib.scriptConfig.on('equipment', function()
  registerEquipmentUseables()
end)

-- Conditionally register debug commands
lib.scriptConfig.on('basic', function(basic)
  if not basic.debug then return end
  RegisterCommand('debugInfo', function(src)
    -- debug logic
  end, true)
end)

-- React to ALL changes (e.g. regenerate config files)
lib.scriptConfig.on('*', function()
  regenerateExportFiles()
end)

Common client-side patterns

lua
-- Register keybinds from settings
lib.scriptConfig.on('basic', function(basic)
  local controls = basic.defaultControls or {}
  RegisterKeyMapping('+myAction', 'My Action',
    controls.main?._type or 'keyboard',
    controls.main?._key or 'E')
end)

Shared-side patterns

lua
-- Recalculate derived values used on both sides
lib.scriptConfig.on('basic', function(basic)
  local skillSettings = basic.skillSettings or {}
  MySkill.baseLevel = skillSettings.baseLevel or 1
  MySkill.maxLevel = skillSettings.maxLevel or 99
  generateLevelMap()
end)

Real-World Example (dirk_fishing)

The fishing resource demonstrates the full system:

Server watchers (src/server/init.lua):

lua
lib.scriptConfig.on('equipment', function() registerRodUseables() end)
lib.scriptConfig.on('basic', function() registerGuidebookUseable() end)
lib.scriptConfig.on('stores', function() registerStoresAndPrices() end)
lib.scriptConfig.on('fish', function() registerStoresAndPrices() end)
lib.scriptConfig.on('baitDig', function() registerBaitDigUseables() end)

Wildcard watcher (src/server/install.lua):

lua
lib.scriptConfig.on('*', function()
  -- Regenerate ox.lua, qb.lua, esx.sql item definition files
  -- whenever ANY setting changes
  local basic = scriptConfig.?.basic or {}
  local fish = scriptConfig.?.fish or {}
  local equipment = scriptConfig.?.equipment or {}
  -- ... generate and save files
end)

Client keybind registration (src/client/permit.lua):

lua
lib.scriptConfig.on('basic', function(basic)
  if permitControlsRegistered then return end
  permitControlsRegistered = true

  local controls = basic?.defaultControls or {}
  RegisterKeyMapping('+flipFishingPermit', 'Flip Fishing Permit',
    controls.flipCard?.main?._type or 'keyboard',
    controls.flipCard?.main?._key or 'r')
end)

Copyright Š 2026 DirkScripts.

Not affiliated with or endorsed by Rockstar North, Take-Two Interactive, or any other rights holders. FiveM is a copyright and registered trademark of Take-Two Interactive Software, Inc.
Our checkout system is provided by Tebex Limited, who manage payment processing, product delivery, and billing support. Prices shown in currencies other than GBP are approximate conversions updated daily. All purchases are processed in GBP, so the final amount charged may vary depending on your bank or payment provider’s exchange rate.