Case study: a centralized control panel for FiveM (ESX) with Go and React
How we engineered a high-performance admin panel for a GTA V roleplay server in 2021 — bridging modern web tooling with the FXServer runtime long before standardized tools existed.
Back in 2021, administering a GTA V (FiveM) roleplay server was painful. All-in-one admin tools like txAdmin either didn’t exist or weren’t standardized yet, so server owners were stuck juggling in-game commands, rough Discord-bot integrations, and — far too often — editing the production database by hand. Together with a development partner, I set out to fix that with a purpose-built architecture for the ESX framework: a proper admin suite plus a public player portal.
This is a breakdown of how it was built, the decisions behind it, and the one problem that defined the whole design.
The problem
A roleplay server is a live, stateful system. Player data — money, jobs, properties, inventory — lives both in a MySQL database and in the running game server’s memory. Any tool that wanted to change that data safely had to respect both, in real time, without corrupting a connected player’s session. Editing the database directly (the common “solution” at the time) silently desynchronized the in-memory state and caused data loss.
Stack and why
We split the system into independent layers, each chosen for a concrete reason:
- Go (backend). Cheap concurrency and a tiny memory footprint mattered: the panel ran alongside the game server on the same host, so it couldn’t fight it for resources. Go let us handle bursts of HTTP traffic without becoming a bottleneck.
- React + Material UI (frontend). A single-page app gave admins a fast, no-reload workflow. MUI’s ready-made tables, dialogs and forms let us focus on business logic — critical, because we were racing to have the panel ready before the server’s launch day.
- MySQL (database). The native storage layer for ESX. We integrated directly against its structural tables.
- Lua (native scripting). A custom resource inside the FXServer that acted as the real-time bridge.
Architecture: the bridge is everything
The Go service was the engine, sitting between the web UI, the ESX database, and the game process itself. It followed a clean layered design — handlers → services → repositories — with a tuned connection pool for concurrent analytics queries, and JWT + role-based access control (RBAC) middleware guarding every admin endpoint.
But the heart of the system was the Lua bridge resource. Installed directly in the FXServer, it gave Go a way to reach into the live server. The backend would persist a change to MySQL and then fire an HTTP event at the bridge, which forced ESX to refresh that player’s in-memory state instantly:
// After persisting a change, notify the live server so ESX
// refreshes the player's cached state — no desync, no data loss.
func (s *PlayerService) SetMoney(ctx context.Context, id string, amount int) error {
if err := s.repo.UpdateMoney(ctx, id, amount); err != nil {
return err
}
return s.bridge.Emit(ctx, "esx:refreshAccount", PlayerEvent{
ID: id,
Account: "money",
Value: amount,
})
}
That single pattern — write to the database, then tell the game to reconcile — is what made the whole panel safe to use on a live server.
Two surfaces, two audiences
- Internal admin suite (authenticated): live player monitoring with their Steam / License / Discord identifiers, a sanctions module (temporary/permanent bans and kicks, all logged for auditing), and real-time editing of critical ESX variables — balances, properties, job assignments.
- Public player portal (read-only): players could check their characters, registered vehicles and licenses without launching GTA V, plus basic server telemetry. This alone cut a huge volume of support tickets.
On the client, the heavy lifting was data: server-side pagination let admins search across thousands of ESX records without choking the browser, and a small caching layer cut redundant API calls.
The hard part: in-game state synchronization
The defining challenge was exactly the one above: changing a connected player’s database row desynchronized ESX, which keeps that player cached in memory. The bridge resource solved it. When an admin changed a value in the panel, Go updated MySQL and emitted an event to the Lua resource, which forced ESX to refresh that player’s local state in real time — eliminating both data loss and duplication.
Demo: an early prototype
Heads up: this is a very early build — expect visual bugs, placeholder data and half-finished screens. It’s the only video I managed to recover from back then, and I keep it as a little time capsule of how the panel looked in its infancy.
Takeaways
Building this in 2021 showed how much leverage you get from applying real web-engineering practices — Go, React, a layered API, RBAC — to a problem domain (game-server admin) that hadn’t caught up yet. We bridged modern web tooling and the FXServer runtime years before standardized solutions arrived, and the core lesson stuck: own the source of truth, then make every other system reconcile to it. It was also a genuinely shared effort — the result of building it shoulder to shoulder with my development partner.