Skip to main content
Home
Shop
Free Mods
Tools
Bundles
Full Servers
  1. Home
  2. Blog
  3. Development

SQL & Identifiers Migration: steam/license to citizenid

Published on August 16, 2025·by Lars Miller(Founder & Lead Editor)·Credentials·8 min read·Updated on March 24, 2026
Developmentsql identifiers migration steam/license to citizenid

1. Freeze writes during migration (stop the game server + any external bots touching DB). 2. Full backup and a dump of table structures. Store both with timestamps. 3.

SQL & Identifiers Migration: steam/license to citizenid
SQL & Identifiers Migration: steam/license to citizenid

Introduction to Use‑case: You’re moving from ESX to QBCore or QBOX

FiveM SQL Database Migration Guide

Use‑case: You’re moving from ESX to QBCore or QBOX (qbx_core) and need a clean, auditable migration of player identifiers and balances. This guide gives you production‑ready SQL, a reversible plan, and validation steps.

This guide is part of our complete FiveM frameworks guide, where we compare ESX, QBCore, and QBOX in depth and help you choose the right one.

Related reads:

  • Adapter Patterns: ESX↔QBCore↔QBOX (Exports, Events & Player Models) — https://fivemx.com/blog/adapter-patterns
  • Converting FiveM Scripts – ESX, QBCore, QBOX (Framework Guide) — https://fivemx.com/blog/converting-fivem-scripts/ (Pillar)

What changes between ESX and QBCore/QBOX

TopicESX (common)QBCore / QBOX (common)
Primary player keyidentifier (e.g., license:xxx or legacy steam:xxx)citizenid (server‑generated token)
Alt identifiersusers.identifier, sometimes a separate identifiers tablecolumns like license, steam, fivem stored alongside citizenid
Money modelSeparate accounts (cash/bank/black_money) via users.accounts (JSON) or user_accounts rowsSingle money JSON on players (e.g., { "cash": 0, "bank": 5000 }); optional extra wallets
Vehiclesowned_vehicles.owner refers to ESX identifierplayer_vehicles.citizenid (or license on some forks)

QBOX generally follows QB’s DB shape. Treat QBOX as “QB schema + qbx additions.” Always diff your live schema.


Golden rules (don’t skip)

  1. Freeze writes during migration (stop the game server + any external bots touching DB).
  2. Full backup and a dump of table structures. Store both with timestamps.
  3. Work in a transaction per table if possible; keep steps idempotent.
  4. Create a crosswalk (old_identifier → citizenid) you can reuse or roll back to.

Target you’re aiming for (QB/QBOX baseline)

A typical players table (columns vary by fork):

-- Inspect your actual schema and adjust.

DESCRIBE players; -- Expect columns like: citizenid, license, name, money, charinfo, job, gang, metadata

  • citizenid: primary key used across QB/QBOX.
  • license/steam: keep for forensics and re‑linking.
  • money (JSON): e.g. {"cash":123,"bank":456}. Some servers add crypto, dirty, etc.

Step 0 — Snapshot & staging

# MySQL/MariaDB backup mysqldump -u root -p --routines --triggers yourdb > yourdb_$(date +%F_%H%M).sql

Optional: structure‑only snapshot

mysqldump -u root -p --no-data yourdb > yourdb_schema_$(date +%F_%H%M).sql

Spin up a staging copy

Spin up a staging copy. Run everything there first

Step 1 — Build the crosswalk table

We’ll map every ESX identifier to a new citizenid. If you already have a players table with citizenids, you’ll invert the mapping (see Existing QB players note below).

