NullCore: Why I'm Building My Own FiveM Framework
After years living inside QBCore, I'm building NullCore — a standalone, server-authoritative FiveM framework that's correct and small where ESX and QB are sprawling and stringly-typed. Here's the thinking, and where it stands.
I've spent the better part of a decade in and around FiveM, and a huge chunk of that living inside QBCore. I wrote a whole post a while back on why I use QB-Core over ESX, and I stand by every word of it — QB won on simplicity, docs and exports. But there's a difference between "the best of the options in front of me" and "the thing I'd build if I started from scratch." After enough years patching around the same structural problems, I finally admitted I wanted the second one. So I'm building it. It's called NullCore.
Why bother, when QB and ESX already exist?
Because both of them are the product of years of accretion, and it shows. The fastest way to be genuinely different isn't to add more — it's to be correct and small where they're sprawling and stringly-typed. NullCore is a clean-room replacement for ESX, QB-Core and Qbox: server-authoritative, modular, and typed by convention. One developer can own it end to end, and a resource author should be able to learn it in an afternoon.
The design starts by being honest about what the incumbents got right, because there's plenty. I'm keeping GetPlayerServerId as the one canonical identity across the client/server boundary. I'm keeping statebag-driven replication as the default sync for player metadata — it's zero-cost when idle and auto-syncs late joiners, which is genuinely the best modern idea in the ESX/QB lineage. I'm keeping the metadata-driven player model, the bound-method ergonomics (Player.Functions.AddMoney(...)), data-driven hot-reloadable jobs and grades, and the hard-won lesson that inventory and the DB layer belong out of core, behind exports.
What I'm deliberately throwing away
This is where the opinion lives. Every one of these is a specific thing that's bitten me or someone I've helped, on a real server, more than once.
- The god-object. The mutable global
ESX/QBCoretable — and especially ESX's resurrectedgetSharedObjectevent — is gone. NullCore uses explicit,require-able imports and a thin compat export instead. Other resources pull the typedNullCorenamespace in via an import file, not a global. - Closure-per-player objects. ESX allocates roughly 60 closures per player object. NullCore uses one shared method table via an
__indexmetatable, with a thin bound wrapper for the nice call syntax. No per-player closure soup. - Linear-scan money and inventory. Accounts and metadata are keyed maps — O(1) — never
pairs-scanned on a hot path. - Stringly-typed grade keys. Grades are integers end to end. No
tostring(grade), ever. That single change is the number-one migration fix Qbox had to make, and I'd rather just never have the bug. - Identifier-as-primary-key. Characters key on an immutable synthetic
citizenid; a separate identity table maps every license / discord / fivem identifier back to an account. - "The cache is the truth" money. This is the big one, so it gets its own section.
Money is the whole ballgame
If a framework gets one thing catastrophically wrong, it's money — dupes, lost updates, negative balances that shouldn't exist. The classic failure is read-modify-write in Lua with the cache treated as authoritative. Two things happen in the same frame, they race, and someone's now got money that came from nowhere.
NullCore's rule is blunt: the database row is the single runtime source of truth for money. The Lua Player.money table and the replicated statebag are write-through projections — never authorities. Every economy mutation for a given citizenid runs through a single in-process queue, so a paycheck tick and a purchase can't interleave. Decrements are a conditional, atomic UPDATE ... WHERE citizenid = ? AND bank >= ? that asserts exactly one row changed, so the database itself enforces that you can't go negative. After a successful mutation the new balance is read back within the same transaction, and that value — never a Lua-computed sum — is what the cache and statebag are set to.
It's all integer cents, never floats, and there's one money API — no addMoney/addAccountMoney twin, no SetMoney that invites lost-update bugs. It's addressed by citizenid, so it works whether the character is online or offline. Every mutation carries a mandatory reason for the audit log, including the denied ones. That's not gold-plating; that's the spec the entire framework rests on.
The "don't reinvent the wheel" line
The temptation with a project like this is to rebuild everything and lose a year. I'm not doing that. The defensible boundary for a solo developer is: build the core, depend on commodities.
So NullCore depends on oxmysql for the database driver, ox_lib for UI/callbacks/zones, and ox_inventory for inventory — and it reinvents only the player, character, money, job and gang core, plus the database schema. Rebuilding a MySQL driver or ox_inventory's slot-and-weight system would burn exactly the months I need for the actual IP. The one piece of real work there is a maintained nullcore bridge module for ox_inventory, pointed at my tables (characters.citizenid, owned_vehicles.plate), so the entire existing ox ecosystem lights up on day one. Interop is distribution.
And I'm making a clean break, not a compatibility promise. NullCore's data model is deliberately incompatible with ESX/QB — integer grades, cents, a normalised schema. Faithfully emulating QBCore.Functions or ESX's xPlayer across those boundaries is a multi-month sub-project each, with a permanent "works for 80% of scripts, subtly breaks 20%" support tax. So live ESX/QB shims are explicitly cut from launch — maybe later, demand-driven, never a launch commitment. The migrator that does ship has a money-conservation invariant: it sums every account across every character before and after, and if the totals don't match to the cent, it aborts.
Where it actually is right now
This isn't a pitch deck — Phase 0 exists and runs. Phase 0 is the runnable skeleton: DB bootstrap with auto-applied schema, identity resolution, the Player object, crash-safe persistence, and the server.cfg ACE authority layer so multiple players can be on at once at different permission tiers.
That authority layer is the whole point of Phase 0, and it's testable in-game. The source of truth is server.cfg ACEs — tiers are principals from nc_player up to nc_owner that inherit upward, and NullCore only ever reads them via IsPlayerAceAllowed. Join with two clients and you can prove it: /nc prints your resolved tier, /coords is gated to trusted-and-up, /kick needs mod, and /setgroup needs admin and is rank-clamped so you can never promote anyone at or above your own tier. A plain player running /kick gets a denial that lands in the audit log. That difference is the entire lesson of Phase 0: the client can never grant itself anything.
There's also a pure-Lua unit test suite — tier resolution, name sanitisation, citizenid generation, identifier parsing — that runs under busted with no FXServer and no database. Because a framework whose entire pitch is "correct and small" has to be able to demonstrate correctness, not just claim it.
The MVP is Phase 0 plus Phase 1: join, make a validated character, spawn, hold money (online and offline), have a job and a society account, and persist correctly under failure. After that it's inventory bridge, vehicles and garages, admin tooling, and — because a framework is really a distribution, not a core resource — a lean txAdmin recipe that bundles the whole thing.
Why "NullCore"? It reads as "the core substrate everything runs through," it's got zero naming collision with the incumbents, and it gives a crisp exports.nullcore: surface. That's the plan, and it's underway.
If you run a server, write FiveM scripts, or just have strong opinions about how a framework should handle money — I want to hear them. Come argue with me on Discord. This is exactly the kind of thing that gets better with pushback.
More posts.
Why I Use QB-Core Over ESX (And When You Shouldn't)
After trying every framework out there, I settled on QB-Core. Here's why — and the trade-offs.
How to Create FiveM Assets with Sollumz 2.8.0
Getting started with Sollumz and Blender for custom FiveM models and maps.
AJD Dumpster Diving — Free QB-Core Script Release
A free QB-Core scavenging script — how it works and what's inside.