{"id":199542,"date":"2025-10-02T15:33:52","date_gmt":"2025-10-02T13:33:52","guid":{"rendered":"https:\/\/fivemx.com\/?p=199542"},"modified":"2026-08-10T07:53:36","modified_gmt":"2026-08-10T05:53:36","slug":"how-to-migrate-esx-qbcore","status":"publish","type":"post","link":"https:\/\/fivemx.com\/es\/how-to-migrate-esx-qbcore\/","title":{"rendered":"C\u00f3mo Migrar ESX \u2192 QBCore de la Manera Correcta"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal: move your server from ESX to QBCore with minimal downtime.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"prerequisites\" class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Tools\n<ol class=\"wp-block-list\">\n<li>GIT and a separate branch for the migration.<\/li>\n\n\n\n<li>MariaDB or MySQL 8 with full backups enabled.<\/li>\n\n\n\n<li>A staging server that mirrors production.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Server artifacts\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/fivemx.com\/troubleshooting-fxserver-is-not-responding-how-to-fix\/\" data-type=\"post\" data-id=\"189713\">FXServer<\/a> updated to the same build as production.<\/li>\n\n\n\n<li><a href=\"https:\/\/fivemx.com\/qbcore-scripts\/\" data-type=\"product_cat\" data-id=\"512\">QBCore<\/a> base framework and default resources.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Libraries you will use\n<ol class=\"wp-block-list\">\n<li><code data-no-translation=\"\">oxmysql<\/code> for database.<\/li>\n\n\n\n<li><code data-no-translation=\"\">ox_lib<\/code> for callbacks, UI helpers, and utility wrappers.<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-1-make-a-plan-and-a-rollback-point\" class=\"wp-block-heading\">Step 1. Make a Plan and a Rollback Point<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Freeze production changes. Stop new script installs and DB writes not required for testing.<\/li>\n\n\n\n<li>Back up your entire database dump as a named snapshot.<\/li>\n\n\n\n<li>Branch your server repository and create a dedicated <code data-no-translation=\"\">migrate-esx-to-qbcore<\/code> branch.<\/li>\n\n\n\n<li>Write a runbook. Include commands to start and stop the staging server, restore DB, and run health checks.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-2-build-a-clean-qb-core-base\" class=\"wp-block-heading\">Step 2. Build a Clean QBCore Base<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Deploy a fresh QBCore base to staging.<\/li>\n\n\n\n<li>Keep only essentials enabled. Disable jobs, inventories, and custom scripts until after DB migration.<\/li>\n\n\n\n<li>Install and start these resources first\n<ol class=\"wp-block-list\">\n<li><code data-no-translation=\"\">qb-core<\/code><\/li>\n\n\n\n<li><code data-no-translation=\"\">qb-vehicles<\/code> or your preferred replacements<\/li>\n\n\n\n<li><code data-no-translation=\"\">oxmysql<\/code><\/li>\n\n\n\n<li><code data-no-translation=\"\">ox_lib<\/code><\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-3-replace-mysql-async-with-oxmysql\" class=\"wp-block-heading\">Step 3. Replace mysql-async with oxmysql<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If any remaining ESX scripts still use <code data-no-translation=\"\">MySQL.Async<\/code>, convert the calls to oxmysql. Use simple find and replace with verification.<\/p>\n\n\n\n<h3 id=\"common-conversions\" class=\"wp-block-heading\">Common conversions<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX mysql-async\nMySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @id', {['@id'] = identifier}, function(rows)\n -- ...\nend)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- QBCore oxmysql\nlocal rows = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', { citizenid })\n-- rows is a Lua table; handle nil and length checks directly\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX scalar example\nMySQL.Async.fetchScalar('SELECT COUNT(1) FROM owned_vehicles', {}, function(count)\n -- ...\nend)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- oxmysql scalar\nlocal count = MySQL.scalar.await('SELECT COUNT(1) FROM player_vehicles')\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX insert\nMySQL.Async.execute('INSERT INTO addon_account VALUES (@owner, @name, @money)', {\n ['@owner'] = identifier, ['@name'] = name, ['@money'] = money\n})\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- oxmysql insert\nMySQL.prepare.await('INSERT INTO player_accounts (citizenid, name, amount) VALUES (?, ?, ?)', { citizenid, name, amount })\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notes<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Prefer <code data-no-translation=\"\">query.await<\/code>, <code data-no-translation=\"\">scalar.await<\/code>, and <code data-no-translation=\"\">prepare.await<\/code> for clean flow.<\/li>\n\n\n\n<li>Use prepared statements for write operations.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-4-map-esx-data-structures-to-qb-core\" class=\"wp-block-heading\">Step 4. Map ESX Data Structures to QBCore<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You will move player identities and owned entities. Use this reference to map tables.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>ESX table<\/th><th>Key column<\/th><th>QBCore table<\/th><th>Key column<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code data-no-translation=\"\">users<\/code><\/td><td><code data-no-translation=\"\">identifier<\/code><\/td><td><code data-no-translation=\"\">players<\/code><\/td><td><code data-no-translation=\"\">citizenid<\/code><\/td><td>Convert identifiers and create <code data-no-translation=\"\">citizenid<\/code> for each row<\/td><\/tr><tr><td><code data-no-translation=\"\">owned_vehicles<\/code><\/td><td><code data-no-translation=\"\">owner<\/code><\/td><td><code data-no-translation=\"\">player_vehicles<\/code><\/td><td><code data-no-translation=\"\">citizenid<\/code><\/td><td>Convert plate casing and JSON payloads<\/td><\/tr><tr><td><code data-no-translation=\"\">datastore_data<\/code><\/td><td><code data-no-translation=\"\">owner<\/code><\/td><td><code data-no-translation=\"\">player_metadata<\/code><\/td><td><code data-no-translation=\"\">citizenid<\/code><\/td><td>If you store JSON, merge carefully<\/td><\/tr><tr><td><code data-no-translation=\"\">addon_account_data<\/code><\/td><td><code data-no-translation=\"\">owner<\/code><\/td><td><code data-no-translation=\"\">player_accounts<\/code><\/td><td><code data-no-translation=\"\">citizenid<\/code><\/td><td>Map account names to QBCore banking or cash<\/td><\/tr><tr><td><code data-no-translation=\"\">addon_inventory_items<\/code><\/td><td><code data-no-translation=\"\">owner<\/code><\/td><td><code data-no-translation=\"\">player_inventories<\/code><\/td><td><code data-no-translation=\"\">citizenid<\/code><\/td><td>If you move to <code data-no-translation=\"\">ox_inventory<\/code>, migrate separately<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You can keep custom tables. Adjust only foreign keys that reference ESX identifiers.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-5-stabilize-identifiers\" class=\"wp-block-heading\">Step 5. Stabilize Identifiers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ESX often stores a CFX identifier like <code data-no-translation=\"\">license:xxxx<\/code> or historical <code data-no-translation=\"\">steam:xxxx<\/code>. QBCore uses <code data-no-translation=\"\">citizenid<\/code> as the stable player key and keeps the runtime identifiers for authentication only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You will<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a <code data-no-translation=\"\">citizenid<\/code> for every player.<\/li>\n\n\n\n<li>Link legacy identifiers to the new record.<\/li>\n\n\n\n<li>Keep a lookup table for support and audits.<\/li>\n<\/ol>\n\n\n\n<h3 id=\"sql-bootstrap\" class=\"wp-block-heading\">SQL bootstrap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run this on a copy of your ESX database to prepare QBCore tables.<\/p>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- 1) Create players table if missing. Adjust to your QBCore schema.\nCREATE TABLE IF NOT EXISTS players (\n citizenid VARCHAR(11) PRIMARY KEY,\n license VARCHAR(64) UNIQUE,\n identifiers JSON NOT NULL,\n name VARCHAR(64),\n charinfo JSON NOT NULL,\n metadata JSON NOT NULL,\n money JSON NOT NULL,\n job JSON NOT NULL,\n position VARCHAR(128) DEFAULT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- 2) Helper function equivalent in SQL using a deterministic generator would be complex.\n-- Instead, stage the mapping in a separate table and generate citizenid in Lua.\nCREATE TABLE IF NOT EXISTS legacy_identifier_map (\n license VARCHAR(64) PRIMARY KEY,\n steam VARCHAR(64) NULL,\n fivem VARCHAR(64) NULL,\n discord VARCHAR(64) NULL,\n xbl VARCHAR(64) NULL,\n liveid VARCHAR(64) NULL,\n citizenid VARCHAR(11) UNIQUE\n);\n\n-- 3) Seed the mapping from ESX users\nINSERT INTO legacy_identifier_map (license)\nSELECT DISTINCT REPLACE(identifier, 'identifier:', '')\nFROM users\nWHERE identifier LIKE 'license:%' OR identifier LIKE 'steam:%';\n<\/pre>\n\n\n\n<h3 id=\"generate-citizenid-and-insert-players-in-lua\" class=\"wp-block-heading\">Generate citizenid and insert players in Lua<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run once on staging. Back up first.<\/p>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- server\/migrate_identifiers.lua\nlocal QBCore = exports['qb-core']:GetCoreObject()\n\nlocal function generateCitizenId()\n local charset = {}\n for c = 65, 90 do table.insert(charset, string.char(c)) end\n for n = 48, 57 do table.insert(charset, string.char(n)) end\n math.randomseed(GetGameTimer())\n local id = {}\n for i = 1, 11 do id[i] = charset[math.random(1, #charset)] end\n return table.concat(id)\nend\n\nlocal rows = MySQL.query.await('SELECT license FROM legacy_identifier_map WHERE citizenid IS NULL')\nfor _, r in ipairs(rows) do\n local citizenid = generateCitizenId()\n MySQL.prepare.await('UPDATE legacy_identifier_map SET citizenid = ? WHERE license = ?', { citizenid, r.license })\nend\n\n-- Build players from ESX users\nlocal users = MySQL.query.await([[SELECT u.identifier, u.firstname, u.lastname, u.dateofbirth, u.sex, u.height\n FROM users u]])\nfor _, u in ipairs(users) do\n local license = u.identifier\n local map = MySQL.single.await('SELECT citizenid FROM legacy_identifier_map WHERE license = ?', { license })\n if map and map.citizenid then\n local name = string.format('%s %s', u.firstname or 'John', u.lastname or 'Doe')\n local charinfo = json.encode({ firstname = u.firstname, lastname = u.lastname, birthdate = u.dateofbirth, gender = u.sex, height = u.height })\n local metadata = json.encode({ hunger = 100, thirst = 100 })\n local money = json.encode({ cash = 0, bank = 0, crypto = 0 })\n local job = json.encode({ name = 'unemployed', label = 'Unemployed', grade = { name = '0', level = 0 }})\n\n MySQL.prepare.await('INSERT IGNORE INTO players (citizenid, license, identifiers, name, charinfo, metadata, money, job) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {\n map.citizenid,\n license,\n json.encode({ license = license }),\n name,\n charinfo,\n metadata,\n money,\n job\n })\n end\nend\nprint('Identifier migration finished')\n<\/pre>\n\n\n\n<h3 id=\"move-owned-vehicles\" class=\"wp-block-heading\">Move owned vehicles<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">INSERT IGNORE INTO player_vehicles (citizenid, plate, vehicle, garage, state)\nSELECT m.citizenid,\n UPPER(JSON_UNQUOTE(JSON_EXTRACT(v.vehicle, '$.plate'))),\n v.vehicle,\n 'legion',\n 1\nFROM owned_vehicles v\nJOIN legacy_identifier_map m ON m.license = v.owner;\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Validate random samples in game. Verify plate formats and garages.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-6-port-esx-code-to-qb-core-with-ox-lib\" class=\"wp-block-heading\">Step 6. Port ESX Code to QBCore with ox_lib<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Replace the ESX runtime API with QBCore equivalents. Use ox_lib for callbacks and notifications.<\/p>\n\n\n\n<h3 id=\"player-object\" class=\"wp-block-heading\">Player object<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX\nlocal xPlayer = ESX.GetPlayerFromId(src)\nxPlayer.addMoney(100)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- QBCore\nlocal Player = QBCore.Functions.GetPlayer(src)\nPlayer.Functions.AddMoney('cash', 100)\n<\/pre>\n\n\n\n<h3 id=\"jobs\" class=\"wp-block-heading\">Jobs<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX job check\nif xPlayer.getJob().name == 'police' then\n -- ...\nend\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- QBCore job check\nlocal job = Player.PlayerData.job\nif job and job.name == 'police' then\n -- ...\nend\n<\/pre>\n\n\n\n<h3 id=\"callbacks-and-ui\" class=\"wp-block-heading\">Callbacks and UI<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX server callback\nESX.RegisterServerCallback('resource:getData', function(source, cb)\n cb({ ok = true })\nend)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ox_lib callback\nlib.callback.register('resource:getData', function(source)\n return { ok = true }\nend)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- Notification\nlib.notify(source, { title = 'Job', description = 'Promotion granted', type = 'success' })\n<\/pre>\n\n\n\n<h3 id=\"commands\" class=\"wp-block-heading\">Commands<\/h3>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- ESX\nRegisterCommand('pay', function(src, args)\n local amount = tonumber(args[1]) or 0\n xPlayer.removeMoney(amount)\nend)\n<\/pre>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- QBCore with permissions\nQBCore.Commands.Add('pay', 'Pay cash', {{name = 'amount', help = 'Amount'}}, false, function(src, args)\n local amount = tonumber(args[1]) or 0\n local Player = QBCore.Functions.GetPlayer(src)\n Player.Functions.RemoveMoney('cash', amount)\nend)\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-7-inventory-and-items\" class=\"wp-block-heading\">Step 7. Inventory and Items<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you move from <code data-no-translation=\"\">es_extended<\/code> inventories to <code data-no-translation=\"\">qb-inventory<\/code> or <code data-no-translation=\"\">ox_inventory<\/code>, treat this as a separate sub\u2011migration.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Freeze item additions.<\/li>\n\n\n\n<li>Export the item master list.<\/li>\n\n\n\n<li>Map item names one to one.<\/li>\n\n\n\n<li>Migrate player inventories in batches. Validate stack sizes and weights.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Example item mapping CSV<\/p>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">esx_name,qb_name,notes\nbread,bread,\nwater,water,\nlockpick,lockpick,\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"step-8-test-and-roll-out\" class=\"wp-block-heading\">Step 8. Test and Roll Out<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Unit tests\n<ol class=\"wp-block-list\">\n<li>Test identifier lookups for a random set of players.<\/li>\n\n\n\n<li>Test money transfers, job changes, and vehicle ownership.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Gameplay tests\n<ol class=\"wp-block-list\">\n<li>Spawn players with old ESX identifiers and confirm auto mapping.<\/li>\n\n\n\n<li>Run a police duty flow, a store robbery, and a vehicle purchase.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Performance tests\n<ol class=\"wp-block-list\">\n<li>Use <code data-no-translation=\"\">resmon<\/code> to watch CPU and memory.<\/li>\n\n\n\n<li>Confirm DB query counts dropped after oxmysql conversion.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Rollout plan\n<ol class=\"wp-block-list\">\n<li>Move staging DB to production during a maintenance window.<\/li>\n\n\n\n<li>Announce a 60 minute downtime.<\/li>\n\n\n\n<li>Monitor logs for missing identifiers and foreign key errors.<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"troubleshooting\" class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Duplicated citizens\n<ol class=\"wp-block-list\">\n<li>Cause. Running the migration twice.<\/li>\n\n\n\n<li>Fix. Enforce unique keys on <code data-no-translation=\"\">citizenid<\/code> and use <code data-no-translation=\"\">INSERT IGNORE<\/code> during seeding.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Missing vehicles\n<ol class=\"wp-block-list\">\n<li>Cause. Owner key mismatch between <code data-no-translation=\"\">owned_vehicles.owner<\/code> and <code data-no-translation=\"\">legacy_identifier_map.license<\/code>.<\/li>\n\n\n\n<li>Fix. Normalize owner values and re\u2011run the vehicle insert for the affected plates.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Players spawn without inventory\n<ol class=\"wp-block-list\">\n<li>Cause. Inventory migration skipped.<\/li>\n\n\n\n<li>Fix. Rebuild the inventory mapping and re\u2011import.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Scripts fail with <code data-no-translation=\"\">MySQL.Async<\/code> not found\n<ol class=\"wp-block-list\">\n<li>Cause. Script still depends on mysql-async.<\/li>\n\n\n\n<li>Fix. Replace calls with oxmysql and remove mysql-async from the server.<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"cutover-checklist\" class=\"wp-block-heading\">Cutover Checklist<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Back up production database with a timestamp.<\/li>\n\n\n\n<li>Stop the server and lock player joins.<\/li>\n\n\n\n<li>Restore the final staging dump to production.<\/li>\n\n\n\n<li>Deploy QBCore build with <code data-no-translation=\"\">qb-core<\/code>, <code data-no-translation=\"\">oxmysql<\/code>, <code data-no-translation=\"\">ox_lib<\/code> first in the ensure order.<\/li>\n\n\n\n<li>Run the identifier seeding script once.<\/li>\n\n\n\n<li>Enable converted scripts only when their queries are on oxmysql.<\/li>\n\n\n\n<li>Reopen the server and watch logs for 30 minutes.<\/li>\n\n\n\n<li>Post a rollback plan if critical errors appear.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"appendix-a-example-fxmanifest-for-migration-helper\" class=\"wp-block-heading\">Appendix A. Example fxmanifest for migration helper<\/h2>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">fx_version 'cerulean'\ngame 'gta5'\n\nlua54 'yes'\n\nserver_scripts {\n '@oxmysql\/lib\/MySQL.lua',\n '@ox_lib\/init.lua',\n 'server\/migrate_identifiers.lua'\n}\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"appendix-b-safe-json-helpers\" class=\"wp-block-heading\">Appendix B. Safe JSON helpers<\/h2>\n\n\n\n<pre data-no-translation=\"\" class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">local function safeDecode(jsonStr, fallback)\n if type(jsonStr) ~= 'string' or jsonStr == '' then return fallback end\n local ok, result = pcall(json.decode, jsonStr)\n if not ok then return fallback end\n return result\nend\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"what-you-achieved\" class=\"wp-block-heading\">What you achieved<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Stable player records keyed by <code data-no-translation=\"\">citizenid<\/code>.<\/li>\n\n\n\n<li>A clean oxmysql layer with prepared statements and awaits.<\/li>\n\n\n\n<li>ESX code ported to QBCore using ox_lib callbacks and utilities.<\/li>\n\n\n\n<li>A versioned plan you can repeat for future servers.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 id=\"useful-links-inside-your-site\" class=\"wp-block-heading\">Useful links inside your site<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Framework conversion hub. <a href=\"https:\/\/fivemx.com\/framework-conversion\/\">https:\/\/fivemx.com\/framework-conversion\/<\/a><\/li>\n\n\n\n<li>MySQL Async to oxmysql guide. <a href=\"https:\/\/fivemx.com\/mysql-async-to-oxmysql\/\">https:\/\/fivemx.com\/mysql-async-to-oxmysql\/<\/a><\/li>\n\n\n<!-- fivemx-blog-portfolio-wave4:removed-sql-self-link -->\n<li>Adapter patterns for script ports. <a href=\"https:\/\/fivemx.com\/adapter-patterns\/\">https:\/\/fivemx.com\/adapter-patterns\/<\/a><\/li>\n\n\n\n<li>QBCore install quickstart. <a href=\"https:\/\/fivemx.com\/qbcore\/\">https:\/\/fivemx.com\/qbcore\/<\/a><\/li>\n\n\n\n<li>Script conversion checklist. <a href=\"https:\/\/fivemx.com\/converting-fivem-scripts\/\">https:\/\/fivemx.com\/converting-fivem-scripts\/<\/a><\/li>\n\n\n\n<li>QBOX with ox stack overview. <a href=\"https:\/\/fivemx.com\/qbox-ox-stack\/\">https:\/\/fivemx.com\/qbox-ox-stack\/<\/a><\/li>\n\n\n\n<li><a class=\"wpil_keyword_link\" href=\"https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/\" title=\"How to Use Resmon in FiveM (To Optimize Resources)\" data-wpil-keyword-link=\"linked\" data-wpil-monitor-id=\"1738\">Resmon<\/a> and performance. <a href=\"https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/\">https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/<\/a><\/li>\n<\/ol>\n\n\n\n<h2 id=\"external-references\" class=\"wp-block-heading\">External references<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/qbcore.net\/\" target=\"_blank\" rel=\"noopener\">QBCore framework<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/coxdocs.dev\/oxmysql\" target=\"_blank\" rel=\"noopener\">oxmysql documentation.<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/coxdocs.dev\/ox_lib\" target=\"_blank\" rel=\"noopener\">ox_lib documentation.<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.fivem.net\/docs\/scripting-reference\/runtimes\/lua\/functions\/GetPlayerIdentifiers\/\" target=\"_blank\" rel=\"noopener\">CFX.re identifiers reference.<\/a><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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. Prerequisites Step 1. Make a Plan and a Rollback Point Step 2. Build [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":199543,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2882,1902,1899],"tags":[2858,2950,2859],"class_list":["post-199542","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-framework-conversion","category-fivem-related","category-tutorials","tag-esx","tag-framework","tag-qbcore"],"_links":{"self":[{"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/posts\/199542","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/comments?post=199542"}],"version-history":[{"count":5,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/posts\/199542\/revisions"}],"predecessor-version":[{"id":214893,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/posts\/199542\/revisions\/214893"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/media\/199543"}],"wp:attachment":[{"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/media?parent=199542"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/categories?post=199542"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemx.com\/es\/wp-json\/wp\/v2\/tags?post=199542"}],"curies":[{"name":"gracias","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}