Ahorra hoy mismo en 20%. Usa el código WELCOME al finalizar la compra. WELCOME

How to Migrate ESX → QBCore the Right Way

You want a clean switch from ESX to QBCore without losing data or breaking core systems. Follow this plan. You will finish with stable identifiers, oxmysql queries, and ox_lib powered code.

Goal: move your server from ESX to QBCore with minimal downtime.



Prerrequisitos

  1. Herramientas
    1. GIT and a separate branch for the migration.
    2. MariaDB or MySQL 8 with full backups enabled.
    3. A staging server that mirrors production.
  2. Server artifacts
    1. Servidor FX updated to the same build as production.
    2. QBCore base framework and default resources.
  3. Libraries you will use
    1. oxmysql for database.
    2. biblioteca de buey for callbacks, UI helpers, and utility wrappers.

Step 1. Make a Plan and a Rollback Point

  1. Freeze production changes. Stop new script installs and DB writes not required for testing.
  2. Back up your entire database dump as a named snapshot.
  3. Branch your server repository and create a dedicated migrate-esx-to-qbcore branch.
  4. Write a runbook. Include commands to start and stop the staging server, restore DB, and run health checks.

Step 2. Build a Clean QBCore Base

  1. Deploy a fresh QBCore base to staging.
  2. Keep only essentials enabled. Disable jobs, inventories, and custom scripts until after DB migration.
  3. Install and start these resources first
    1. núcleo qb
    2. qb-vehicles or your preferred replacements
    3. oxmysql
    4. biblioteca de buey

Step 3. Replace mysql-async with oxmysql

If any remaining ESX scripts still use MySQL.Async, convert the calls to oxmysql. Use simple find and replace with verification.

Common conversions

-- ESX mysql-async
MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @id', {['@id'] = identifier}, function(rows)
  -- ...
end)
-- QBCore oxmysql
local rows = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', { citizenid })
-- rows is a Lua table; handle nil and length checks directly
-- ESX scalar example
MySQL.Async.fetchScalar('SELECT COUNT(1) FROM owned_vehicles', {}, function(count)
  -- ...
end)
-- oxmysql scalar
local count = MySQL.scalar.await('SELECT COUNT(1) FROM player_vehicles')
-- ESX insert
MySQL.Async.execute('INSERT INTO addon_account VALUES (@owner, @name, @money)', {
  ['@owner'] = identifier, ['@name'] = name, ['@money'] = money
})
-- oxmysql insert
MySQL.prepare.await('INSERT INTO player_accounts (citizenid, name, amount) VALUES (?, ?, ?)', { citizenid, name, amount })

Notas

  1. Preferir query.await, scalar.await, y prepare.await for clean flow.
  2. Use prepared statements for write operations.

Step 4. Map ESX Data Structures to QBCore

You will move player identities and owned entities. Use this reference to map tables.

ESX tableKey columnQBCore tableKey columnNotas
usuariosidentificadorjugadoresID de ciudadanoConvert identifiers and create ID de ciudadano for each row
owned_vehiclesownerplayer_vehiclesID de ciudadanoConvert plate casing and JSON payloads
datos del almacén de datosownerplayer_metadataID de ciudadanoIf you store JSON, merge carefully
addon_account_dataownerplayer_accountsID de ciudadanoMap account names to QBCore banking or cash
addon_inventory_itemsownerplayer_inventoriesID de ciudadanoIf you move to inventario_de_bueyes, migrate separately

You can keep custom tables. Adjust only foreign keys that reference ESX identifiers.


Step 5. Stabilize Identifiers

ESX often stores a CFX identifier like license:xxxx or historical steam:xxxx. QBCore uses ID de ciudadano as the stable player key and keeps the runtime identifiers for authentication only.

You will

  1. Crear una ID de ciudadano for every player.
  2. Link legacy identifiers to the new record.
  3. Keep a lookup table for support and audits.

SQL bootstrap

Run this on a copy of your ESX database to prepare QBCore tables.

-- 1) Create players table if missing. Adjust to your QBCore schema.
CREATE TABLE IF NOT EXISTS players (
  citizenid VARCHAR(11) PRIMARY KEY,
  license VARCHAR(64) UNIQUE,
  identifiers JSON NOT NULL,
  name VARCHAR(64),
  charinfo JSON NOT NULL,
  metadata JSON NOT NULL,
  money JSON NOT NULL,
  job JSON NOT NULL,
  position VARCHAR(128) DEFAULT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2) Helper function equivalent in SQL using a deterministic generator would be complex.
