Southwest Roleplay — FiveM Server
This resource is a TypeScript FiveM port of the Southwest Roleplay game-server (originally written for RageMP). It shares the same database layer, REST API, and feature module structure as the original. The goal is a 1-to-1 feature parity port: one feature at a time, minimal API surface changes.
Table of Contents
- Prerequisites
- Getting Started
- Environment Variables
- Project Structure
- Architecture Overview
- Build System
- Boot Sequence
- Key Systems
- REST API
- Porting a Feature
- RageMP → FiveM Cheatsheet
- Porting Status
Prerequisites
| Tool | Version |
|---|---|
| Node.js | 18+ |
| npm | 8+ |
| MySQL | 8.0 |
| Redis | 6+ |
| FiveM server artifact | build 7290+ |
Getting Started
# 1. Install dependencies
npm install
# 2. Build both server and client bundles
npm run build
# 3. Drop the compiled resource into your FiveM server resources folder
# and add `ensure swrp-fivem-server` to server.cfg
For active development, use watch mode — both bundles rebuild automatically on save:
npm run watch
Lint the TypeScript source:
npm run lint
Environment Variables
All variables have sensible defaults for the dev environment. Override them in your FiveM server.cfg using set convars, or export them in your shell when running outside FiveM.
| Variable | Default | Description |
|---|---|---|
DB_HOST |
play.southwest-roleplay.dev |
MySQL host |
DB_PORT |
3306 |
MySQL port |
DB_USER |
username |
MySQL user |
DB_PASS |
(see database.ts) |
MySQL password |
DB_NAME |
game |
MySQL database name |
REDIS_HOST |
swrp-redis |
Redis host |
REDIS_PORT |
6379 |
Redis port |
REDIS_URL |
(unset) | Full Redis URL (overrides host/port) |
LSRAGE_MONGO_QUEUE |
mongo:jobs |
Redis list key used for the Mongo write queue |
SWRP_UCP_URL |
http://127.0.0.1 |
Comma-separated allowed origins for the REST API CORS policy |
Note: The database does not auto-migrate (
synchronize: false). Schema migrations are managed separately.
Project Structure
fivem-server/
├── fxmanifest.lua # FiveM resource manifest
├── package.json
├── webpack.server.js # Server bundle config (Node target)
├── webpack.client.js # Client bundle config (browser target)
├── gulpfile.js # Orchestrates webpack via gulp
├── tsconfig.server.json
├── tsconfig.client.json
├── ormconfig.js # TypeORM CLI config (uses compiled entities)
├── PORTING.md # Feature porting checklist and guide
│
├── src/
│ ├── server/
│ │ ├── index.ts # Entry point — boot sequence
│ │ ├── core/
│ │ │ ├── mp-shim.ts # Global `mp` object (RageMP compat layer)
│ │ │ ├── database.ts # TypeORM / MySQL
│ │ │ ├── redis.ts # Redis client
│ │ │ ├── mongo.ts # Mongo write queue (via Redis list)
│ │ │ ├── preload.ts # Eager DB cache (game entity types)
│ │ │ ├── feature-system.ts # Base class for all feature modules
│ │ │ ├── commands/ # Command system + guards
│ │ │ ├── log/ # Structured logging + log events
│ │ │ ├── rpc/ # Bidirectional client ↔ server RPC
│ │ │ ├── player/ # PlayerWrapper + player registry
│ │ │ └── vehicle/ # VehicleWrapper + vehicle registry
│ │ ├── entities/ # TypeORM entities (207 tables)
│ │ ├── features/ # Feature modules (one folder per feature)
│ │ └── rest/ # Express REST API (external service hooks)
│ │
│ ├── client/
│ │ ├── index.ts # Client entry point
│ │ ├── core/
│ │ │ ├── mp-shim.ts # Client-side `mp` compat layer
│ │ │ └── rpc/ # Client RPC (call server procs)
│ │ └── features/ # Ported client features
│ │
│ └── shared/
│ └── const/ # Constants shared between server and client
│
├── dist/ # Compiled output (gitignored, produced by build)
│ ├── server/index.js
│ └── client/index.js
│
└── ui/ # Pre-built Vue NUI (served as the resource's ui_page)
Architecture Overview
┌──────────────────────────────────────────────────┐
│ FiveM Server │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ mp shim │ │ FeatureSystem modules │ │
│ │ (global mp) │ │ (player, login, chat…) │ │
│ └──────┬───────┘ └──────────────────────────┘ │
│ │ │
│ ┌──────▼───────┐ ┌──────────────────────────┐ │
│ │ CommandSystem│ │ REST API (Express :3001) │ │
│ └──────────────┘ └──────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ MySQL │ │ Redis │ │ Mongo queue │ │
│ │ (TypeORM)│ │ (cache) │ │ (via Redis) │ │
│ └──────────┘ └──────────┘ └────────────────┘ │
└──────────────────────────────────────────────────┘
▲ net events / RPC ▼
┌──────────────────────────────────────────────────┐
│ FiveM Client │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ mp shim │ │ Feature modules │ │
│ │ (global mp) │ │ (login, vehicle, …) │ │
│ └──────────────┘ └────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ NUI (Vue — ui/index.html) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
The mp compatibility shim
Both server and client expose a global mp object that mirrors the RageMP API. This means ported feature files compile and run with minimal changes. Functions that have no FiveM equivalent are stubbed and log a console.warn so they are easy to find.
Key mappings:
| Concept | RageMP | FiveM (via shim) |
|---|---|---|
| Player state | player.setVariable(k, v) |
FiveM StateBag |
| Server → client event | player.call('event', [...]) |
TriggerClientEvent |
| Client → server event | mp.events.callRemote('event') |
TriggerServerEvent |
| Server proc (RPC) | mp.events.addProc('name', fn) |
rpc.register('name', fn) |
| Config values | mp.config.key |
GetConvar('key', ...) |
Build System
Webpack bundles the TypeScript source into two CommonJS files that FiveM loads at runtime:
| Bundle | Entry | Output | Notes |
|---|---|---|---|
| Server | src/server/index.ts |
dist/server/index.js |
target: node; npm deps are external |
| Client | src/client/index.ts |
dist/client/index.js |
Runs inside FiveM's V8 sandbox |
npm packages are left as external require() calls in the server bundle — FiveM resolves them from the resource's node_modules/ directory at runtime.
Source maps are emitted alongside each bundle for stack-trace readability.
Boot Sequence
src/server/index.ts orchestrates startup in this order:
mp-shim— must be the first import; builds the globalmpobject before anything else.databaseSystem.init()— opens the MySQL connection (retries up to 5 times).mongoSystem.init()— verifies the Redis-backed Mongo write queue is available.redisSystem.init()— connects to Redis and flushes all keys.preloadSystem.init()— eager-loads game entity types from the DB into memory.- Parallel features —
featuresParallelarray; allinit()calls run concurrently. - Series features —
featuresSeriesarray; eachinit()runs in order (use for features with dependencies). postInitpass — optional second-pass for cross-feature wiring.commandSystem.init()— collects commands from all features and registers them with FiveM'sRegisterCommand().
To add a newly-ported feature to the boot sequence, import it in src/server/index.ts and push it onto featuresParallel or featuresSeries.
Key Systems
FeatureSystem
Every feature extends FeatureSystem:
import { FeatureSystem } from '../../core/feature-system';
class MyFeature extends FeatureSystem {
commands = [MyCommand]; // optional — registers with CommandSystem
async init(): Promise<void> {
await super.init();
// set up event handlers, load DB data, etc.
}
async postInit(): Promise<void> {
// cross-feature wiring that requires other features to be initialised
}
}
export const myFeature = new MyFeature();
CommandSystem
Commands are classes that extend BaseCommand. They declare their name, parameters, guards, and a run() method. The system validates parameters automatically and runs guards before run().
Guards live in src/server/core/commands/guards/ and return true on success or an error string on failure. Available guards:
PermissionsCommandGuard— checks player permission groupPlayerStateCommandGuard— checks alive/dead statePrisonCommandGuard— blocks commands while imprisonedNoSpectateCommandGuard— blocks commands while spectatingPhoneCommandGuard— requires a phoneVehicleCommandGuard— requires the player to be in a vehicle
RPC System
Two-way remote procedure calls between server and client, replacing mp.events.addProc / mp.events.callRemoteProc.
Server registers a handler:
import { rpc } from '../core/rpc';
rpc.register('getPlayerInfo', async (player, ...args) => {
return { name: player.name };
});
Client calls the server:
import { rpc } from '../core/rpc';
const info = await rpc.call('getPlayerInfo');
Server calls a client proc:
const result = await rpc.callClient(player, 'getPedPosition');
The wire protocol uses two net events (_rpc:call / _rpc:result) and auto-times out client calls after 10 seconds.
Player Registry
PlayerWrapper wraps a FiveM net ID and exposes a RageMP-compatible player API (.name, .id, .notify(), .call(), .setVariable(), .getVariable(), etc.). The registry is maintained by the mp-shim on playerConnecting / playerDropped.
REST API
An Express server starts on port 3001 alongside the FiveM resource. It is used by external services (UCP, Discord bots, admin tooling) to interact with the live game server.
All requests must include an rpckey header matching a key stored in the rpc_keys database table. Optionally the key can be IP-restricted to a CIDR range.
Routes
| Prefix | Module | Purpose |
|---|---|---|
/account |
AccountRpcModule | Bans, admin notes, session invalidation |
/character |
CharacterRpcModule | Spawn items/money, refunds, in-game checks |
/chat |
ChatRpcModule | Post messages to in-game chat channels |
/company |
CompanyRpcModule | Manage company employees and metadata |
/faction |
FactionRpcModule | Faction creation / kick events |
/group |
GroupRpcModule | Permission group and frequency changes |
/motd |
MotdRpcModule | Push new MOTD / force reload |
/players |
PlayersRpcModule | Online player list / admin list |
/property |
PropertyRpcModule | Reload property parameters |
/vehicle |
VehicleRpcModule | Reload vehicle parameters, spawn checks |
Porting a Feature
See PORTING.md for the full guide and suggested porting order. The short version:
Server feature
- Copy
game-server/src/features/<name>/→fivem-server/src/server/features/<name>/ - Fix relative imports (
../../entities/,../../core/paths stay the same). - Replace
mp.colshapes.*with the polygon position-tracking system. - Replace
mp.objects.new/mp.peds.newwith FiveMCreateObject/CreatePednatives. player.call(...)works unchanged viaPlayerWrapper.call().- Uncomment the import in
src/server/index.tsand add tofeaturesParallelorfeaturesSeries.
Client feature
- Copy
game-client/src/<name>/→fivem-server/src/client/features/<name>/ - Fix imports.
- Replace
mp.events.addProc('name', fn)withrpc.registerClientProc('name', fn). - Replace
mp.browsers.new(url).execute(js)withnui.send(type, data)(PostMessage to Vue). - Replace
mp.game.invoke('0xHASH', ...)withCitizen.invokeNative('0xHASH', ...)or the named native. - Uncomment the import in
src/client/index.ts.
RageMP → FiveM Cheatsheet
| RageMP (server) | FiveM (server) |
|---|---|
mp.events.add('playerJoin', fn) |
Shim maps → playerConnecting |
mp.events.add('playerQuit', fn) |
Shim maps → playerDropped |
mp.events.add('myEvent', fn) |
Shim registers net event; player injected from source |
player.call('event', [a, b]) |
TriggerClientEvent('event', netId, a, b) |
player.notify(msg) |
TriggerClientEvent('_mp:notify', netId, msg) |
player.setVariable(k, v) |
FiveM StateBag: Player(id).state.set(k, v, true) |
mp.vehicles.new(model, pos, opts) |
vehicleRegistry.create(model, pos, opts) |
mp.events.addProc('name', fn) |
rpc.register('name', fn) |
mp.colshapes.newSphere(...) |
Polygon position-tracking system |
mp.players.broadcast(msg) |
TriggerClientEvent('_mp:chatMessage', -1, msg) |
| RageMP (client) | FiveM (client) |
|---|---|
mp.events.add('render', fn) |
Shim adds to tick loop |
mp.events.callRemote('event', ...args) |
TriggerServerEvent('event', ...args) |
mp.events.addProc('name', fn) |
rpc.registerClientProc('name', fn) |
mp.players.local.setComponentVariation(...) |
SetPedComponentVariation(PlayerPedId(), ...) |
mp.game.invoke('0xHASH', ...args) |
Citizen.invokeNative('0xHASH', ...args) |
mp.browsers.new(url) |
FiveM NUI (ui/index.html) |
mp.gui.chat.push(msg) |
TriggerEvent('chat:addMessage', { args: [msg] }) |
Porting Status
| Layer | Status | Notes |
|---|---|---|
| TypeORM entities (207) | Done | Copied verbatim, no changes needed |
| Express REST API | Done | Copied verbatim, no changes needed |
| Shared constants | Done | |
| Core log / DB / Redis / Mongo | Done | |
mp server shim |
Done | Events, players, vehicles, config |
mp client shim |
Done | Events, game, gui, NUI |
| RPC system | Done | Replaces mp.events.addProc |
| Player wrapper | Done | Wraps FiveM net ID into PlayerMp-compatible object |
| Vehicle wrapper | Done | Wraps FiveM entity handle |
| CommandSystem | Done | Uses RegisterCommand() per command |
| Server features (101) | Not started | See PORTING.md for suggested order |
| Client features (~80) | Not started | See PORTING.md for suggested order |
The recommended first features to port (in order) are: time, weather, chat, login, session, player, permissions, money, inventory, vehicle.
Defer features with heavy colshape or server-object dependencies (properties, furniture, drugs, graffiti) until the polygon position-tracking system is fully integrated.