Most Garry’s Mod addons are “session-local”: you join, you roleplay, you disconnect, and most of what happened exists only in chat logs and memory.

That’s fine for casual servers. It breaks down fast when you want a real roleplay ecosystem: persistent characters, ranks, equipment, a shared economy, territory control, and long-term community progression.

This post breaks down how I built a hybrid system for a Star Wars roleplay server: real-time gameplay in Lua, persistence and administration in PHP/MySQL, and rich in-game UI via DHTML panels. You’ll learn the architecture, the main data flows (join → select character → spawn), how the conquest/tech/crafting layers work, and the practical trade-offs you run into when you fuse a game server with a web stack.

Background / Context

Garry’s Mod runs gameplay logic in Lua, split across server and client environments. If you want persistence, you need something outside the map lifecycle: a database, a service layer, and a way to expose complex UI without re-implementing a full UI framework in VGUI.

Key terms, in plain language:

  • Server-side Lua: authoritative gameplay, spawning, jobs, weapons, money.
  • Client-side Lua: HUD, input, rendering, local data cache, UI triggers.
  • DHTML panels: an embedded browser inside GMod. You can render HTML/CSS/JS and bridge JS ↔ Lua for interactions.
  • PHP/MySQL backend: stores characters, ranks, inventory/equipment, conquest state, tech progress, crafting storage.
  • API endpoints: JSON feeds consumed by the game server to keep the game state synchronized with the database.
Why it matters now : roleplay servers increasingly want “meta systems” (community progression, territory wars, crafting economies) but still need in-world immersion. DHTML gives you modern UI velocity; a web backend gives you persistence and admin tooling; Lua stays focused on gameplay and presentation.

Main Content

1) The high-level architecture: three layers that behave like one product

I treated the whole thing as a single system with three responsibilities:

  • Game layer (Lua, Garry’s Mod): Owns spawning, jobs, weapons, in-world entities (consoles), HUD overlays. Talks to backend through an HTTP integration layer. Broadcasts synced data to clients via net messages.
  • Persistence + business logic (PHP/MySQL): Owns database state: users, characters, ranks, jobs, factions, storage, tech progress, conquest. Implements “rules” that are easier server-side: prerequisites, timers, crafting stack logic, admin forms.
  • UI layer (DHTML, web/ingame pages): Character creation/selection, conquest map, tech tree, crafting, reports, whitelist, etc. Uses JS-to-Lua calls for in-game actions (select character, enter game, close panel, change model).

Here’s the mental model:

         ┌──────────────────────┄
         │   PHP/MySQL Backend   │
         │  - DB + rules + admin │
         └─────────┬────────────┘
                   │ JSON over HTTP
                   ▼
┌───────────────────────────────┐
│      GMod Server (Lua)         │
│ - cache global data            │
│ - fetch per-user data          │
│ - spawn characters             │
│ - broadcast to clients         │
└─────────┬─────────────────────┘
          │ net messages (GMod)
          ▼
┌───────────────────────────────┐
│      GMod Clients (Lua)        │
│ - local data caches            │
│ - HUD (nameplates, ID cards)   │
│ - DHTML panels + JS bridge     │
└───────────────────────────────┘

2) Core initialization: bootstrapping without chaos

In GMod addons, load order and realm separation (shared/server/client) can wreck you if it’s sloppy.

The bootstrap file (autorun) is the entry point. Its job is boring but critical:

  • load shared config and language stubs first
  • load server modules (HTTP integration, data, network, commands)
  • load client modules (input, local cache, network handlers, visuals, HUD)
  • ensure client files get sent to players (AddCSLuaFile)

Configuration is centralized in a shared config module, including:

  • API URL
  • API key
  • keybinds like F4 (character) and F6 (web panel)

This design is intentionally “single source of truth”. When you change the backend URL or move environments, you don’t want to grep 20 files.

3) Server-side: HTTP API integration + caching + spawning

The server is the bridge between a persistent world and a real-time session.

I split responsibilities into clear modules.

  • HTTP API layer: wraps every endpoint call: status, user registration, full profile fetch, ranks/models/equipment sync, targeted “spawn data” fetch; keeps backend I/O out of gameplay logic
  • Data layer: caches global datasets (ranks, models, equipment); tracks connected players and which character they chose; owns the spawning logic
  • Server loop / orchestration: hooks player lifecycle events (connect/spawn/setmodel); periodically refreshes data from the backend to keep things consistent
  • Network layer: registers net strings; sends payloads to clients; receives character selections from clients

The spawn pipeline is where it all becomes “RP real”:

  1. Resolve the selected character
  2. Apply model
  3. Build display name using rank patterns (example: "{rank} {name} {surname}")
  4. Apply DarkRP job and money
  5. Strip/re-grant equipment from character loadout
  6. Mark the player as “active character” and broadcast it so other clients can display identity info

This is the core value: you’re not just storing a name in a SQL row. You’re producing a consistent in-game identity and loadout that matches rank/job/equipment rules.

4) Client-side: local caches, network handlers, DHTML UI, and HUD overlays

On the client, I optimized for responsiveness and “don’t block on the web”.

  • Local data cache: mirrors rank/model/user/equipment datasets; stores “who is playing what character” for other players; handles UTF-8 quirks for names (accents, etc.)
  • Network layer: receive handlers populate the local cache; send handlers request refresh or send the selected character choice

DHTML UI system

  • Fullscreen character menu (F4)
  • Web UI hosted by the backend, loaded in-game
  • A Lua/JS bridge for actions like: SelectCharacter(id), EnterTheGame(id), ChangeModel(modelId), Close, Disconnect, ChangePage, animation lock/unlock

