Technology Stacks¶
MIP is built across two distinct environments — the Unreal Engine game client/server and the Node.js backend — connected by a common communication layer. This page catalogues every major technology in the stack, what role it plays, and how the pieces fit together.
Overview¶
┌──────────────────────────┐ ┌──────────────────────────┐
│ GAME CLIENT (UE5) │ │ DEDICATED SERVER (UE5) │
│ VaRest ──► REST API │ │ Socket.IO (role=server) │
│ Socket.IO (role=gc) │ │ Agones SDK (health/rdy) │
└────────────┬─────────────┘ └────────────┬─────────────┘
│ │
└──────────────┬───────────────────┘
▼
┌─────────────────────┐
│ NestJS Backend │
│ Port 3000 │
└─────────┬───────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
MongoDB Redis Kubernetes API
(data) (cache/lock/ (Agones fleet +
pub-sub) allocation)
Unreal Engine 5¶
Version: UE 5.7 Targets: Windows (client), Linux (dedicated server)
The game is split into two build targets:
- Client — runs the player's game, handles rendering, input, UI, and Socket.IO/REST communication with the backend.
- Dedicated Server — runs the authoritative game world, loads player data from the backend on join, and writes data back on logout or at timed intervals.
Key UE-side integrations¶
| Integration | Purpose |
|---|---|
| VaRest | HTTP plugin used for REST calls (login, register) |
| Socket.IO Client (UE) | WebSocket plugin; game client connects as role=gc, game server as role=server |
| Gameplay Ability System (GAS) | Abilities, attributes, gameplay effects, status effects |
| Enhanced Input | Input mapping and context management |
| Agones SDK (UE) | Game server lifecycle reporting from inside UE (health pings, ready/shutdown signals) |
Custom Control Channel¶
MIP replaces the default Unreal Engine control channel with UMIPControlChannel to intercept the NMT_Login handshake and verify the single-use JWT before admitting any player connection. This is registered in DefaultEngine.ini:
[/Script/OnlineSubsystemUtils.IpNetDriver]
!ChannelDefinitions=ClearArray
+ChannelDefinitions=(ChannelName=Control, \
ClassName=/Script/ModularInventoryPlus.MIPControlChannel, \
StaticChannelIndex=0, bTickOnCreate=true, \
bServerOpen=false, bClientOpen=true, \
bInitialServer=false, bInitialClient=true)
Info
JWT verification is skipped when WITH_EDITOR is defined, so Play-In-Editor works without a running backend.
NestJS¶
Runtime: Node.js Language: TypeScript Port: 3000
NestJS is the framework for the MIP backend (mip-be). It provides structured modules, dependency injection, guards, and middleware that map cleanly to the backend's responsibilities: REST API, WebSocket gateway, Kubernetes integration, and data persistence.
Modules¶
| Module | Responsibility |
|---|---|
UsersModule |
Registration, login, JWT issuance, character selection data |
CharactersModule |
Character CRUD and family/character relationships |
FamilyModule |
Family (account) creation and lookup |
SaveModule |
Item read/write, saveable object persistence |
SocketIoModule |
Socket.IO gateway, event handlers, session routing |
PlayersModule |
Player session state and routing logic |
FriendsModule |
Friend requests, accept/reject, whisper routing |
KubernetesModule |
Agones fleet creation, GameServer allocation, watch loop |
RedisCacheModule |
Redis client, Redlock, pub/sub, session key helpers |
AuthorizationModule |
JWT strategy, guards, global auth pipe |
REST API¶
The API is prefixed with /api. Key endpoints:
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/users/register |
None | Create a new account |
POST |
/api/users/login |
None | Authenticate; returns session JWT |
POST |
/api/users/login/editor |
None | Editor-only login (skips some checks) |
GET |
/api/users/me |
Bearer JWT | Return character selection data |
GET |
/api/users/me/editor |
Bearer JWT | Editor variant of above |
POST |
/api/users/editor-join-token |
Bearer JWT | Generate a join token for PIE |
Authentication¶
All authenticated REST endpoints use Passport + JWT strategy. The @NoAuth() decorator exempts public endpoints (register, login) from the global auth guard.
Login flow:
1. POST /api/users/login validates credentials via LocalAuthGuard (username/password).
2. On success, UsersService.login() calls JwtService.sign() with the user payload.
3. The returned JWT is used as a Bearer token for all subsequent REST calls and as the Authorization credential when opening the Socket.IO connection.
Socket.IO¶
Library: socket.io v4 (server), matching UE client plugin
Adapter: @socket.io/redis-adapter (horizontal scaling via Redis)
Socket.IO handles all real-time game communication. Two distinct roles connect to the same gateway:
| Role | Token | Connected by |
|---|---|---|
gc (game client) |
Player session JWT | UE game client after login |
server |
Server JWT (embedded in container annotations) | UE dedicated server on startup |
The Redis adapter allows multiple backend instances to share the same Socket.IO event bus — a message sent from one backend pod is received by all other pods and forwarded to the correct socket.
Key event categories¶
Client → Backend
| Event | Payload | Effect |
|---|---|---|
character_selection |
— | Returns account, family, character list |
join_game |
{ characterId } |
Validates character, resolves map, issues single-use JWT, emits gate_travel |
allocate_dungeon |
{ mapName, additionalJsonString } |
Calls Agones allocation API, adds player to awaiting list |
quit_dungeon |
— | Routes player back to previous persistent area |
chat |
{ message, channel } |
Validates and broadcasts to channel |
friend_request / accept_friend / reject_friend / unsend_friend |
Friend identifiers | Friend management |
Server → Backend
| Event | Payload | Effect |
|---|---|---|
im_ready |
Session info | Server signals readiness; backend dispatches awaiting players |
load_player_server |
Character info | Backend loads and emits player data (items, currencies, saveables, temp objects) |
write_items |
Item/saveable data + save tag | Backend persists data to MongoDB under distributed lock |
gate_travel |
Map name | Routes a player to a new server |
verify_join_game_token |
JWT | Backend validates single-use token; returns 1 or 0 |
session_pong |
— | Health check response |
Backend → Client / Server
| Event | Recipient | Purpose |
|---|---|---|
gate_travel |
Client | Provides server URL + single-use JWT for map travel |
read_items |
Server | Character and family storage items |
read_currencies |
Server | Character and family currencies |
read_savable_objects |
Server | Quests, abilities, and other saveable components |
read_temp_duration_objects |
Server | Cooldowns, temporary effects |
chat |
Client(s) | Incoming chat message |
global_notice |
All clients | Server-wide announcement |
enhance_notice |
All clients | Equipment enhancement broadcast |
friend_request / accept_friend / reject_friend |
Client | Friend event delivery |
MongoDB¶
Driver: Mongoose v6 (@nestjs/mongoose)
Default port: 27017
Container: mongo:latest (mip_mongo_db_local)
MongoDB is the primary persistent store for all player data. The schema is document-oriented, which maps naturally to MIP's JSON-serialized game data.
What is stored¶
| Collection | Contents |
|---|---|
| Users | Account credentials (hashed with bcrypt), userId |
| Characters | Character data, class, lastAreaMap, lastTransform |
| Families | Family (account-wide) name and character list |
| Items | Per-character and per-family inventory documents |
| Saveable objects | Quests, abilities, and any SaveGame-marked component data |
| Temp duration objects | Cooldowns, timed effects |
| Currencies | Character and family currency values |
| Friends | Friend relationships and pending requests |
Data format¶
All game component data is serialized to JSON (matching Unreal's SaveGame property serialization). This means adding a new persisted property to a component requires only marking it SaveGame — the framework serializes and deserializes it automatically.
Redis¶
Client: ioredis v5
Distributed lock: Redlock v4
Default port: 6379
Container: redis:latest (mip_redis_local)
Redis serves three distinct roles in MIP:
1 — Session Store¶
All live session state is held in Redis, not MongoDB. This keeps lookups fast and avoids hitting the database on every Socket.IO event. Redis keys are namespaced by function (see the key namespace reference).
2 — Distributed Lock (Redlock)¶
When a game server writes player data (write_items), the backend acquires a Redlock distributed lock before reading from and writing to MongoDB. This prevents race conditions when a player disconnects and reconnects rapidly, or when two save events arrive simultaneously.
Two lock configurations are used:
| Instance | Retry count | Retry delay | Use case |
|---|---|---|---|
redlock |
10 | 100 ms | Normal save/load operations |
noRetryRedlock |
0 | — | Fire-and-forget locks (e.g., one-time token consumption) |
3 — Pub/Sub Bus¶
Redis pub/sub connects backend instances and decouples the Kubernetes watch loop from the Socket.IO gateway. When the Kubernetes watcher detects a GameServer transitioning to Ready, it publishes to the gameservers_ready channel. The Socket.IO service subscribes and dispatches travel events to awaiting players.
Docker¶
Compose file: docker-compose.yml
Local image: mip-be:local (built by scripts/deploy-docker-local.bat)
Docker packages both the backend and the game server binary into reproducible images. The local compose stack brings up three containers:
| Container | Image | Port |
|---|---|---|
mip_backend_local |
mip-be:local |
3000 |
mip_redis_local |
redis:latest |
6379 |
mip_mongo_db_local |
mongo:latest |
27017 |
The local backend image is built in two stages (Dockerfile-local):
1. Builder — installs all dependencies and compiles TypeScript to dist/.
2. Runtime — copies only dist/, production node_modules, and required runtime files (KUBECONFIG-LOCAL, .env.local, yamls/).
Kubernetes + Agones¶
Local: Minikube
Production/Staging: K3S
Agones version: agones.dev/v1
K8s client: @kubernetes/client-node v0.20
The KubernetesService connects to the cluster using an embedded kubeconfig (the KUBE_CONFIG_PATH env variable). On startup it:
- Creates the
mip-server-fleetFleet (if it does not already exist). - Creates the
FleetAutoscalerfor automatic buffer management. - Opens a long-lived watch on the
gameserversresource in thedefaultnamespace. When aGameServertransitions toReady, it publishes to Redis.
Fleet configuration (local)¶
apiVersion: "agones.dev/v1"
kind: Fleet
metadata:
name: mip-server-fleet
spec:
replicas: 1
template:
spec:
ports:
- name: game
portPolicy: Dynamic
containerPort: 7777
protocol: UDP
- name: beacon
portPolicy: Dynamic
containerPort: 12345
protocol: UDP
template:
spec:
containers:
- name: mip-server
image: docker.io/library/mip-server:latest
imagePullPolicy: IfNotPresent
Dungeon allocation¶
When a player requests a dungeon, KubernetesService.allocateDungeon() patches the allocation YAML with the session ID, map name, and a signed JWT, then calls:
k8sCustomObjectsApi.createNamespacedCustomObject(
'allocation.agones.dev', 'v1', 'default', 'gameserverallocations', resource
)
Agones atomically selects a Ready server from the fleet, transitions it to Allocated, and injects the annotations as environment variables into the running container.
JWT (JSON Web Tokens)¶
Library: jsonwebtoken v9 / @nestjs/jwt
Secret: JWT_SECRET environment variable
MIP uses three distinct JWT types:
| Type | Issued by | TTL | Purpose |
|---|---|---|---|
| Session JWT | Backend on login | Long-lived | Client auth for REST + Socket.IO |
| Server JWT | Backend on allocation | Long-lived | Game server identity with Socket.IO |
| Single-use join JWT | Backend on join_game |
120 s | One-time gate travel authorization |
The single-use join JWT is double-protected: it has a cryptographic expiresIn and its playerSessionId claim must be present in Redis. Verification deletes the Redis key immediately, so the token cannot be reused even if intercepted.
Stack Summary¶
| Layer | Technology | Version |
|---|---|---|
| Game engine | Unreal Engine | 5.7 |
| Backend framework | NestJS | 8.x |
| Language | TypeScript | 4.x |
| Database | MongoDB | 6 (Mongoose) |
| Cache / lock / pub-sub | Redis + Redlock | ioredis 5, Redlock 4 |
| Real-time transport | Socket.IO | 4.x |
| Containerization | Docker | — |
| Orchestration (local) | Minikube | — |
| Orchestration (prod) | K3S | — |
| Game server management | Agones | v1 |
| HTTP client (UE) | VaRest | — |
| Auth | JWT (Passport) | jsonwebtoken 9 |
| Password hashing | bcrypt | 5.x |