esx_datastore
esx_datastore stores JSON data per player or per society in ESX Legacy. Learn the tables, the get/set store API, server events and autosave behavior.
esx_datastore is a server-only library resource that stores arbitrary Lua tables as JSON in MySQL. Other resources use it for things like society armories, saved outfits (property → dressing) and accessory slots (user_mask, user_helmet). Use it when you need small, persistent key-value data that is tied to a player or to a job.
Overview
| Fact | Value |
|---|---|
| Resource | esx_datastore |
| Side | Server only |
| Dependencies | esx_lib (plus es_extended and oxmysql via imports) |
| Tables | datastore, datastore_data |
| Used by | esx_society, esx_policejob, esx_ambulancejob, esx_taxijob, esx_property, esx_accessories, esx_clotheshop |
There are two kinds of datastores:
- Shared (
shared = 1): one store for the whole server, without an owner. Typical for job data such associety_police. - Per player (
shared = 0): one store per character identifier. esx_datastore creates the missingdatastore_datarow for every non-shared datastore when a player loads (esx:playerLoaded).
Installation
Database
The resource loads the esx_lib SQL catalog on start. It creates the datastore and datastore_data tables automatically if they are missing. esx_datastore.sql contains the same schema if you prefer to import it manually:
CREATE TABLE `datastore` (
`name` VARCHAR(60) NOT NULL,
`label` VARCHAR(100) NOT NULL,
`shared` INT NOT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `datastore_data` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(60) NOT NULL,
`owner` VARCHAR(60),
`data` LONGTEXT,
PRIMARY KEY (`id`),
UNIQUE INDEX `index_datastore_data_name_owner` (`name`, `owner`),
INDEX `index_datastore_data_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;server.cfg
Start it after esx_lib and es_extended and before every resource that reads datastores:
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_datastoreThe default txAdmin recipe already starts it as part of [esx_addons].
Register a datastore
Add one row per datastore. label is informational only.
INSERT INTO `datastore` (`name`, `label`, `shared`) VALUES
('society_mafia', 'Mafia', 1), -- one store for the whole job
('property', 'Property', 0); -- one store per playerRestart the server (or restart esx_datastore and relog players) so the new row is loaded.
Seeded datastores
Some addons seed their own rows through the esx_lib SQL catalog: esx_accessories (user_ears, user_glasses, user_helmet, user_mask) and the job addons (society_ambulance, society_mechanic, society_police, society_taxi). When such a resource seeds new rows, esx_datastore receives esx:sqlCatalogReady and loads them without a restart.
Configuration
esx_datastore has no config.lua. Everything is driven by the rows in the datastore table.
| Column | Type | Description |
|---|---|---|
name | VARCHAR(60) | Unique datastore name used in code. |
label | VARCHAR(100) | Display label. Not used by the resource. |
shared | INT | 1 = one shared store, 0 = one store per player. |
Server events
All three events are registered with AddEventHandler only, so they can be triggered from other server resources but not from clients. The callback runs synchronously.
esx_datastore:getSharedDataStore
TriggerEvent('esx_datastore:getSharedDataStore', name, cb)| Name | Type | Description |
|---|---|---|
name | string | Name of a datastore with shared = 1. |
cb | function(store) | Receives the store object, or nil if it does not exist. |
TriggerEvent('esx_datastore:getSharedDataStore', 'society_police', function(store)
if not store then return end
local weapons = store.get('weapons') or {}
weapons[#weapons + 1] = { name = 'WEAPON_PUMPSHOTGUN', count = 1 }
store.set('weapons', weapons)
end)esx_datastore:getDataStore
TriggerEvent('esx_datastore:getDataStore', name, owner, cb)| Name | Type | Description |
|---|---|---|
name | string | Name of a datastore with shared = 0. |
owner | string | Character identifier (xPlayer.getIdentifier()). |
cb | function(store) | Receives the store object, or nil if it does not exist. |
local xPlayer = ESX.GetPlayerFromId(source)
TriggerEvent('esx_datastore:getDataStore', 'property', xPlayer.getIdentifier(), function(store)
if not store then return end
local dressing = store.get('dressing') or {}
print(('%s has %d saved outfits'):format(xPlayer.getName(), #dressing))
end)esx_datastore:getDataStoreOwners
TriggerEvent('esx_datastore:getDataStoreOwners', name, cb)| Name | Type | Description |
|---|---|---|
name | string | Name of a non-shared datastore. |
cb | function(owners) | Receives an array of identifiers that have a row for this datastore. |
Store object
Every store returned by the events above has these fields and functions:
| Member | Description |
|---|---|
store.name | Datastore name. |
store.owner | Owner identifier, or nil for shared stores. |
store.data | The raw Lua table. Prefer get/set so changes are saved. |
store.get(key, i?) | Returns data[key]. Dotted keys walk nested tables ('outfits.work'). If i is given and the value is a table, returns value[i]. Returns nil for empty or non-string keys. |
store.set(key, value) | Sets data[key] and schedules a save. Returns false for empty or non-string keys. Dotted keys are not split on set. |
store.count(key, i?) | Length (#) of the table at key, or 0. |
store.save() | Marks the store dirty and schedules a save. Called by set automatically. |
store.flush() | Writes pending changes immediately. Blocks the current thread until MySQL answers. |
store.isDirty() | true while unsaved changes exist or a write is in progress. |
Saving behavior
- Saves are debounced through
xLib.timeout: the last change wins and the JSON is written 10 seconds after it. - A failed write is logged (
[esx_datastore] Save failed for ...) and rescheduled. - On
onResourceStopall stores are flushed, so a clean restart of esx_datastore does not lose data.
get returns the live table, not a copy. If you modify a nested table in place you must still call store.set(key, value) (or store.save()), otherwise the change never reaches the database.
Integration tips
- Name society stores
society_<job>.esx_societyand the job addons expect that pattern. - Use a per-player store (
shared = 0) instead of newuserscolumns for small optional data like favorites or saved presets. - Keep values small. Every save writes the whole JSON blob of that store.
- Because
getDataStorecallbacks run synchronously, you can capture the store in a local variable and use it after theTriggerEventcall.
Common issues
attempt to index a nil value (local 'store'): the datastore row is missing or was added after the resource started. Insert the row and restart.- Outfits are not saved in the clothes shop:
esx_clotheshopwrites to a per-player datastore namedproperty. No addon seeds it, so insert('property', 'Property', 0)yourself. - Data lost after killing the server process: changes from the last ~10 seconds are only in memory. Stop the server normally so stores get flushed.
Related
Frequently asked questions
Independent documentation
Last updated on
esx_cruisecontrol
esx_cruisecontrol adds cruise control and a seatbelt with ejection to ESX Legacy. Covers keybinds, config keys, the isSeatbeltOn export and HUD integration.
esx_death
esx_death handles death state, the death screen, respawn timers, penalties and revives in ESX Legacy. Config keys, exports, events and /revive explained.