To install, run:
haxelib install fivem-hx 1.2.2
See using Haxelib in Haxelib documentation for more information.
README.md
fivem-hx
Write FiveM resources in a language that catches your mistakes before your players do.
Typed Haxe externs for the complete FiveM/RedM native API — generated straight from
CitizenFX's own native databases — plus an object-oriented core layer for entities,
players, zones, callbacks and UI. Compiles to plain Lua that runs on an unmodified server.

API reference ·
Getting started ·
Core API ·
Examples
The 30-second version
A speedometer. First, what you write in Lua today — then the same thing with fivem-hx.
-- Lua
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
local ped = PlayerPedId()
local veh = GetVehiclePedIsIn(ped, false)
if veh ~= 0 then
SetTextFont(4)
SetTextScale(0.45, 0.45)
SetTextColour(255, 255, 255, 255)
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(math.floor(GetEntitySpeed(veh) * 3.6) .. " km/h")
DrawText(0.5, 0.9)
end
end
end)
// Haxe
Thread.everyFrame(() -> {
var vehicle = LocalPlayer.vehicle();
if (vehicle != null)
Ui.drawText('${Math.round(vehicle.speed * 3.6)} km/h', new Vector2(0.5, 0.9));
});
Shorter is nice. Not shipping a typo to production is nicer — and that's the actual pitch.
Why Haxe over Lua for FiveM?
Lua is a fine language. The problem isn't Lua — it's that FiveM's Lua runtime has
no idea what the natives are. Every native is a global lookup resolved at call time,
every argument is untyped, and every mistake is a runtime surprise on a full server.
Haxe knows every one of them at compile time.
1. A typo is a compile error, not a 3 AM incident
-- Lua: a completely valid program. It just doesn't work.
local ped = PlayerPedId()
local coords = GetEntityCoors(ped) -- typo — this is nil
SetEntityCoords(ped, coords.x, coords.y, coords.z + 10.0)
Lua happily loads this resource. Nothing complains until a player triggers the code path,
and then you get attempt to index a nil value (local 'coords') in a console nobody
was watching.
// Haxe: the compiler stops you.
var ped = LocalPlayer.ped();
var coords = ped.coors;
// ^^^^^ Ped has no field coors (Suggestion: coords)
The build fails. Your editor underlines it while you type. The bad resource never
reaches the server.
2. Wrong arguments can't get through
SET_ENTITY_COORDS takes eight parameters. Lua doesn't care if you pass three.
-- Lua: missing arguments quietly become nil
SetEntityCoords(ped, 215.9, -810.1) -- forgot z, and the four flags
// Haxe: the type system won't let the call compile
ped.coords = new Vector3(215.9, -810.1);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ Not enough arguments, expected z:Float
Same for types: pass a String where a native wants a Float, or a Bool where it
wants an entity handle, and you find out at build time — not from a bug report.
3. Magic numbers become names you can actually read
-- Lua: what is -1? what is 4? good luck.
TaskWarpPedIntoVehicle(ped, veh, -1)
SetVehicleDoorsLocked(veh, 4)
// Haxe: enums, with autocomplete and documentation on hover
ped.warpIntoVehicle(vehicle, Driver);
vehicle.lockState = LockedPlayerInside; // "Players can't get in, and anyone inside can't get out."
VehicleSeat, VehicleLockState, VehicleDoor, EntityType, Control and the rest are
enum abstracts — they're plain integers at runtime with zero overhead, but the
compiler rejects the values that don't exist and your editor lists the ones that do.
4. Vector math that reads like vector math
-- Lua: spawn a car three metres in front of a ped
local pos = GetEntityCoords(ped)
local fwd = GetEntityForwardVector(ped)
local spawn = vector3(pos.x + fwd.x * 3.0, pos.y + fwd.y * 3.0, pos.z + fwd.z * 3.0)
local dist = #(pos - GetEntityCoords(target))
// Haxe: operator overloading, fully inlined
var spawn = ped.coords + ped.forwardVector * 3.0;
var dist = ped.distanceToEntity(target);
Vector2/Vector3/Vector4/Quaternion/Rgba are inline abstracts with
+ - * / == -, distance, lerp, normalized, cross, clampLength and friends.
They compile down to the same arithmetic you'd have written by hand.
5. Client↔server round trips stop being a two-file scavenger hunt
Asking the server a question in plain Lua means inventing two event names, wiring
both directions, and hoping the reply arrives:
-- Lua, client
RegisterNetEvent("garage:spawned")
AddEventHandler("garage:spawned", function(netId)
-- ...and now continue the logic you started somewhere above
end)
TriggerServerEvent("garage:spawn", "adder")
-- Lua, server
RegisterNetEvent("garage:spawn")
AddEventHandler("garage:spawn", function(model)
local src = source
-- ...
TriggerClientEvent("garage:spawned", src, netId)
end)
// Haxe, client — one line, and the code stays in one place
var netId:Int = Callbacks.await("garage:spawn", "adder");
// Haxe, server — just return the answer
Callbacks.register("garage:spawn", (source, args) -> {
var player = Player.fromSource(source);
if (player == null) return 0;
var vehicle = Vehicle.create(args[0], player.coords, player.ped.heading);
return vehicle.netId;
});
Under the hood it's a correlation key layered over FiveM's own events — no external
dependency — and it works client→server and server→client. await suspends only the
calling coroutine (a thread, event handler, command or export), so the rest of the
resource keeps running while it waits.
6. Shared code that is actually shared
FiveM's shared_script gives you one Lua file loaded into two runtimes. It does not
give you any guarantee that the two sides agree about what's in it. So the model
whitelist gets copy-pasted into the client for the UI, drifts from the server's copy,
and now players see cars they can't spawn.
// src/Shared/Vehicles.hx — compiled into BOTH client.lua and server.lua
class Vehicles {
public static final allowed = ["adder", "zentorno", "t20"];
public static function isAllowed(model:String):Bool
return allowed.indexOf(model.toLowerCase()) != -1;
}
// Client — same function, same list
if (!Vehicles.isAllowed(model)) { Ui.notify("~r~Not on the list."); return; }
// Server — same function, same list, and it's still the authority
if (!Vehicles.isAllowed(model)) return 0;
Rename isAllowed, change its signature, drop a model from the list — both builds fail
or both builds update. One side can never quietly fall behind the other.
7. Everything else the compiler does for free
| Lua | Haxe | | Rename a function | grep, pray | compiler finds every call site |
| "What does this return?" | read the wiki, guess | hover in your editor |
| Autocomplete on natives | none | every native, with its docs inline |
nil handling | if x ~= nil and x ~= 0 everywhere | Null<T>, ?., and null-safety checks |
| Structured data | untyped tables | typedefs, enums, generics, real classes |
| Dead code / unused imports | ships to the server | flagged at build time |
| Refactoring across files | manual | safe by construction |
8. …and it costs nothing at runtime
This is the part people expect a catch on. There isn't one. Natives are extern
declarations mapped onto FiveM's Lua globals, so the compiler emits the call you would
have written by hand.
Straight from examples/basic-resource — Haxe in, Lua out:
// src/Client/ClientMain.hx
CoreEvents.triggerServerEvent("hx_resource:server_ping", "Hello from client");
-- dist/client/client.lua, generated
_G.TriggerServerEvent("hx_resource:server_ping", "Hello from client")
No wrapper table, no dispatch layer, no reflection. The math types (Vector3, Rgba,
Hash, the enum abstracts) are abstracts over plain data with inline methods, so
they compile away entirely; the entity wrappers are thin objects around the raw handle
you'd have been passing around anyway. You only pay for what you call.
When Lua is still the right call
Worth saying plainly: if you're writing a 20-line resource, editing files live on a
running server, or pasting a snippet off the forums to try in the next five minutes,
Lua's lack of a build step wins. fivem-hx pays off when a resource gets big enough
that you stop being able to hold all of it in your head — which, for most gamemodes,
is somewhere around week three.
Install
haxelib install fivem-hx
Working on the library and a resource at the same time? Use haxelib dev instead — see
docs/getting-started.md.
Requirements: Haxe 4.3+, a FiveM or RedM server to run
against, and Python 3.9+ only if you want to regenerate the natives yourself.
Quick start
// src/Client/ClientMain.hx
import fivem.client.Core;
class ClientMain {
static function main() {
Commands.register("whereami", (_, _) -> {
var coords = LocalPlayer.coords();
Ui.notify('You are at ${coords.toString()}');
});
}
}
# client.hxml
-lib fivem-hx
-p src
-main ClientMain
-lua dist/client/client.lua
haxe client.hxml
Point your fxmanifest.lua at dist/client/client.lua and you're running. The full
walkthrough — manifest, server side, build layout — is in
docs/getting-started.md.
A single import fivem.client.Core; (or fivem.server.Core) brings the whole core layer
into scope. Every name in it is a type alias, so the import costs nothing.
What's in the box
- Every native, typed — the complete GTA and CitizenFX native surface for client,
server and shared, regenerated on demand from the live FiveM docs (
generate.py). - An object-oriented core layer —
Entity, Ped, Vehicle, Player, Players,
World, Zones, Blip, Ui, Nui, Controls, Raycast, Streaming, Thread,
Events, Callbacks, StateBag, Kvp, Exports, Logger, and the math types.
See docs/core-api.md. - Correct apiset separation — client-only, server-only and shared natives kept apart,
with shared natives merged into both sides so nothing is unreachable
(why).
- Multi-return natives that actually return everything — FiveM's Lua runtime appends
out-parameters to the return values;
NativeOutputs models that with @:multiReturn
so results like ground-Z and shape tests aren't silently thrown away. - Zone system — circles, spheres, boxes and polygons with
onEnter/onExit/onInside
callbacks, on both client and server — all zones share one adaptive poll loop instead
of one thread each. - Database driver —
fivem.server.db wraps oxmysql
as a sys.db.Connection, usable directly or with the record-macros ORM
(docs). - Working example resources — not snippets: they compile, and they run.
Examples
| Example | What it shows | examples/core-demo | The core API end to end — zones, blips, keybinds, server-authoritative vehicle spawning through callbacks, the player registry, state bags |
examples/basic-resource | The smallest complete client+server resource: native calls, commands, an event across the boundary |
examples/mysql-example | The oxmysql driver: schema creation, loading and saving a row through a plain Haxe model |
Project structure
fivem-hx/
├── src/fivem/
│ ├── client/ package fivem.client — client-only + shared natives
│ │ └── core/ Entity, Ped, Vehicle, Player, Zones, Ui, Raycast, ...
│ ├── server/ package fivem.server — server-only + shared natives
│ │ ├── core/ Entity, Ped, Vehicle, Player, Players, Zones, Http, ...
│ │ └── db/ oxmysql-backed sys.db.Connection driver
│ └── shared/ package fivem.shared — shared natives + hand-written externs
│ ├── core/ Thread, Events, Commands, StateBag, Kvp, Resource, ...
│ ├── util/ Vector2/3/4, Quaternion, Rgba, Hash, MathUtil, Logger, ...
│ └── colshape/ Sphere, Circle, Box, Poly
├── examples/ basic-resource, core-demo, mysql-example
├── docs/ in-depth guides (see below)
├── scripts/
│ └── Publish-Docs.ps1 builds the Dox API site and pushes it to the docs branch
├── .github/workflows/ CI (typecheck + build example) and tag-triggered releases
├── generate.py regenerates src/fivem/*/natives from FiveM's docs
├── test-*.hxml type-check-only builds for each package
├── CONTRIBUTING.md
├── CHANGELOG.md
└── haxelib.json
One classpath, one package tree, mirroring how the Haxe standard library is organized —
haxelib install and go, no juggling three separate packages.
docs/architecture.md has the full rationale.
Documentation
The generated API reference covers the
what — every class and method signature under fivem.client, fivem.server and
fivem.shared, browsable and searchable. The guides below cover the how.
| Guide | What's in it | | Getting Started | Install, wire up your first resource |
| Core API | The OOP layer: entities, players, vehicles, zones, vectors, callbacks, state bags |
| API Guide | Calling conventions, player source types, vectors, multi-return natives |
| Architecture | Repo layout rationale, apiset merging, how externs compile to zero-cost calls |
| Database (oxmysql) | The sys.db.Connection driver, direct queries, wiring up record-macros |
| Haxe + FiveM Tips | Lua runtime versions, debugging, structuring client/server/shared code |
| Dev Experience | Editor setup, fast builds, watch loops, debugging without source maps |
| Regenerating Natives | How generate.py works, its limitations, how to extend it |
| Versioning | What's PATCH/MINOR/MAJOR when most of the API is generated |
| Publishing | Release checklist, tag-triggered packaging, haxelib submit |
| Troubleshooting | Common compiler and runtime errors, explained |
See also CONTRIBUTING.md and CHANGELOG.md.
Working on the library
Type-check every package — no build target needed, the fastest way to confirm a
change to generate.py or a manual edit under src/ didn't break anything:
haxe test-shared.hxml && haxe test-server.hxml && haxe test-client.hxml
Regenerate the natives — downloads the live FiveM/RedM native databases and rewrites
everything under src/fivem/*/natives/ plus the Natives.hx entry points. Hand-written
files are left alone:
python generate.py
Details in docs/regenerating-natives.md.
Contributing
Issues and PRs welcome at
github.com/Kleppinger/fivem-hx.
CONTRIBUTING.md covers dev setup, the PR checklist, and where a given
fix actually belongs (generator vs. generated output vs. hand-written externs) — that
last part matters more here than in a typical repo. Every push and PR runs
CI: type-check all three packages, build both example
resources for client and server, and verify the generated Lua parses.
Versioning & releases
Follows SemVer, scoped for a mostly-generated externs library in
docs/versioning.md — notably, a native's signature changing upstream
is a MAJOR change even when it only comes from re-running the generator. Release history
lives in CHANGELOG.md.
Maintainers cutting a release: docs/publishing.md. Pushing a
vX.Y.Z tag runs the release workflow, which verifies
the tag matches haxelib.json, type-checks, and attaches a release zip to the GitHub
Release. haxelib submit stays a manual last step — haxelib's CLI has no
non-interactive auth flow to script around.
License
MIT