Where this gets interesting is the hybrid UX:

  • HTML does the data-heavy UI (lists, multi-step forms, dynamic CSS)
  • Lua does what HTML can’t: render a real 3D model viewer (DModelPanel), animate it, and manage camera transitions

HUD system

I replaced default DarkRP HUD and built roleplay-specific overlays:

  • World-space nameplates when you look at a player in range
  • An “identity card” mode triggered by a pocket item: shows full rank/faction/accreditation/licenses with styled animation
  • A medical scanner overlay when holding a medkit

This is not “UI for UI’s sake”. It makes RP friction lower: you can identify people, verify credentials, and run interactions without chat spam.

5) Web backend: in-game pages are “tools”, not a website

I separated two web experiences:

  • In-game panels (web/ingame): accessed via in-game entities (consoles) and keybinds; authenticated via session + API key checks; designed for quick interaction while playing
  • Admin website (web/website): separate login; server owner tooling: factions, jobs, user management, etc.; Steam OpenID integration for account linking and auto-registration

This split matters because in-game UX has different constraints: players will alt-tab less; navigation should be shallow; security model must prevent cross-user data access even if someone copies a URL

6) The “meta” systems: conquest, tech tree, and crafting

These are the systems that turn an RP server into a persistent community game.

Conquest (galaxy map)

  • planets positioned from database coordinates
  • links between planets drawn dynamically with geometry math (rotation + width from coordinates)
  • missions attached to planets, with visibility rules (“????” vs known)
  • in-game access via conquest console entity

Tech tree (server-wide research)

  • only one research active at a time (global queue)
  • prerequisites (IDs list) gate progression
  • timed unlocking: start/end timestamps in technology_progress
  • acceleration techs compound research speed bonuses
  • visible as: locked / in progress / unlocked
  • includes rewards into the economy (resources)

Crafting (industrial + storage stack management)

  • blueprint list + AJAX blueprint detail loading
  • crafting consumes community storage resources
  • stack-aware consumption: drain smaller stacks first, delete empty stacks
  • output is stored with stack limits (max count per resource type), filling existing stacks before creating new ones

This trio forms a loop: conquest generates resources; tech tree unlocks capabilities and bonuses; crafting converts resources into equipment/items; equipment feeds back into missions and progression

3a) Minimal working example: “Character selection → spawn” end-to-end

This is the smallest full path that proves the architecture works:

  • Step 1: player joins: server pings backend (status); server syncs global datasets (ranks/models/equipment); server ensures user exists (steamid auto-registration); server fetches full profile (characters list); server sends user data to client via net message
  • Step 2: player opens character menu (F4): client opens DHTML panel to character selection page; PHP renders character list (join user_character, character, rank, job); JS calls into Lua when user clicks “Enter Game”
  • Step 3: selection becomes a spawn”: client sends selected character ID to server; server fetches targeted spawn data for that character; server applies model/job/money/equipment and spawns player; server broadcasts selected character to all clients for HUD identity overlays

If this path is stable, everything else (conquest, tech, crafting) can be treated as “tools that modify persistence”, while Lua is “presentation + gameplay application”.

3b) Common pitfalls / misconceptions

  • “Just poll the backend frequently, it’ll be fine.”: You’ll create load spikes and race conditions if you don’t cache and you refresh per-player too aggressively.
  • “DHTML is easy, I’ll just build everything in web.”: You still need in-engine rendering, input locking, focus management, and fallbacks when panels close unexpectedly.
  • “One API key is enough.”: A leaked key exposes your endpoints. You need at least per-user secrets or server-side signature validation, plus rate limits.
  • “Database rules are safer than game rules.”: They’re different kinds of safety. DB constraints help consistency, but gameplay rules still need to be enforced server-side in Lua.
  • “Periodic sync keeps everything consistent.”: It helps, but it doesn’t replace event-driven updates. Some changes should push immediately (rank change, equipment change, etc.) to avoid stale UI.

3c) When to use it vs when not to use it

Use this hybrid architecture when:

  • you need persistence across sessions (characters, ranks, economy)
  • you want complex UI quickly (multi-page forms, maps, tech trees)
  • you need admin tools outside the game
  • you want community-wide progression, not just individual stats

Don’t use it when:

  • your server is small and session-local RP is enough
  • you can’t maintain a web backend reliably (downtime will break core flows)
  • you need ultra-low-latency authoritative state from the backend (HTTP + PHP isn’t ideal)
  • you can’t secure endpoints properly (public exposure risk)
Trade-offs : Performance: periodic sync + per-player fetches can be expensive; caching and selective updates matter. Complexity: you’re maintaining two runtimes (Lua + PHP) and a DB schema. DX: debugging becomes “is it Lua, net messages, HTTP, or PHP?” Reliability: backend downtime must be handled, or your server becomes partially unusable.

Takeaways / Opinion

  • A GMod RP server becomes “persistent” only when the database drives identity, not just stats.
  • Separate responsibilities: Lua applies gameplay, PHP owns persistence rules, DHTML gives UI velocity.
  • Cache global datasets (ranks/models/equipment) and treat per-user fetches carefully.
  • The character selection → spawn flow is the backbone; everything else is a system that feeds it.
  • Community systems (conquest + tech + crafting) work best as a tight economy loop, not isolated features.

My opinionated recommendation: If you build something like this, stop thinking of it as “an addon”. Treat it like a small product with an API contract, an auth model, and a failure mode plan. The biggest risk isn’t implementing features—it’s keeping the system stable when the backend is slow, down, or returns partial data.