-- Instead, stage the mapping in a separate table and generate citizenid in Lua.
CREATE TABLE IF NOT EXISTS legacy_identifier_map (
  license VARCHAR(64) PRIMARY KEY,
  steam VARCHAR(64) NULL,
  fivem VARCHAR(64) NULL,
  discord VARCHAR(64) NULL,
  xbl VARCHAR(64) NULL,
  liveid VARCHAR(64) NULL,
  citizenid VARCHAR(11) UNIQUE
);

-- 3) Seed the mapping from ESX users
INSERT INTO legacy_identifier_map (license)
SELECT DISTINCT REPLACE(identifier, 'identifier:', '')
FROM users
WHERE identifier LIKE 'license:%' OR identifier LIKE 'steam:%';

Generate citizenid and insert players in Lua

Run once on staging. Back up first.

-- server/migrate_identifiers.lua
local QBCore = exports['qb-core']:GetCoreObject()

local function generateCitizenId()
  local charset = {}
  for c = 65, 90 do table.insert(charset, string.char(c)) end
  for n = 48, 57 do table.insert(charset, string.char(n)) end
  math.randomseed(GetGameTimer())
  local id = {}
  for i = 1, 11 do id[i] = charset[math.random(1, #charset)] end
  return table.concat(id)
end

local rows = MySQL.query.await('SELECT license FROM legacy_identifier_map WHERE citizenid IS NULL')
for _, r in ipairs(rows) do
  local citizenid = generateCitizenId()
  MySQL.prepare.await('UPDATE legacy_identifier_map SET citizenid = ? WHERE license = ?', { citizenid, r.license })
end

-- Build players from ESX users
local users = MySQL.query.await([[SELECT u.identifier, u.firstname, u.lastname, u.dateofbirth, u.sex, u.height
                                  FROM users u]])
for _, u in ipairs(users) do
  local license = u.identifier
  local map = MySQL.single.await('SELECT citizenid FROM legacy_identifier_map WHERE license = ?', { license })
  if map and map.citizenid then
    local name = string.format('%s %s', u.firstname or 'John', u.lastname or 'Doe')
    local charinfo = json.encode({ firstname = u.firstname, lastname = u.lastname, birthdate = u.dateofbirth, gender = u.sex, height = u.height })
    local metadata = json.encode({ hunger = 100, thirst = 100 })
    local money = json.encode({ cash = 0, bank = 0, crypto = 0 })
    local job = json.encode({ name = 'unemployed', label = 'Unemployed', grade = { name = '0', level = 0 }})

    MySQL.prepare.await('INSERT IGNORE INTO players (citizenid, license, identifiers, name, charinfo, metadata, money, job) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
      map.citizenid,
      license,
      json.encode({ license = license }),
      name,
      charinfo,
      metadata,
      money,
      job
    })
  end
end
print('Identifier migration finished')

Move owned vehicles

INSERT IGNORE INTO player_vehicles (citizenid, plate, vehicle, garage, state)
SELECT m.citizenid,
       UPPER(JSON_UNQUOTE(JSON_EXTRACT(v.vehicle, '$.plate'))),
       v.vehicle,
       'legion',
       1
FROM owned_vehicles v
JOIN legacy_identifier_map m ON m.license = v.owner;

Validate random samples in game. Verify plate formats and garages.


Step 6. Port ESX Code to QBCore with ox_lib

Replace the ESX runtime API with QBCore equivalents. Use ox_lib for callbacks and notifications.

Player object

-- ESX
local xPlayer = ESX.GetPlayerFromId(src)
xPlayer.addMoney(100)
-- QBCore
local Player = QBCore.Functions.GetPlayer(src)
Player.Functions.AddMoney('cash', 100)

Empleos

-- ESX job check
if xPlayer.getJob().name == 'police' then
  -- ...
end
-- QBCore job check
local job = Player.PlayerData.job
if job and job.name == 'police' then
  -- ...
end

Callbacks and UI

-- ESX server callback
ESX.RegisterServerCallback('resource:getData', function(source, cb)
  cb({ ok = true })
end)
-- ox_lib callback
lib.callback.register('resource:getData', function(source)
  return { ok = true }
end)
-- Notification
lib.notify(source, { title = 'Job', description = 'Promotion granted', type = 'success' })

Comandos

-- ESX
RegisterCommand('pay', function(src, args)
  local amount = tonumber(args[1]) or 0
  xPlayer.removeMoney(amount)
end)
-- QBCore with permissions
QBCore.Commands.Add('pay', 'Pay cash', {{name = 'amount', help = 'Amount'}}, false, function(src, args)
  local amount = tonumber(args[1]) or 0
  local Player = QBCore.Functions.GetPlayer(src)
  Player.Functions.RemoveMoney('cash', amount)
end)

Step 7. Inventory and Items

If you move from es_extendido inventories to inventario qb o inventario_de_bueyes, treat this as a separate sub‑migration.

  1. Freeze item additions.
  2. Export the item master list.
  3. Map item names one to one.
  4. Migrate player inventories in batches. Validate stack sizes and weights.

Example item mapping CSV

esx_name,qb_name,notes
bread,bread,
water,water,
lockpick,lockpick,

Step 8. Test and Roll Out

  1. Unit tests
    1. Test identifier lookups for a random set of players.
    2. Test money transfers, job changes, and vehicle ownership.
  2. Gameplay tests
    1. Spawn players with old ESX identifiers and confirm auto mapping.
    2. Run a police duty flow, a store robbery, and a vehicle purchase.
  3. Performance tests
    1. Usar resmón to watch CPU and memory.
    2. Confirm DB query counts dropped after oxmysql conversion.
  4. Rollout plan
    1. Move staging DB to production during a maintenance window.
    2. Announce a 60 minute downtime.
    3. Monitor logs for missing identifiers and foreign key errors.

Solución de problemas

  1. Duplicated citizens
    1. Cause. Running the migration twice.
    2. Fix. Enforce unique keys on ID de ciudadano y uso INSERT IGNORE during seeding.
  2. Missing vehicles
    1. Cause. Owner key mismatch between vehículos_propiedad.propietario y legacy_identifier_map.license.
    2. Fix. Normalize owner values and re‑run the vehicle insert for the affected plates.
  3. Players spawn without inventory
    1. Cause. Inventory migration skipped.
    2. Fix. Rebuild the inventory mapping and re‑import.
  4. Scripts fail with MySQL.Async not found
    1. Cause. Script still depends on mysql-async.
    2. Fix. Replace calls with oxmysql and remove mysql-async from the server.

Cutover Checklist

  1. Back up production database with a timestamp.
  2. Stop the server and lock player joins.
  3. Restore the final staging dump to production.
  4. Deploy QBCore build with núcleo qb, oxmysql, biblioteca de buey first in the ensure order.
  5. Run the identifier seeding script once.
  6. Enable converted scripts only when their queries are on oxmysql.
  7. Reopen the server and watch logs for 30 minutes.
  8. Post a rollback plan if critical errors appear.

Appendix A. Example fxmanifest for migration helper

fx_version 'cerulean'
game 'gta5'

lua54 'yes'

server_scripts {
  '@oxmysql/lib/MySQL.lua',
  '@ox_lib/init.lua',
  'server/migrate_identifiers.lua'
}

Appendix B. Safe JSON helpers

local function safeDecode(jsonStr, fallback)
  if type(jsonStr) ~= 'string' or jsonStr == '' then return fallback end
  local ok, result = pcall(json.decode, jsonStr)
  if not ok then return fallback end
  return result
end

What you achieved

  1. Stable player records keyed by ID de ciudadano.
  2. A clean oxmysql layer with prepared statements and awaits.
  3. ESX code ported to QBCore using ox_lib callbacks and utilities.
  4. A versioned plan you can repeat for future servers.

  1. Framework conversion hub. https://fivemx.com/framework-conversion
  2. MySQL Async to oxmysql guide. https://fivemx.com/mysql-async-to-oxmysql
  3. SQL identifiers migration. https://fivemx.com/sql-identifiers-migration
  4. Adapter patterns for script ports. https://fivemx.com/adapter-patterns
  5. QBCore install quickstart. https://fivemx.com/how-to-install-qbcore
  6. Script conversion checklist. https://fivemx.com/converting-fivem-scripts
  7. QBOX with ox stack overview. https://fivemx.com/qbox-ox-stack
  8. Resmon and performance. https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources

External references

  1. Framework QBCore
  2. oxmysql documentation.
  3. ox_lib documentation.
  4. CFX.re identifiers reference.

Lucas
Lucas

Soy Luke, gamer y me encanta escribir sobre FiveM, GTA y juegos de rol. Dirijo una comunidad de juegos de rol y tengo unos 10 años de experiencia administrando servidores.

Artículos: 436