# Database Schema (/docs/guides/database-schema)

ESX Legacy stores all persistent data in a MySQL or MariaDB database accessed through `oxmysql`. The complete schema lives in [`[SQL]/legacy.sql`](https://github.com/esx-framework/esx_core/blob/main/%5BSQL%5D/legacy.sql) in esx_core, and each core resource and addon ships its own `.sql` file with the tables it needs. This page lists every table, its columns and which resource reads or writes it.

## How the schema is created [#how-the-schema-is-created]

There are three ways tables end up in your database:

1. **`legacy.sql`**: the full schema with seed data (jobs, items, vehicles, fines). txAdmin's ESX recipe imports it for you.
2. **Per-resource `.sql` files**: for example `esx_billing.sql` or `esx_identity.sql`. Useful if you install resources one by one.
3. **Automatic migrations**: since 1.16.0, `es_extended` loads the **SQL catalog** from `esx_lib` on startup. It creates missing tables, adds missing columns and inserts missing seed rows for `es_extended` and the official resources (identity, multicharacter, skin, addonaccount, billing, banking, garage, jobs and more). It records what it did in `esx_catalog_imports`. Several addons (`esx_billing`, `esx_banking`, `esx_garage`, `esx_addonaccount`, `esx_license`, `esx_whitelist`, `esx_property`) also run their own migrations.

<Callout type="info" title="Seed rows are only inserted when missing">
The catalog inserts a seed row only if no row with the same key exists (`jobs.name`, `job_grades (job_name, grade)`, `items.name` and so on). It does not overwrite your own changes to labels or salaries.
</Callout>

## Core tables (es_extended) [#core-tables-es_extended]

### users [#users]

One row per character. Created by `es_extended`; several resources add columns.

| Column | Type | Default | Added by | Notes |
| --- | --- | --- | --- | --- |
| `identifier` | `varchar(60)` | none | es_extended | **Primary key.** License without prefix, or `char<N>:<license>` with multicharacter |
| `ssn` | `varchar(11)` | none | es_extended | Unique (`unique_ssn`). Generated per character, read with `xPlayer.getSSN()` |
| `accounts` | `longtext` | `NULL` | es_extended | JSON object of account name to balance |
| `group` | `varchar(50)` | `'user'` | es_extended | Permission group, see [Admin permissions](/docs/guides/admin-permissions) |
| `inventory` | `longtext` | `NULL` | es_extended | JSON of item name to count (default inventory) |
| `job` | `varchar(20)` | `'unemployed'` | es_extended | Must match `jobs.name` |
| `job_grade` | `int(11)` | `0` | es_extended | Must match `job_grades.grade` |
| `loadout` | `longtext` | `NULL` | es_extended | JSON weapons list (default inventory) |
| `metadata` | `longtext` | `NULL` | es_extended | JSON, read with `xPlayer.getMeta()` |
| `position` | `longtext` | `NULL` | es_extended | JSON coords and heading |
| `firstname`, `lastname` | `varchar(16)` | `NULL` | esx_identity | |
| `dateofbirth` | `varchar(10)` | `NULL` | esx_identity | |
| `sex` | `varchar(1)` | `NULL` | esx_identity | `m` or `f` |
| `height` | `int(11)` | `NULL` | esx_identity | |
| `skin` | `longtext` | `NULL` | esx_skin | JSON skinchanger data |
| `status` | `longtext` | `NULL` | esx_status | Hunger, thirst and other statuses |
| `is_dead` | `tinyint(1)` | `0` | esx_ambulancejob / esx_death | Used when `Config.SaveDeathStatus` is on |
| `death_time` | `bigint` | `NULL` | esx_death | Added by `esx_death.sql` |
| `id` | `int(11)` | auto increment | legacy.sql | Unique key `id` |
| `disabled` | `tinyint(1)` | `0` | esx_multicharacter | Disabled character slots |
| `last_property` | `longtext` | `NULL` | esx_property | Last property the player was in |
| `created_at` | `timestamp` | `CURRENT_TIMESTAMP` | legacy.sql | |
| `last_seen` | `timestamp` | `NULL`, `ON UPDATE CURRENT_TIMESTAMP` | legacy.sql | Updated whenever the row changes |
| `phone_number` | `varchar(20)` | `NULL` | legacy.sql | Not read by es_extended; available for phone resources |
| `pincode` | `int` | `NULL` | esx_banking | ATM PIN |

Example `accounts` value with the default `Config.Accounts`:

```json
{ "bank": 50000, "money": 0, "black_money": 0 }
```

### jobs [#jobs]

| Column | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | `varchar(50)` | none | **Primary key**, used in `users.job` and `xPlayer.setJob()` |
| `label` | `varchar(50)` | `NULL` | Display name |
| `type` | `varchar(50)` | `'civ'` | Job type, filter with `ESX.GetJobs(type)`. Seeds use `civ`, `leo`, `ems`, `mechanic` |
| `whitelisted` | `tinyint(1)` | `0` | Used by `esx_joblisting` to hide jobs from the job center |

### job_grades [#job_grades]

| Column | Type | Default | Notes |
| --- | --- | --- | --- |
| `id` | `int(11)` | auto increment | **Primary key** |
| `job_name` | `varchar(50)` | `NULL` | References `jobs.name` (no foreign key) |
| `grade` | `int(11)` | none | Grade number, starting at 0 |
| `name` | `varchar(50)` | none | Internal grade name. `boss` grants the society boss menu by default |
| `label` | `varchar(50)` | none | Display name |
| `salary` | `int(11)` | none | Paycheck amount per `Config.PaycheckInterval` |
| `skin_male`, `skin_female` | `longtext` | none | JSON uniform, `'{}'` if unused. Written by the esx_society uniform manager |

A job without any grade is ignored by `ESX.RefreshJobs()` with a warning. See [Create a job](/docs/guides/create-a-job).

### items [#items]

| Column | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | `varchar(50)` | none | **Primary key** |
| `label` | `varchar(50)` | none | Display name |
| `weight` | `int(11)` | `1` | Counted against `Config.MaxWeight` |
| `rare` | `tinyint(4)` | `0` | Stored and sent to the client; no core gameplay effect |
| `can_remove` | `tinyint(4)` | `1` | `0` blocks dropping and giving the item |

Not used when `ox_inventory` runs; items then come from `ox_inventory/data/items.lua`. See [Add items](/docs/guides/add-items).

### owned_vehicles [#owned_vehicles]

Created by `esx_vehicleshop.sql` and `legacy.sql`; extended by `esx_garage`. Used by es_extended (extended vehicles), esx_garage, esx_vehicleshop, esx_policejob, esx_lscustom, esx_boat, esx_property and others.

| Column | Type | Default | Added by | Notes |
| --- | --- | --- | --- | --- |
| `owner` | `varchar(60)` | `NULL` | core | `users.identifier`. Indexed |
| `plate` | `varchar(12)` | none | core | **Primary key** |
| `vehicle` | `longtext` | `NULL` | core | JSON vehicle properties |
| `type` | `varchar(20)` | `'car'` | core | `car`, `boat`, `aircraft`... |
| `job` | `varchar(20)` | `NULL` | core | Set for job vehicles |
| `stored` | `tinyint(4)` | `0` | core | 1 = in a garage |
| `parking` | `varchar(60)` | `NULL` | esx_garage | Garage the vehicle is parked in |
| `pound` | `varchar(60)` | `NULL` | esx_garage | Impound location |
| `custom_name` | `varchar(50)` | `NULL` | esx_garage | Player-chosen name |
| `is_favorite` | `tinyint(1)` | `0` | esx_garage | |
| `last_used` | `int` | `NULL` | esx_garage | |
| `mileage` | `decimal(10,2)` | `0.00` | esx_garage | |

`esx_garage` also creates composite indexes such as `idx_owned_vehicles_owner_plate` and `idx_owned_vehicles_owner_stored_pound_plate`, and tracks its schema version in `esx_garage_migrations`.

### esx_catalog_imports [#esx_catalog_imports]

Created by the `esx_lib` SQL catalog.

| Column | Type | Notes |
| --- | --- | --- |
| `resource` | `varchar(64)` | **Primary key**, resource whose seed data was prepared |
| `version` | `varchar(32)` | Catalog version, for example `1.16.0` |
| `locale` | `varchar(32)` | Locale used for seeded labels |
| `updated_at` | `timestamp` | |

## Character and identity tables [#character-and-identity-tables]

### multicharacter_slots [#multicharacter_slots]

Used by `esx_multicharacter` to give individual players more character slots.

| Column | Type | Notes |
| --- | --- | --- |
| `identifier` | `varchar(60)` | **Primary key**, license without the `char` prefix |
| `slots` | `int(11)` | Number of slots. Indexed |

### whitelist [#whitelist]

| Column | Type | Notes |
| --- | --- | --- |
| `identifier` | `varchar(60)` | **Primary key** |

This table is still in `legacy.sql`, but the current `esx_whitelist` resource uses the two tables below instead.

### esx_whitelist_config and esx_whitelist_identifiers [#esx_whitelist_config-and-esx_whitelist_identifiers]

Created by `esx_whitelist` (`whitelist.sql` or automatically on start).

| Table | Column | Type | Notes |
| --- | --- | --- | --- |
| `esx_whitelist_config` | `setting_key` | `varchar(64)` | **Primary key** |
| | `setting_value` | `longtext` | |
| | `updated_at` | `timestamp` | Auto-updated |
| `esx_whitelist_identifiers` | `identifier` | `varchar(128)` | Primary key with `type` |
| | `type` | `varchar(32)` | Default `'whitelist'`, indexed |
| | `created_at` | `timestamp` | |

## Society and shared data tables [#society-and-shared-data-tables]

### addon_account and addon_account_data [#addon_account-and-addon_account_data]

Used by `esx_addonaccount`, `esx_society` (society money) and `esx_vehicleshop`. Society accounts are named `society_<job>`.

| Table | Column | Type | Notes |
| --- | --- | --- | --- |
| `addon_account` | `name` | `varchar(60)` | **Primary key**, e.g. `society_police` |
| | `label` | `varchar(100)` | |
| | `shared` | `int(11)` | `1` = one account for everyone (society), `0` = one per player |
| `addon_account_data` | `id` | `int(11)` | **Primary key**, auto increment |
| | `account_name` | `varchar(100)` | Indexed |
| | `money` | `int(11)` | Balance |
| | `owner` | `varchar(60)` | `NULL` for shared accounts, identifier otherwise. Indexed |

Unique key `(account_name, owner)`. The 2026-10-02 hardening adds a generated `owner_key` column (`COALESCE(owner, '')`) with a unique key, so shared accounts cannot be duplicated either.

### addon_inventory and addon_inventory_items [#addon_inventory-and-addon_inventory_items]

Used by `esx_addoninventory` for society and property stashes (default inventory).

| Table | Column | Type | Notes |
| --- | --- | --- | --- |
| `addon_inventory` | `name` | `varchar(60)` | **Primary key** |
| | `label` | `varchar(100)` | |
| | `shared` | `int(11)` | Shared or per player |
| `addon_inventory_items` | `id` | `int(11)` | **Primary key**, auto increment |
| | `inventory_name` | `varchar(100)` | Indexed |
| | `name` | `varchar(100)` | Item name |
| | `count` | `int(11)` | |
| | `owner` | `varchar(60)` | `NULL` for shared. Indexed |

### datastore and datastore_data [#datastore-and-datastore_data]

Generic JSON storage, used by `esx_datastore`, `esx_accessories` (`user_mask`, `user_helmet`...), `esx_property` and society resources.

| Table | Column | Type | Notes |
| --- | --- | --- | --- |
| `datastore` | `name` | `varchar(60)` | **Primary key** |
| | `label` | `varchar(100)` | |
| | `shared` | `int(11)` | |
| `datastore_data` | `id` | `int(11)` | **Primary key**, auto increment |
| | `name` | `varchar(60)` | Datastore name |
| | `owner` | `varchar(60)` | Indexed |
| | `data` | `longtext` | JSON |

Unique key `(name, owner)`.

### society_moneywash [#society_moneywash]

Used by `esx_society` for money washing.

| Column | Type | Notes |
| --- | --- | --- |
| `id` | `int(11)` | **Primary key**, auto increment |
| `identifier` | `varchar(60)` | Indexed |
| `society` | `varchar(60)` | |
| `amount` | `int(11)` | |

Current `esx_society` also creates `society_moneywash_operations` (`operation_id` primary key, `identifier`, `society`, `amount`, `state`, `created_at`) on start and moves pending rows from `society_moneywash` into it.

## Economy tables [#economy-tables]

### billing [#billing]

Used by `esx_billing` (and jobs that send bills).

| Column | Type | Notes |
| --- | --- | --- |
| `id` | `int(11)` | **Primary key**, auto increment |
| `identifier` | `varchar(60)` | Player who must pay. Indexed |
| `sender` | `varchar(60)` | Identifier of the sender |
| `target_type` | `varchar(50)` | `player` or `society` |
| `target` | `varchar(60)` | Receiving identifier or society account |
| `label` | `varchar(255)` | |
| `amount` | `int(11)` | |

### billing_payments [#billing_payments]

Created by `esx_billing` on start (and by the hardening script). It is a payment journal that makes paying a bill idempotent.

| Column | Type | Notes |
| --- | --- | --- |
| `bill_id` | `int` | **Primary key** |
| `operation_id` | `varchar(64)` | Unique |
| `identifier`, `sender` | `varchar(60)` | |
| `target_type` | `varchar(10)` | |
| `target` | `varchar(100)` | |
| `label` | `varchar(255)` | |
| `amount` | `int` | |
| `payment_account` | `varchar(20)` | Account used to pay |
| `state` | `enum('claimed','applied')` | Default `claimed` |
| `created_at`, `applied_at` | `timestamp` | |

Rows stuck in `claimed` should be inspected manually. The hardening script explicitly says not to replay or refund them automatically.

### banking [#banking]

Transaction history for `esx_banking`.

| Column | Type | Notes |
| --- | --- | --- |
| `ID` | `int(11)` | **Primary key**, auto increment |
| `identifier` | `varchar(46)` in legacy.sql | `esx_banking` widens it to `varchar(60)` on start |
| `type` | `varchar(50)` | Deposit, withdraw, transfer... |
| `amount` | `int(64)` | |
| `time` | `bigint(20)` | Unix time in ms |
| `balance` | `int(11)` | Balance after the transaction |
| `label` | `varchar(255)` | |

Keys: `idx_banking_identifier_time (identifier, time)` and `idx_banking_time (time)`.

### fine_types [#fine_types]

Fine catalog for `esx_policejob`.

| Column | Type | Notes |
| --- | --- | --- |
| `id` | `int` | **Primary key**, auto increment |
| `label` | `varchar(255)` | |
| `amount` | `int` | |
| `category` | `int` | Groups fines in the police fine menu. Seed data uses 0 (traffic) to 3 (most severe) |

## License tables [#license-tables]

Used by `esx_license`, and through it by `esx_dmvschool`, `esx_weaponshop`, `esx_boat` and `esx_drugs`.

| Table | Column | Type | Notes |
| --- | --- | --- | --- |
| `licenses` | `type` | `varchar(60)` | **Primary key**, e.g. `dmv`, `drive`, `weapon` |
| | `label` | `varchar(60)` | |
| `user_licenses` | `id` | `int(11)` | **Primary key**, auto increment |
| | `type` | `varchar(60)` | License type |
| | `owner` | `varchar(60)` | Identifier |

Unique key `uq_user_licenses_owner_type (owner, type)` so a player cannot hold the same license twice.

## Vehicle shop tables [#vehicle-shop-tables]

All used by `esx_vehicleshop` (`vehicles` is also read by `esx_lscustom`).

| Table | Columns | Notes |
| --- | --- | --- |
| `vehicles` | `name varchar(60)`, `model varchar(60)` (**PK**), `price int(11)`, `category varchar(60)` | Vehicles for sale |
| `vehicle_categories` | `name varchar(60)` (**PK**), `label varchar(60)` | Shop categories |
| `cardealer_vehicles` | `id int(11)` (**PK**), `vehicle varchar(255)`, `price int(11)` | Car dealer job stock |
| `vehicle_sold` | `id int(11)` (**PK**), `client`, `model`, `plate`, `soldby`, `date` (all `varchar(50)`) | Sales log of the car dealer job |
| `rented_vehicles` | `vehicle varchar(60)`, `plate varchar(12)` (**PK**), `player_name varchar(255)`, `base_price int`, `rent_price int`, `owner varchar(60)` | Rentals |

`esx_vehicleshop` also creates `vehicle_rental_payments` (`plate`, `billing_date` as primary key, `owner`, `amount`, `operation_id` unique, `state`) on start.

## Other tables created by addons [#other-tables-created-by-addons]

| Table | Created by | Purpose |
| --- | --- | --- |
| `esx_garage_migrations` | esx_garage | Applied garage schema migrations (`name`, `version`, `applied_at`) |
| `esx_properties` | esx_property | Property definitions as JSON (`id`, `data`) |
| `esx_property_migrations` | esx_property | Applied property migrations |
| `bans` | esx_adminmenu | Bans with `identifier`, `reason`, `banned_by`, `expires_at` |
| `admin_logs` | esx_adminmenu | Admin action log (actor, target, action, payload) |

## Inspecting your own database [#inspecting-your-own-database]

```sql
-- Which catalog imports ran
SELECT * FROM esx_catalog_imports;

-- Jobs that have no grades (ignored by ESX)
SELECT j.name FROM jobs j
LEFT JOIN job_grades g ON g.job_name = j.name
WHERE g.id IS NULL;

-- Players whose job no longer exists (reset to unemployed on load)
SELECT identifier, job FROM users
WHERE job NOT IN (SELECT name FROM jobs);
```

## Common issues [#common-issues]

- **`Unknown column 'ssn'`** on old databases: start `es_extended` 1.16 once, its migration adds and fills `users.ssn`. It stops if duplicate SSNs exist.
- **"Economy tables must use InnoDB"**: convert `billing`, `users` and `addon_account_data` with `ALTER TABLE ... ENGINE=InnoDB` after a backup.
- **Duplicate key errors in the hardening script**: you have duplicate `user_licenses` or `addon_account_data` rows. Reconcile them by hand; the script never deletes financial rows.
- **Editing JSON columns while the server runs**: online players are kept in memory and overwrite your change on the next save. Edit offline players only, or use in-game commands.

## Related [#related]

- [Create a job](/docs/guides/create-a-job)
- [Add items](/docs/guides/add-items)
- [Money and accounts](/docs/guides/money-and-accounts)
- [oxmysql with ESX](/docs/guides/oxmysql)
- [Performance](/docs/guides/performance)
