Guides · Netcode for GameObjects

Player accounts and a database for Netcode for GameObjects

Netcode for GameObjects runs your gameplay. Your BackendGen.dev backend runs everything around it: player sign-in, saved player data, leaderboards and cloud saves, on your own server. This guide uses NGO's connection approval, so your server knows exactly who each player is before it lets them in.

How it fits together

Players never tell your Netcode server who they are: your backend does. The whole handoff is four steps:

  1. Game → your backendSign in. The player signs in with their gamertag and gets a session token (it lasts 30 days).
  2. Game → Netcode serverConnect and send the token once, through connection approval.
  3. Netcode server → your backendCheck. POST /api/server/verify-session with the server's own key. The backend answers with the real gamertag, or says the token isn't valid.
  4. Netcode serverLet in, or turn away: response.Approved = true with the confirmed name, or response.Approved = false.
POST /api/server/verify-session      X-Server-Key: ubgs1.xxxx.yyyy
{ "token": "<the player's session>" }

-> { "success": true, "valid": true, "player": "Alice", "expiresAt": 1796222795561 }
-> { "success": true, "valid": false }          (made up, expired or from another backend)

Your backend never touches the gameplay traffic, and Netcode for GameObjects never sees a password. With the Advanced licence, step 1 can also hand out a one-use join ticket for a particular server instead.

