Guides · Fish-Net

An account system and database for your Fish-Net game

Fish-Net runs your gameplay. Your BackendGen.dev backend runs everything around it: player accounts, saved player data, leaderboards and cloud saves, on your own server. This guide plugs the two together with a Fish-Net Authenticator, so your server knows exactly who each player is before they join.

How it fits together

Players never tell your Fish-Net 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 → Fish-Net serverConnect and send the token once, through Authenticator.
  3. Fish-Net 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. Fish-Net serverLet in, or turn away: OnAuthenticationResult(conn, true) with the confirmed name, or OnAuthenticationResult(conn, 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 Fish-Net 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 FishNet;
    using UniversalBackendGenerator.Network;
    
    // your sign-in screen
    MyGameClient.Instance.SignIn(gamertag, null, (ok, message) =>
    {
        if (ok) InstanceFinder.ClientManager.StartConnection();   // 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 your NetworkManager's GameObject and drag it into the
    // ServerManager's "Authenticator" field. Players sign in with your backend first, then connect.
    using System;
    using System.Collections.Generic;
    using FishNet.Authenticating;
    using FishNet.Broadcast;
    using FishNet.Connection;
    using FishNet.Managing;
    using FishNet.Transporting;
    using UniversalBackendGenerator.Network;
    
    public struct SessionBroadcast : IBroadcast { public string PlayerToken; }
    public struct SessionResultBroadcast : IBroadcast { public bool Passed; public string Player; }
    
    public class BackendAuthenticator : Authenticator
    {
        public override event Action<NetworkConnection, bool> OnAuthenticationResult;
        /// <summary>Advanced: a one-use join ticket from FleetJoin / matchmaking, sent instead of the session.</summary>
        public static string JoinTicket;
    
        private readonly HashSet<int> checking = new HashSet<int>();
    
        public override void InitializeOnce(NetworkManager networkManager)
        {
            base.InitializeOnce(networkManager);
            NetworkManager.ClientManager.OnClientConnectionState += OnClientConnectionState;
            NetworkManager.ServerManager.RegisterBroadcast<SessionBroadcast>(OnSessionBroadcast, false);   // before authentication
            NetworkManager.ClientManager.RegisterBroadcast<SessionResultBroadcast>(OnSessionResult);
        }
    
        // ---------- client: send the session as soon as the connection is up ----------
        private void OnClientConnectionState(ClientConnectionStateArgs args)
        {
            if (args.ConnectionState != LocalConnectionState.Started) return;
            string proof = string.IsNullOrEmpty(JoinTicket) ? MyGameClient.Instance.playerToken : JoinTicket;
            NetworkManager.ClientManager.Broadcast(new SessionBroadcast { PlayerToken = proof });
        }
    
        private void OnSessionResult(SessionResultBroadcast msg, Channel channel)
        {
            NetworkManager.Log(msg.Passed ? "Joined as " + msg.Player : "The server refused the session. Sign in again.");
        }
    
        // ---------- server: ask the backend who this is ----------
        private async void OnSessionBroadcast(NetworkConnection conn, SessionBroadcast msg, Channel channel)
        {
            if (conn.IsAuthenticated) { conn.Disconnect(true); return; }      // already in: shouldn't happen
            if (!checking.Add(conn.ClientId)) return;                          // one check per connection
            string player = await BackendSessionCheck.PlayerFor(msg.PlayerToken);
            checking.Remove(conn.ClientId);
            if (!conn.IsActive) return;                                        // they left while we asked
    
            if (player != null) conn.CustomData = player;                      // later: (string)conn.CustomData
            NetworkManager.ServerManager.Broadcast(conn, new SessionResultBroadcast { Passed = player != null, Player = player }, false);
            OnAuthenticationResult?.Invoke(conn, player != null);              // FishNet lets them in, or kicks them
        }
    }

    Add BackendAuthenticator to the GameObject with your NetworkManager and drag it into the ServerManager's Authenticator field. When a client connects it sends its session; the server asks your backend, then Fish-Net lets the player in or kicks them.

  4. Use who they are

    // in a NetworkBehaviour on the player object (server side)
    public override void OnStartServer()
    {
        base.OnStartServer();
        string player = (string)Owner.CustomData;                     // checked by your backend
        Debug.Log("Spawned " + player);
    }

    The gamertag stays on the connection (CustomData). 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);
    BackendAuthenticator.JoinTicket = join.ticket;              // send this instead of the session
    InstanceFinder.ClientManager.StartConnection(join.server.address, (ushort)join.server.port);
});

[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 Fish-Net API and the session check ran for real.

Questions

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