Migrating from QBCore to QBox without downtime.
A field guide to the upgrade path: schema diffs, the bridge layer, and the four edge cases that will bite you on go-live night.
We've now done four QBCore → QBox migrations in production, including one for a 200-slot server with twenty-thousand persisted vehicles. Every one of them surfaced an edge case the migration guides don't mention. This post is the field guide we wish we'd had on the first one.
The high-level path
The migration itself is conceptually simple:
- Add the QBox core resource alongside QBCore.
- Run the QBox migration scripts against your existing database.
- Swap the bridge layer your scripts use to talk to the framework.
- Cut over at a maintenance window. Smoke-test. Done.
Each of those steps has at least one foot-gun. The goal of this post is to point them out before they bite.
Step 1: schema diffs you'll actually see
QBox is mostly a superset of QBCore's schema, but "mostly" is the word doing all the work. The columns that have changed shape in production for us:
players.charinfo— JSON shape gains agenderenum field. QBCore servers that stored gender asm/fneed to migrate tomale/female. A one-lineUPDATEquery, but if you miss it, character creation will reject every existing player.player_vehicles.mods— thetyreSmokeColorfield moves from a flat array to an object. The compat shim handles reads but not writes; vehicle saves silently lose tyre smoke until you migrate.bans—discordandlicenseidentifiers now live in a normalizedidentifiersJSON column. The compat view papers over reads.
-- the migration we run before the cutover
UPDATE players
SET charinfo = JSON_SET(
charinfo,
'$.gender',
CASE JSON_EXTRACT(charinfo, '$.gender')
WHEN '"m"' THEN 'male'
WHEN '"f"' THEN 'female'
ELSE JSON_EXTRACT(charinfo, '$.gender')
END
)
WHERE JSON_EXTRACT(charinfo, '$.gender') IN ('"m"', '"f"');Run this before the framework swap, while QBCore is still authoritative. It's idempotent.
Step 2: the bridge layer
Every RB Studios script ships with a bridge — a thin adapter between our internal API and whatever framework you're running. The bridge for QBCore and the bridge for QBox are deliberately separate files, not branches inside one file. This means the cutover is a config flip, not a code change.
-- before
Config.framework = "qbcore"
-- after
Config.framework = "qbox"If you've been editing our bridge directly (please don't), this is the moment your edits become invisible. Move customizations to the Config.hooks table — it survives bridge swaps.
Step 3: the four edge cases
These are the ones that will bite you on go-live night. We've now hit each of them at least twice.
1. State bags vs. metadata
QBox prefers state bags for player metadata; QBCore prefers a PlayerData.metadata object. If you have third-party scripts reading PlayerData.metadata.<thing>, they'll silently stop seeing updates the moment QBox is authoritative.
The fix is a one-line bridge in QBox that mirrors state-bag changes back into the PlayerData object. We ship this by default; most other resources don't. Audit your third-party scripts for PlayerData.metadata reads before the cutover.
2. Job grades as strings
QBCore stored job grades as integers. QBox stores them as strings. If any of your scripts compare grades with == and a number literal, they will silently fail closed and tell your mechanics they're not on duty.
3. The vehicle key event
QBCore fires qb-vehiclekeys:client:SetOwner. QBox fires qbx_vehiclekeys:client:setOwner. Different namespace, different casing. The compat shim aliases one to the other for the lifetime of a single boot — if you restart qbx_vehiclekeys mid-session, the alias is gone until the next full server restart.
4. Bans take effect on next connect, not now
QBCore's ban check runs on every player tick. QBox's runs on playerConnecting. This is the right behavior — way cheaper — but it means a ban issued mid-session doesn't kick the player until they reconnect. If your admins are used to instant-kick-on-ban, give them a heads-up.
What "no downtime" actually means
Strictly speaking, there is a downtime: the moment you ensure qbx_core and restart the framework, every active player is dropped. What we mean by no-downtime is that the migration itself doesn't require a long maintenance window — a single thirty-second framework restart is enough, and the database work happens online.
For the 200-slot server, the actual visible downtime was forty-one seconds. Players reconnected, characters loaded, and the migration was done before the support queue caught up.
When to migrate
We're now recommending QBox for any new server. For existing QBCore servers: if you're stable and your players are happy, the migration is optional. The performance and ergonomic improvements are real, but they're not "drop everything" real.
If you do migrate, run the database migrations on a staging clone first. There is no substitute. The schemas you'll find in your production database are almost never exactly what the migration scripts expect — every server has its own history of half-finished schema changes from old resources. Find them on staging, not at 2am on cutover night.