…and why I kept an Unreal Sessions fallback if server costs don’t scale with the player base

1) Introduction

SuperTag matches are currently hosted by players (listen servers). The question wasn’t “how do I network gameplay” — Unreal already does that. The real problem was everything around the match: player profiles, queueing, session discovery by join code, and eventually a path to dedicated servers.

So I built a thin “control plane” on top:

  • Unreal (Blueprint-only) = gameplay, UI, travel, hosting
  • Node.js API = player data, matchmaking, session registry
  • MongoDB = persistence

But I didn’t fully commit to the backend as a hard dependency. I intentionally kept my old Blueprint code that uses Unreal’s Sessions system. If backend costs aren’t justified by the actual player base, I can revert discovery/matchmaking to Sessions with minimal disruption.

This post explains how that hybrid decision is structured and why it matters.

2) Context: Why I Split It Like This (and why I kept a fallback)

I didn’t want the client to own:

  • matchmaking rules
  • player data persistence
  • “join by code” discovery
  • future dedicated server orchestration

At the same time, paying for always-on API + DB + logs + uptime doesn’t always make sense early. If you have a small player base, the simplest solution is often “good enough”:

  • Unreal Sessions discovery
  • player-hosted listen servers
  • minimal backend (or none)

So the design constraint became:

  • Build the Node.js path for control and future scalability
  • Keep the Sessions path alive as a reversible plan B

That only works if the client doesn’t hardcode assumptions. The API is optional plumbing, not the foundation of gameplay networking.

3) My Actual Architecture (with the “Plan B” path)

                (Plan A)
[Unreal Client]  --->  [Node.js API]  --->  [MongoDB]
     |
     | (Plan B / fallback)
     ---> [Unreal Sessions Discovery]

Plan A gives me:

  • persistent profiles
  • queueing
  • join codes
  • clean "match to join" returns
  • dedicated-server-ready shape

Plan B gives me:

  • near-zero backend cost
  • fewer operational moving parts
  • quick revert if economics don’t work

4) Player Login / Initialization (API-only)

This part stays API-driven because it’s where persistence matters most.

Endpoint

GET /api/player/:steamId

Server behavior:

  • find by steamId
  • create if missing
  • return profile JSON

Client behavior (Blueprint):

  • call endpoint on startup
  • parse JSON
  • initialize UI/state

If I ever go full “no backend”, this part becomes the main casualty (you lose persistence unless you replace it with Steam stats/Cloud or local save). That’s a business decision, not a technical one.

5) Matchmaking / Discovery (Two paths)

5.1 Plan A: API-driven matchmaking (current direction)

The key is: the client only wants a connection target.

Endpoints

POST /api/match/join
GET  /api/match/status/:steamId

Status response when found:

{
  "found": true,
  "isHost": true,
  "matchId": "abc123-efgh456-ijkl789"
}

Blueprint behavior:

  • POST join
  • poll every 3–5s
  • if we find a match then we check if we are the host
  • if we are the host then we open the level
  • if we are not the host then we connect to the host by searching for the matchId in existing sessions

This stays valid even when hostType becomes "dedicated" later. The client doesn’t change.

5.2 Plan B: Unreal Sessions system (kept in Blueprint)

This is the older code path that can be re-enabled if Node.js + MongoDB isn’t worth the monthly cost relative to player count.

Typical Session flow (Blueprint-only):

Host:

  • Create Session (listen server)
  • Advertise session properties (map, mode, etc.)
  • OpenLevel with ?listen

Join:

  • Find Sessions
  • Filter results (optional)
  • Join Session

A rough “diagram list” version:

Host:
  CreateSession → OpenLevel(Map?listen)

Client:
  FindSessions → (filter) → JoinSession

This path is cheaper operationally, but you lose:

  • strong matchmaking rules
  • join-code registry (unless you hack it into session settings)
  • centralized queue logic
  • easy dedicated server migration

It’s still a valid fallback when the game is early.

6) Open World Hosting (API registry + heartbeat)

Open world is player-hosted, and the API acts like a directory.

Endpoints

POST /api/match/openworld/host
POST /api/match/openworld/heartbeat

This gives you join codes and prevents “dead lobbies” with heartbeat cleanup.

If reverting to Sessions, open world discovery can be replaced by:

  • FindSessions with a specific keyword/tag
  • or exposing session settings as “join code”-like fields (less clean, but workable)