# esx_datastore (/docs/addons/esx_datastore)

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 [#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 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 [#installation]

<Steps>
<Step>

### Database [#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:

```sql title="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;
```

</Step>
<Step>

### server.cfg [#servercfg]

Start it after `esx_lib` and `es_extended` and before every resource that reads datastores:

```bash title="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]`.

</Step>
<Step>

### Register a datastore [#register-a-datastore]

Add one row per datastore. `label` is informational only.

```sql
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.

</Step>
</Steps>

<Callout type="info" title="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.
</Callout>

## Configuration [#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 [#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 [#esx_datastoregetshareddatastore]

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

```lua title="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 [#esx_datastoregetdatastore]

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

```lua title="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 [#esx_datastoregetdatastoreowners]

```lua
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 [#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 [#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.

<Callout type="warn">
`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.
</Callout>

## Integration tips [#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 [#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.

## Related [#related]

- [esx_addonaccount](/docs/addons/esx_addonaccount)
- [esx_addoninventory](/docs/addons/esx_addoninventory)
- [esx_society](/docs/addons/esx_society)
- [esx_clotheshop](/docs/addons/esx_clotheshop)
- [Database schema](/docs/guides/database-schema)