-- 1) Create crosswalk
CREATE TABLE IF NOT EXISTS identifier_crosswalk (

old_identifier VARCHAR(60) PRIMARY KEY, citizenid VARCHAR(20) NOT NULL, license VARCHAR(60) NULL, steam VARCHAR(60) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2) Seed from ESX users (adjust table/column names to your ESX flavor)
-- Common ESX has \`users.identifier\` holding license:xxx or steam:xxx
INSERT IGNORE INTO identifier_crosswalk (old_identifier, license, steam, citizenid)

SELECT u.identifier AS old_identifier, CASE WHEN u.identifier LIKE 'license:%' THEN u.identifier ELSE NULL END AS license, CASE WHEN u.identifier LIKE 'steam:%' THEN u.identifier ELSE NULL END AS steam, UPPER(SUBSTRING(REPLACE(UUID(),'-',''),1,10)) AS citizenid FROM users u;

-- 3) If you have a separate \`identifiers\` table, merge best‑known values
-- Example (optional): prefer license when available
UPDATE identifier_crosswalk x

JOIN (

  SELECT i1.identifier AS old_identifier,
         MAX(CASE WHEN i1.type='license' THEN i1.value END) AS license,
         MAX(CASE WHEN i1.type='steam'   THEN i1.value END) AS steam

FROM identifiers i1 GROUP BY i1.identifier

) i ON i.old_identifier = x.old_identifier

SET x.license = COALESCE(i.license, x.license),

    x.steam   = COALESCE(i.steam,   x.steam);

-- 4) Uniqueness & indexes
ALTER TABLE identifier_crosswalk

ADD UNIQUE KEY ux_cid (citizenid), ADD KEY ix_license (license), ADD KEY ix_steam (steam);

Existing QB players? If you already have players rows, create the crosswalk by selecting their license/steam and existing citizenid instead of generating new ones. Your crosswalk must never assign a new citizenid to an existing QB player.


Step 2 — Normalize/prepare target players rows

Create any missing players rows based on ESX users

Create any missing players rows based on ESX users.

-- Ensure \`players\` exists and inspect its columns first.
-- We’ll insert shells for missing citizens only.

INSERT INTO players (citizenid, license, name, money, charinfo, metadata)

SELECT x.citizenid, COALESCE(NULLIF(x.license,''), NULLIF(x.steam,'')) AS license_like, COALESCE(u.firstname, '') || ' ' || COALESCE(u.lastname, '') AS name_like,

  '{"cash":0,"bank":0}' AS money,

JSON_OBJECT(

    'firstName', COALESCE(u.firstname,''),
    'lastName',  COALESCE(u.lastname,''),
    'birthdate', COALESCE(u.dateofbirth,''),
    'gender',    COALESCE(u.sex,'')
  ) AS charinfo,

JSON_OBJECT('esx_identifier', u.identifier) AS metadata FROM users u JOIN identifier_crosswalk x ON x.old_identifier = u.identifier LEFT JOIN players p ON p.citizenid = x.citizenid WHERE p.citizenid IS NULL;

Note: Use your SQL flavor’s string concat (CONCAT in MySQL) and JSON functions accordingly. For MySQL 5.7, replace JSON_OBJECT with manual string building if needed.

MySQL‑safe variant:

INSERT INTO players (citizenid, license, name, money, charinfo, metadata)

SELECT x.citizenid, COALESCE(NULLIF(x.license,''), NULLIF(x.steam,'')) AS license_like, TRIM(CONCAT(COALESCE(u.firstname,''), ' ', COALESCE(u.lastname,''))) AS name_like,

  '{"cash":0,"bank":0}' AS money,
  CONCAT('{',
    '"firstName":"', REPLACE(COALESCE(u.firstname,''),'"','\\"'), '",',
    '"lastName":"',  REPLACE(COALESCE(u.lastname,''),'"','\\"'),  '",',
    '"birthdate":"', REPLACE(COALESCE(u.dateofbirth,''),'"','\\"'),'",',
    '"gender":"',    REPLACE(COALESCE(u.sex,''),'"','\\"'),       '"',
  '}') AS charinfo,
  CONCAT('{',
    '"esx_identifier":"', REPLACE(u.identifier,'"','\\"'), '"',
  '}') AS metadata

FROM users u JOIN identifier_crosswalk x ON x.old_identifier = u.identifier LEFT JOIN players p ON p.citizenid = x.citizenid WHERE p.citizenid IS NULL;


Step 3 — Migrate Accounts → Money

There are two common ESX patterns:

A) ESX stores balances inside users.accounts JSON

-- Example: users.accounts = '{"bank":5000, "money":750, "black_money":200}'

-- 1) Extract from ESX JSON safely
-- Create a temp view/table with parsed numbers
CREATE TEMPORARY TABLE esx_balances AS

SELECT u.identifier, COALESCE(JSON_EXTRACT(u.accounts, '$.money'), 0) AS esx_cash, COALESCE(JSON_EXTRACT(u.accounts, '$.bank'), 0) AS esx_bank, COALESCE(JSON_EXTRACT(u.accounts, '$.black_money'), 0) AS esx_black FROM users u;

-- 2) Merge into QB/QBOX money JSON
-- Decide how to handle black_money (see Options below)
UPDATE players p

JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_OBJECT(

  'cash', CAST(b.esx_cash AS UNSIGNED),
  'bank', CAST(b.esx_bank AS UNSIGNED)
);

If MySQL without native JSON operations (or old

If MySQL without native JSON operations (or old version): build JSON strings using CONCAT.

B) ESX stores balances in user_accounts rows

-- Example: user_accounts(identifier, account, money)
CREATE TEMPORARY TABLE esx_balances AS
SELECT ua.identifier,
       SUM(CASE WHEN ua.account='money'        THEN ua.money ELSE 0 END) AS esx_cash,
       SUM(CASE WHEN ua.account='bank'         THEN ua.money ELSE 0 END) AS esx_bank,
       SUM(CASE WHEN ua.account='black_money'  THEN ua.money ELSE 0 END) AS esx_black

FROM user_accounts ua GROUP BY ua.identifier;

UPDATE players p

JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_OBJECT(

  'cash', CAST(b.esx_cash AS UNSIGNED),
  'bank', CAST(b.esx_bank AS UNSIGNED)
);

Handling black_money (choose one)

  • Option 1 (recommended): Create a dedicated wallet key in QB money JSON, e.g. "dirty".
  • Option 2: Convert to items (e.g., marked bills) and credit inventory instead (requires item migration; out of scope here).
  • Option 3: Zero it out (strongly discouraged unless you’ve announced a wipe).

Option 1 implementation:

-- Add dirty wallet in JSON (servers that support extra wallets)
UPDATE players p

JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_MERGE_PATCH(p.money, JSON_OBJECT('dirty', CAST(b.esx_black AS UNSIGNED)));

Ensure your framework/resources actually respect the extra wallet. Otherwise prefer Option 2.


Step 4 — Re‑key foreign tables that referenced ESX identifier

Typical tables to fix:

  • owned_vehicles.owner → map to citizenid (QB: player_vehicles.citizenid)
  • Any custom tables containing identifier columns (houses, billing, gangs, businesses)

Vehicles (ESX → QB)

-- If you keep ESX \`owned_vehicles\`, re‑key owner → citizenid for forward compatibility
ALTER TABLE owned_vehicles ADD COLUMN citizenid VARCHAR(20) NULL;

UPDATE owned_vehicles v

JOIN identifier_crosswalk x ON x.old_identifier = v.owner SET v.citizenid = x.citizenid WHERE v.citizenid IS NULL;

CREATE INDEX ix_ov_cid ON owned_vehicles (citizenid);

**Vehicles into QB’s **“ (minimal fields; adjust

**Vehicles into QB’s **“ (minimal fields; adjust to your schema):

INSERT IGNORE INTO player_vehicles (citizenid, plate, vehicle, state, garage)
SELECT x.citizenid, v.plate, v.vehicle, 0 AS state, 'A' AS garage

FROM owned_vehicles v JOIN identifier_crosswalk x ON x.old_identifier = v.owner;

Validate JSON field names (vehicle vs mods/props) and column list against your actual QB/QBOX schema.


Step 5 — Constraints, indexes, and integrity checks

-- Ensure primary/unique keys
ALTER TABLE players

ADD UNIQUE KEY ux_players_citizenid (citizenid);

-- Optional: keep a quick lookup by license/steam
ALTER TABLE players

ADD KEY ix_players_license (license);

-- Spot orphaned crosswalks (no players row)
SELECT x.*

FROM identifier_crosswalk x LEFT JOIN players p ON p.citizenid = x.citizenid WHERE p.citizenid IS NULL;

-- Spot players with zeroed wallets (sanity)
SELECT citizenid, money FROM players

WHERE JSON_EXTRACT(money, '$.cash') IS NULL OR JSON_EXTRACT(money, '$.bank') IS NULL;

-- Detect duplicates (same human with multiple identifiers)
SELECT old_identifier, COUNT(*)

FROM identifier_crosswalk GROUP BY old_identifier HAVING COUNT(*) > 1;


Step 6 — Validation suite

  1. Row counts: COUNT(users) ≈ COUNT(players) (within expected deltas).
  2. Balance totals: Sum of ESX cash/bank ≈ Sum of QB wallets after migration.
  3. Sample audit: Pick 10 players by name; verify citizenid, balances, vehicles.
  4. Login test: Bring server up in staging; log in a few known players; verify UIs.

Totals check examples:

-- ESX totals

SELECT SUM(COALESCE(JSON_EXTRACT(accounts,'$.money'),0)) AS esx_cash_total, SUM(COALESCE(JSON_EXTRACT(accounts,'$.bank'),0)) AS esx_bank_total FROM users;

-- QB totals

SELECT SUM(COALESCE(JSON_EXTRACT(money,'$.cash'),0)) AS qb_cash_total, SUM(COALESCE(JSON_EXTRACT(money,'$.bank'),0)) AS qb_bank_total FROM players;


Step 7 — Runtime compatibility (adapters)

Even after migration, some legacy scripts may still reference ESX identifier. Keep the crosswalk and use a helper to resolve “ (or inverse) at runtime.

Lua helper (server):

Lua helper (server):

--- lookup_citizenid.lua
local function getCitizenIdByIdentifier(identifier)
    local result = MySQL.query.await('SELECT citizenid FROM identifier_crosswalk WHERE old_identifier = ? LIMIT 1', { identifier })
    if result and result[1] then return result[1].citizenid end
    return nil
end

return { getCitizenIdByIdentifier = getCitizenIdByIdentifier }

Use this in legacy event handlers until all scripts are QB/QBOX‑native. See the adapter patterns article for full interface shims.

  • Adapter Patterns: https://fivemx.com/blog/adapter-patterns
  • Full conversion guide (Pillar): https://fivemx.com/blog/converting-fivem-scripts/

Rollback strategy

  1. Keep identifier_crosswalk and a pre‑migration backup.
  2. If something goes wrong, drop the new players rows created in this window and restore the backup.
  3. Re‑run migration after fixing data edge cases.

Simple label to mark your window:

-- Tag new rows
UPDATE players SET metadata = JSON_MERGE_PATCH(COALESCE(metadata,'{}'), JSON_OBJECT('migration_tag','esx_to_qb_2025_08_16'))

WHERE citizenid IN (SELECT citizenid FROM identifier_crosswalk);


Edge cases & tips

  • Multiple characters per human: If your ESX used one identifier per account (no multi‑char), but you plan multi‑char on QB, consider generating additional citizens later via in‑game flows, not here.
  • Name collisions: Two ESX users with same firstname/lastname are fine; citizenid is the key.
  • Missing “ values: Prefer whatever stable identifier you have (steam, license2, fivem). Populate players.license with the best available.
  • Old MySQL without JSON: Use plain text JSON strings and parse in app code; plan to upgrade.
  • Black money policy: Communicate your decision. If converting to items, run a separate, transparent item migration.

Cutover checklist (production)


FAQ

Q: Can I keep using ESX “ anywhere?
A: Yes, but treat it as legacy. Use the crosswalk to resolve when needed, and update scripts to citizenid ASAP.

Q: Does QBOX require different SQL?
A: Not for identifiers/money; QBOX tracks QB’s schema closely. Validate column names before running.

Q: What about inventories, jobs, gangs

Q: What about inventories, jobs, gangs?
A: Outside this article’s scope. Handle them after identifiers/money stabilize. Use the Pillar guide for full coverage.


Next steps

  • Implement runtime shims from Adapter Patterns: https://fivemx.com/blog/adapter-patterns
  • Finish your full migration using the Framework Guide (Pillar): https://fivemx.com/blog/converting-fivem-scripts/
  • Document your local deviations (custom wallets, extra columns) inside your repo.

Appendix — Idempotent wrappers

Wrap critical UPDATE/INSERTs with guards so you can re‑run safely.

-- Example guard: only update players with untouched money
UPDATE players p

JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_OBJECT('cash', CAST(b.esx_cash AS UNSIGNED), 'bank', CAST(b.esx_bank AS UNSIGNED)) WHERE JSON_EXTRACT(p.money, '$.cash') = 0 AND JSON_EXTRACT(p.money, '$.bank') = 0;

Keep the crosswalk forever. It’s your Rosetta Stone for old logs and scripts.

Frequently Asked Questions

When migrating from ESX to QBCore/QBOX, why is it necessary to convert identifiers?

ESX typically uses `identifier` (like `license:xxx` or `steam:xxx`) as the primary player key, while QBCore and QBOX utilize `citizenid`, a server-generated token. This fundamental difference requires a migration process to ensure player data, like balances and vehicle ownership, is correctly associated with the new identifier system in the target framework. Failing to migrate identifiers will result in loss of progress and broken associations within the game.

What are the critical 'golden rules' to follow when performing the SQL migration?

Before initiating the SQL migration, it's essential to freeze all write operations to the database by stopping the game server and any external bots. Next, perform a complete database backup accompanied by a dump of table structures, storing both with timestamps for easy restoration. Where possible, execute operations within transactions per table to maintain data integrity, and make sure steps are idempotent (repeatable without adverse effects). Lastly, validate the data at each step. These precautions are important to minimize data loss and ensure a smooth transition.

Besides the player identifier, what other key data structures require migration when switching from ESX to QBCore/QBOX, and how do their structures differ?

Aside from player identifiers, the money model and vehicle ownership records require migration. ESX commonly uses separate accounts (cash/bank/black_money) stored as JSON or in separate `user_accounts` rows. QBCore/QBOX commonly store a single `money` JSON on the `players` table (e.g., `{ "cash": 0, "bank": 5000 }`). For vehicles, ESX's `owned_vehicles.owner` refers to the ESX `identifier`, while QBCore/QBOX's `player_vehicles.citizenid` column stores the new citizen ID.

How can I validate that my SQL migration from ESX to QBCore/QBOX was successful?

Validation is a critical step. After running the migration scripts, cross-reference data between the old and new database structures. Verify that player balances, vehicle ownership, and other key data points have been accurately transferred and associated with the correct `citizenid`. You can use SQL queries to compare data across tables. Have a test account with money and vehicles to ensure they're carried over.

How does SQL & Identifiers Migration: steam/license to citizenid work?

Use‑case: You’re moving from ESX to QBCore or QBOX (qbx core) and need a clean, auditable migration of player identifiers and balances. This guide gives you production‑ready SQL, a reversible plan, and validation steps.

Previous Article

Adapter Patterns: ESX, QBCore & QBOX (Exports, Events & APIs)

Next Article

From mysql-async to oxmysql: Safe Migration & Query Patterns

More on This Topic

From mysql-async to oxmysql: Safe Migration & Query PatternsQBox Framework Guide: Migrate from QBCore and Boost Performance (2026)

Move from research to a production-ready server stack

Once you know the direction, jump into the highest-leverage commercial hubs for verified scripts, curated bundles, and framework-specific buying paths.

Framework hub

Browse QBCore-ready scripts

Move into the QBCore landing page to compare verified scripts, framework fit, and install-ready products built for modern FiveM servers.

Open QBCore hub

Premium catalog

Browse premium FiveM scripts

Move from research into the main shop to compare real products, framework labels, screenshots, and production-ready quality signals.

Open premium shop

Launch faster

Compare curated bundles

Bundles shorten the path from planning to launch by grouping the highest-leverage scripts into a cleaner commercial starting point.

View bundles

Disclosure: Some links below are affiliate links to FiveMX products. We may earn a commission at no extra cost to you.

Free Scripts You Might Like

Project X Prompt Sandy Bank Robbery Heist - QB | QBOX | ESX | Custom

Project X Prompt Sandy Bank Robbery Heist - QB | QBOX | ESX | Custom

294 downloads
Realistic Grapple Gun - Nodus Scripts

Realistic Grapple Gun - Nodus Scripts

202 downloads
OP Gangs 3.0 — Most Advanced Gang Script [ESX/QB/QBOX]

OP Gangs 3.0 — Most Advanced Gang Script [ESX/QB/QBOX]

161 downloads
[FREE][QBOX][QBCORE] Pawnshop Script + FREE MLO

[FREE][QBOX][QBCORE] Pawnshop Script + FREE MLO

157 downloads

Related Articles

Build a Custom Phone App (NUI + React) for QBCore/ESX

Build a Custom Phone App (NUI + React) for QBCore/ESX

Create a production‑ready in‑game smartphone for FiveM using NUI + React. You will scaffold a resource, wire QBCore/ESX events, persist data in MySQL, and ship a smooth UI that…

October 4, 2025
How To Create an alt:V Server (2026 Quickstart Guide)

How To Create an alt:V Server (2026 Quickstart Guide)

Want to host your own GTA V multiplayer world with alt:V? This guide shows you two reliable setup paths (Windows & Linux), gives you a clean server.toml, a first working…

September 22, 2025
How to Set Up a Discord Whitelist for Your FiveM Server (2026 Guide)

How to Set Up a Discord Whitelist for Your FiveM Server (2026 Guide)

Gate access to your FiveM server with a Discord role-based whitelist. Covers three implementation approaches (pure Discord, identifier-based, hybrid), step-by-step bot setup, working QBCore and ESX examples, common mistakes that break auth, and security hardening.

August 17, 2025
Secure CheckoutInstant AccessMoney-Back GuaranteeLifetime Updates
FiveMX

Premium FiveM scripts and mods for serious server owners.

Shop

  • Shop
  • QBCore Scripts
  • ESX Scripts
  • FiveM Scripts
  • Free Mods
  • Best Scripts & Mods

Help

  • About
  • FAQ
  • Support
  • Contact
  • Account
  • Affiliate Program

Legal

  • Privacy Policy
  • Terms of Service
  • Refund Policy
  • Cookie Policy
  • GDPR Compliance
  • DMCA
  • Imprint
  • Editorial Policy
© 2026 FiveMX. All rights reserved.·support@fivemx.com

FiveMX is not affiliated with Rockstar Games, Take-Two Interactive, or CFX.re. All trademarks are property of their respective owners.

Flash Sale — Up to 19% off!Flash Sale — 19% off!Shop Now