The steps

  1. Sign the player in

    Before connecting to the game server, sign in with the client that came with your backend:

    using UniversalBackendGenerator.Network;
    
    // your sign-in screen
    MyGameClient.Instance.SignIn(gamertag, null, (ok, message) =>
    {
        if (ok) BackendConnectionApproval.JoinAsClient();   // sends the session as connection data
        else Debug.LogWarning("Sign-in failed: " + message);
    });

    Replace MyGameClient with your generated client's class name: it's your project name plus Client, e.g. StarRaidersClient.

  2. Give your server a key

    Your game server proves who it is with a server key that may only do what you allow. Make one with the sessions scope. With the Advanced licence: admin dashboard > Server keys. With any licence, from a terminal:

    curl -X POST https://api.yourgame.com/api/admin/server-keys \
      -H "Authorization: Bearer YOUR_ADMIN_PASSWORD" \
      -H "Content-Type: application/json" \
      -d '{"name":"Game servers","scopes":["sessions"]}'

    Or in PowerShell:

    (Invoke-RestMethod -Method Post -Uri https://api.yourgame.com/api/admin/server-keys `
      -Headers @{ Authorization = "Bearer YOUR_ADMIN_PASSWORD" } -ContentType "application/json" `
      -Body '{"name":"Game servers","scopes":["sessions"]}').key

    The key (ubgs1.…) is shown once. Give it to your server as environment variables when it starts:

    UBG_BACKEND_URL=https://api.yourgame.com UBG_SERVER_KEY=ubgs1.xxxx.yyyy ./MyGameServer.x86_64 -batchmode -nographics

    On Windows (PowerShell):

    $env:UBG_BACKEND_URL = "https://api.yourgame.com"
    $env:UBG_SERVER_KEY = "ubgs1.xxxx.yyyy"
    .\MyGameServer.exe -batchmode -nographics

    Never put the key (or your admin password) in a build that players download: anyone could pull it out.

  3. Add the session check

    BackendSessionCheck.cs

    // BackendSessionCheck.cs - on your GAME SERVER only (a headless Unity build).
    // Asks your BackendGen.dev backend who a player's session token belongs to.
    // Needs a server key with the "sessions" scope. Give the server its settings as environment variables:
    //   UBG_BACKEND_URL=https://api.yourgame.com   UBG_SERVER_KEY=ubgs1.xxxx.yyyy
    // Never put the server key in a game build that players download.
    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using UnityEngine;
    
    public static class BackendSessionCheck
    {
    #pragma warning disable 0649 // filled by JsonUtility
        [Serializable] private class Body { public string token; }
        [Serializable] private class Answer { public bool valid; public string player; }
    #pragma warning restore 0649
    
        private static readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
        private static readonly string url = (Environment.GetEnvironmentVariable("UBG_BACKEND_URL") ?? "http://127.0.0.1:3000").TrimEnd('/') + "/api/server/verify-session";
        private static readonly string serverKey = Environment.GetEnvironmentVariable("UBG_SERVER_KEY") ?? "";
    
        /// <summary>The player's gamertag, or null when the token isn't a real session (or the backend can't be reached).</summary>
        public static async Task<string> PlayerFor(string playerToken)
        {
            if (string.IsNullOrEmpty(playerToken)) return null;
            if (string.IsNullOrEmpty(serverKey)) { Debug.LogError("[Backend] UBG_SERVER_KEY isn't set on this server."); return null; }
            try
            {
                var request = new HttpRequestMessage(HttpMethod.Post, url);
                request.Headers.Add("X-Server-Key", serverKey);
                request.Content = new StringContent(JsonUtility.ToJson(new Body { token = playerToken }), Encoding.UTF8, "application/json");
                using (var response = await http.SendAsync(request))
                {
                    string json = await response.Content.ReadAsStringAsync();
                    if (!response.IsSuccessStatusCode) { Debug.LogWarning("[Backend] verify-session: HTTP " + (int)response.StatusCode + " " + json); return null; }
                    var answer = JsonUtility.FromJson<Answer>(json);
                    return answer != null && answer.valid ? answer.player : null;
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[Backend] verify-session failed: " + e.Message);
                return null;
            }
        }
    }

    It reads the backend address and the server key from the server's environment, so neither ever goes into a build players download. With the Advanced licence you can use UBGGameServer.VerifySession from the included game-server SDK instead.

    BackendConnectionApproval.cs

    // BackendConnectionApproval.cs - add it to your NetworkManager's GameObject.
    // Players sign in with your backend first, then join with BackendConnectionApproval.JoinAsClient().
    using System.Collections.Generic;
    using System.Text;
    using Unity.Netcode;
    using UnityEngine;
    using UniversalBackendGenerator.Network;
    
    public class BackendConnectionApproval : MonoBehaviour
    {
        /// <summary>Server: who each connected client is (client id -> gamertag).</summary>
        public static readonly Dictionary<ulong, string> Players = new Dictionary<ulong, string>();
    
        private void Start()
        {
            var nm = NetworkManager.Singleton;
            nm.NetworkConfig.ConnectionApproval = true;
            nm.ConnectionApprovalCallback = ApprovalCheck;
            nm.OnClientDisconnectCallback += id => Players.Remove(id);
        }
    
        /// <summary>Client: call this instead of StartClient(), after signing in.</summary>
        public static bool JoinAsClient()
        {
            NetworkManager.Singleton.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(MyGameClient.Instance.playerToken);
            return NetworkManager.Singleton.StartClient();
        }
    
        private async void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
        {
            if (request.ClientNetworkId == NetworkManager.ServerClientId)       // a host lets itself in
            {
                Players[request.ClientNetworkId] = MyGameClient.Instance.SignedInPlayer;
                response.Approved = true;
                response.CreatePlayerObject = true;
                return;
            }
    
            response.Pending = true;                                           // answer once the backend has replied
            string token = request.Payload != null ? Encoding.UTF8.GetString(request.Payload) : "";
            string player = await BackendSessionCheck.PlayerFor(token);
    
            response.Approved = player != null;
            response.CreatePlayerObject = player != null;
            if (player != null) Players[request.ClientNetworkId] = player;
            else response.Reason = "Your session isn't valid. Sign in again.";
            response.Pending = false;
        }
    }

    Add BackendConnectionApproval to the GameObject with your NetworkManager. It turns on connection approval and checks every player: the session travels as NGO's connection data, the server asks your backend, and the answer waits (Pending) until the backend replies. A host lets itself in.

  4. Use who they are

    // anywhere on the server
    string player = BackendConnectionApproval.Players[clientId];      // checked by your backend
    
    // e.g. in a NetworkBehaviour on the player object
    public override void OnNetworkSpawn()
    {
        if (IsServer) Debug.Log("Spawned " + BackendConnectionApproval.Players[OwnerClientId]);
    }

    Every approved client's gamertag is in Players, keyed by client id. Use it for name tags, to load the player's saved data, or to post their score. Your server can also post scores or read a player's cloud save directly: give its key the leaderboards or saves scope (Advanced).

Advanced: one-use join tickets

With the Advanced licence your servers register with the backend (a heartbeat every few seconds) and players ask the backend where to play. They get a ticket for one server that works once, for 60 seconds. The server redeems it and learns who the player is, so a ticket that's copied, reused or sent to another server is worthless. Matchmaking hands out the same tickets when a match is found.

// client: get a ticket for a server, then connect to it
MyGameClient.Instance.FleetJoin(null, "eu", "deathmatch", null, (ok, json) =>
{
    if (!ok) { Debug.LogWarning(json); return; }
    var join = JsonUtility.FromJson<FleetJoinAnswer>(json);
    var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();   // Unity.Netcode.Transports.UTP
    transport.SetConnectionData(join.server.address, (ushort)join.server.port);
    NetworkManager.Singleton.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(join.ticket);
    NetworkManager.Singleton.StartClient();
});

[System.Serializable] class FleetJoinAnswer { public string ticket; public FleetServer server; }
[System.Serializable] class FleetServer { public string address; public int port; }

The ticket goes in the connection data in place of the session. Then, on the server:

// on the server, once (the key needs the "fleet" scope as well)
static readonly UBGGameServer backend = new UBGGameServer(
    Environment.GetEnvironmentVariable("UBG_BACKEND_URL"), Environment.GetEnvironmentVariable("UBG_SERVER_KEY"), "eu-1");

// where you checked the session, redeem the ticket instead:
UBGGameServer.Joined joined = await backend.RedeemTicket(ticket);   // null: unknown, used, expired or for another server
string player = joined != null ? joined.player : null;

The MULTIPLAYER-SERVICES.txt in your download explains heartbeats, zones and handing players over between servers, and the game-server SDK (UBGGameServer.cs) does it all in C#.

Good to know

We ran every backend call on this page against a freshly generated backend: a real session, a made-up one, a wrong key, a key without the sessions scope and a backend that's down. The C# compiles against the Netcode for GameObjects API and the session check ran for real.

Questions

Does BackendGen.dev replace Netcode for GameObjects?
No, it works alongside it. Netcode for GameObjects runs the gameplay: moving players and syncing the match. Your backend runs everything around it: accounts, saved player data, leaderboards and cloud saves, plus matchmaking and your list of game servers with the Advanced licence.
Can a player pretend to be someone else on my Netcode server?
Not with this set-up. The client only sends its session; the server asks your backend who that session belongs to and uses the name the backend gives back. A made-up, expired or copied-from-another-backend session is turned away.
Do I need the Advanced licence?
No. Checking a player's session works with every licence. Advanced adds one-use join tickets, matchmaking, a list of your game servers, leaderboards, cloud saves and more.

Other engines and libraries

Haven't got a backend yet?

Try the generator free with an account: fill in your settings and see every file you'd get. Buy a licence when you want to download.

Try the generator freeCompare with PlayFab & Firebase