Remote operator dashboard for FiveM / multiplayer servers.
fivem-watch moves live server monitoring out of the game and into a web dashboard. Operators can inspect player state, map position, and live session context remotely instead of manually joining the server and spectating players in-game.
Running a live multiplayer server often means operators need to answer questions quickly:
- Where is this player?
- What is happening around them?
- Is the player alive, moving, damaged, or suspicious?
- Do I need to enter the game just to check context?
The usual workflow is manual: join the game, teleport/spectate, observe, then switch back to admin tooling.
fivem-watch replaces that with a remote operator console:
- player telemetry is pushed to a backend control plane
- operators view players on a live map
- live session context can be streamed on demand
- expensive frame capture only runs while someone is watching
The goal is simple:
give operators live context without forcing them into the game.
- Tracks player state in near real time
- Renders player positions on a GTA V map
- Streams live player session context to a browser dashboard
- Starts capture only when an operator is watching
- Stops capture automatically when no watcher remains
- Supports multiple operators watching the same player
- Keeps latest runtime state in memory for low-dependency deployment
- Runs as a self-hosted backend + dashboard + FiveM resource
FiveM Server Resource
├─ collects player snapshots
├─ sends telemetry to backend
└─ controls NUI capture
↓ HTTP / Socket.io
Backend Control Plane
├─ auth / health / ingest endpoints
├─ Socket.io event router
├─ player state cache
├─ NUI socket index
└─ watcher-scoped stream relay
↓ WebSocket
Operator Dashboard
├─ player list
├─ live map
├─ stream controls
└─ live session viewerThe backend is the routing authority.
The FiveM resource emits state and frames.
The dashboard consumes state and controls streams.
The backend decides who receives what.
A naive implementation would broadcast every captured frame to every connected admin.
That wastes bandwidth and browser resources.
fivem-watch routes frames only to operators watching that specific player:
NUI frame for player A
↓
backend checks streamWatchers[playerA]
↓
relay only to admins watching player AWhen the last watcher leaves:
watcher count = 0
↓
backend emits stop_capture
↓
NUI stops frame captureThis keeps stream cost proportional to real demand.
FiveM runtime package.
Responsibilities:
- collect player snapshots
- send telemetry to the backend
- bootstrap hidden NUI capture context
- capture WebGL frames on demand
- emit frame payloads over Socket.io
Backend control plane.
Responsibilities:
- authenticate dashboard and ingest clients
- receive player telemetry snapshots
- keep latest player state in memory
- track admin and NUI sockets
- start/stop capture sessions
- relay frames only to active watchers
React operator dashboard.
Responsibilities:
- render player list and state
- display players on the map
- start/stop live streams
- show live session context
- expose basic stream controls
Frontend: React, Vite, Leaflet
Backend: Node.js, Express, Socket.io
Runtime: FiveM resource, Lua/JS, NUI WebGL capture
State: In-memory runtime indexes
Deploy: Self-hosted Node.js backend + static dashboard.
├─ client/ # React + Vite operator dashboard
├─ server/ # Express + Socket.io control plane
├─ fivem-watch-resource/ # FiveM runtime package
├─ INSTALL.md # installation and operations guide
├─ TECHNICAL-ARCHITECTURE.md
├─ CHANGELOG.md
└─ VERSIONING.mdcd server
cp .env.example .env
npm install
npm startExample server/.env:
PORT=3001
API_SECRET=CHANGE_ME_TO_A_RANDOM_SECRET
ADMIN_USERNAME=admin
ADMIN_PASSWORD=CHANGE_ME
CORS_ORIGIN=http://localhost:5173cd client
npm install
npm run devOptional client/.env:
VITE_SERVER_URL=http://YOUR_SERVER_IP:3001Copy fivem-watch-resource/ into your FiveM resources/ directory.
Update fivem-watch-resource/config.js:
const FW_CONFIG = {
BACKEND_URL: 'http://YOUR_SERVER_IP:3001',
API_SECRET: 'MATCH_SERVER_ENV_API_SECRET',
TELEMETRY_INTERVAL: 1000,
STREAM_FPS: 20,
STREAM_QUALITY: 0.5,
STREAM_RESOLUTION_SCALE: 0.5,
};Add to server.cfg:
ensure fivem-watchIf the folder is named
fivem-watch-resource, rename it tofivem-watchor update the NUI paths infxmanifest.luaandweb/index.html.
1. FiveM server posts player snapshots to POST /api/ingest.
2. Backend validates the shared secret and updates latest player state.
3. Backend emits players_update to authenticated operators.
4. Operator clicks watch on a player.
5. Backend sends start_capture to that player's NUI client.
6. NUI captures frames and emits them to the backend.
7. Backend relays frames only to watchers of that player.
8. When no watcher remains, backend sends stop_capture.POST /api/auth/login
GET /api/health
POST /api/ingestPOST /api/ingest requires:
x-api-key: <server-secret>Example ingest payload:
[
{
"id": 12,
"name": "player_name",
"coords": {
"x": 123.4,
"y": 456.7,
"z": 21.0
},
"health": 190,
"armor": 50,
"ping": 42,
"heading": 180
}
]admin
fivem-server
fivem-nuistart_stream(playerId)
stop_stream(playerId)
update_stream_config({ playerId, config })players_update(players)
player_frame({ playerId, frame })
stream_error({ playerId, error })
server_offlinestart_capture
stop_capture
update_config| Key | Default | Description |
|---|---|---|
BACKEND_URL |
http://localhost:3001 |
Control plane base URL |
API_SECRET |
CHANGE_ME_TO_A_RANDOM_SECRET |
Shared auth secret |
TELEMETRY_INTERVAL |
1000 |
Snapshot interval in ms |
STREAM_FPS |
20 |
Capture frame rate target |
STREAM_QUALITY |
0.5 |
Image encoder quality |
STREAM_RESOLUTION_SCALE |
0.5 |
Capture resolution multiplier |
| Key | Default | Description |
|---|---|---|
PORT |
3001 |
HTTP + websocket port |
API_SECRET |
CHANGE_ME_TO_A_RANDOM_SECRET |
Shared secret across roles |
ADMIN_USERNAME |
admin |
Login identity |
ADMIN_PASSWORD |
CHANGE_ME |
Login credential |
CORS_ORIGIN |
http://localhost:5173 |
Allowed browser origin |
Before exposing this outside a trusted development network:
- replace all default credentials and secrets
- restrict
CORS_ORIGINto explicit dashboard origins - serve dashboard and API through HTTPS/TLS termination
- keep backend behind a reverse proxy or trusted network boundary
- avoid exposing ingest endpoints publicly unless required
- add process supervision with PM2, systemd, or containers
- pin Node.js runtime and dependency versions in CI/CD
- rotate
API_SECRETif the resource package is shared
| Symptom | Likely cause |
|---|---|
| No players in dashboard | Backend unreachable from FiveM host or API_SECRET mismatch |
| Stream does not start | Target NUI socket is not connected |
| Browser login/network error | Incorrect VITE_SERVER_URL or CORS_ORIGIN |
| Map appears empty | Tile set missing under client/public/styleSatelite/ |
| Stream starts but freezes | NUI capture stopped, player disconnected, or socket dropped |
fivem-watch intentionally starts with a simple single-node model.
Current trade-offs:
- runtime state is kept in memory
- no mandatory database
- no historical replay
- no distributed media relay
- shared-secret auth is used for controlled self-hosted deployments
This keeps setup simple for the target audience.
For larger deployments, see TECHNICAL-ARCHITECTURE.md for scaling and hardening paths.
Contributions are welcome.
For non-trivial changes, please include:
- the problem being solved
- architectural impact
- test or verification notes
- documentation updates for changed behavior or contracts
MIT. See LICENSE.