Guides · Mirror

Player accounts and a database for your Mirror game

Mirror 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 connects the two, so your Mirror server knows exactly who each player is before they join, and nobody can pretend to be someone else.

How it fits together

Players never tell your Mirror 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 → Mirror serverConnect and send the token once, through NetworkAuthenticator.
  3. Mirror 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. Mirror serverLet in, or turn away: ServerAccept with the confirmed name, or ServerReject.
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 Mirror 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 Mirror;
    using UniversalBackendGenerator.Network;
    
    // your sign-in screen
    MyGameClient.Instance.SignIn(gamertag, null, (ok, message) =>
    {
        if (ok) NetworkManager.singleton.StartClient();   // the authenticator sends the session
        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.

    BackendAuthenticator.cs

    // BackendAuthenticator.cs - add it to the same GameObject as your NetworkManager and drag it into the
    // NetworkManager's "Authenticator" slot. Players sign in with your backend first, then connect.
    using System.Collections;
    using System.Collections.Generic;
    using Mirror;
    using UnityEngine;
    using UniversalBackendGenerator.Network;
    
    public class BackendAuthenticator : NetworkAuthenticator
    {
        public struct AuthRequest : NetworkMessage { public string playerToken; }
        public struct AuthResponse : NetworkMessage { public bool accepted; public string player; public string message; }
    
        /// <summary>Advanced: a one-use join ticket from FleetJoin / matchmaking, sent instead of the session.</summary>
        public static string JoinTicket;
    
        private readonly HashSet<NetworkConnectionToClient> checking = new HashSet<NetworkConnectionToClient>();
    
        // ---------- server ----------
        public override void OnStartServer() { NetworkServer.RegisterHandler<AuthRequest>(OnAuthRequest, false); }
        public override void OnStopServer() { NetworkServer.UnregisterHandler<AuthRequest>(); checking.Clear(); }
    
        private async void OnAuthRequest(NetworkConnectionToClient conn, AuthRequest msg)
        {
            if (conn.isAuthenticated || !checking.Add(conn)) return;          // one check per connection
            string player = await BackendSessionCheck.PlayerFor(msg.playerToken);
            checking.Remove(conn);
            if (!NetworkServer.connections.ContainsKey(conn.connectionId)) return;   // they left while we asked
    
            if (player != null)
            {
                conn.authenticationData = player;                             // later: (string)conn.authenticationData
                conn.Send(new AuthResponse { accepted = true, player = player });
                ServerAccept(conn);
            }
            else
            {
                conn.Send(new AuthResponse { accepted = false, message = "Your session isn't valid. Sign in again." });
                conn.isAuthenticated = false;
                StartCoroutine(RejectAfterReply(conn));                       // let the reply arrive first
            }
        }
    
        private IEnumerator RejectAfterReply(NetworkConnectionToClient conn)
        {
            yield return new WaitForSeconds(1f);
            ServerReject(conn);
        }
    
        // ---------- client ----------
        public override void OnStartClient() { NetworkClient.RegisterHandler<AuthResponse>(OnAuthResponse, false); }
        public override void OnStopClient() { NetworkClient.UnregisterHandler<AuthResponse>(); }
    
        public override void OnClientAuthenticate()
        {
            // the session from MyGameClient.Instance.SignIn(...) - sign in before calling StartClient()
            string proof = string.IsNullOrEmpty(JoinTicket) ? MyGameClient.Instance.playerToken : JoinTicket;
            NetworkClient.Send(new AuthRequest { playerToken = proof });
        }
    
        private void OnAuthResponse(AuthResponse msg)
        {
            if (msg.accepted) ClientAccept();
            else { Debug.LogWarning("The server refused the session: " + msg.message); ClientReject(); }
        }
    }

    Add BackendAuthenticator to the GameObject with your NetworkManager, then drag it into the NetworkManager's Authenticator field. Mirror now calls it for every connection: the client sends its session, the server asks your backend and lets the player in only if the session is real.

  4. Use who they are

    // in your NetworkManager subclass
    public override void OnServerAddPlayer(NetworkConnectionToClient conn)
    {
        base.OnServerAddPlayer(conn);
        string player = (string)conn.authenticationData;              // checked by your backend
        conn.identity.GetComponent<PlayerName>().displayName = player; // e.g. a [SyncVar] on your player prefab
    }

    The gamertag is on the connection from then on. Use it for the player's name tag, to load their 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);
    BackendAuthenticator.JoinTicket = join.ticket;              // sent instead of the session
    NetworkManager.singleton.networkAddress = join.server.address;
    // set your transport's port to join.server.port, then:
    NetworkManager.singleton.StartClient();
});

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

BackendAuthenticator already sends JoinTicket instead of the session when it's set. 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 Mirror API and the session check ran for real.

Questions

Does BackendGen.dev replace Mirror?
No, it works alongside it. Mirror 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 Mirror 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