@MartxDev
Back to blog

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.