FiveM Docs
Addons

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

FactValue
Resourceesx_datastore
SideServer only
Dependenciesesx_lib (plus es_extended and oxmysql via imports)
Tablesdatastore, datastore_data
Used byesx_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 as society_police.
  • Per player (shared = 0): one store per character identifier. esx_datastore creates the missing datastore_data row 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:

esx_datastore.sql
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:

server.cfg
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_datastore

The 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 player

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

ColumnTypeDescription
nameVARCHAR(60)Unique datastore name used in code.
labelVARCHAR(100)Display label. Not used by the resource.
sharedINT1 = 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)
NameTypeDescription
namestringName of a datastore with shared = 1.
cbfunction(store)Receives the store object, or nil if it does not exist.
server/main.lua
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)
NameTypeDescription
namestringName of a datastore with shared = 0.
ownerstringCharacter identifier (xPlayer.getIdentifier()).
cbfunction(store)Receives the store object, or nil if it does not exist.
server/main.lua
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)
NameTypeDescription
namestringName of a non-shared datastore.
cbfunction(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:

MemberDescription
store.nameDatastore name.
store.ownerOwner identifier, or nil for shared stores.
store.dataThe 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 onResourceStop all 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_society and the job addons expect that pattern.
  • Use a per-player store (shared = 0) instead of new users columns for small optional data like favorites or saved presets.
  • Keep values small. Every save writes the whole JSON blob of that store.
  • Because getDataStore callbacks run synchronously, you can capture the store in a local variable and use it after the TriggerEvent call.

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_clotheshop writes to a per-player datastore named property. 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.

Frequently asked questions

Independent documentation

esx-fivem.com is a community project and is not affiliated with the ESX Framework team. Facts on this page were checked against the upstream source. Official resources: docs.esx-framework.org and github.com/esx-framework.
Edit on GitHub

Last updated on

On this page