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.
- Unity + Fish-Net
- Dedicated or host servers
- Base or Advanced
- Compiled against the Fish-Net API, checked against a generated backend
How it fits together
Players never tell your Fish-Net server who they are: your backend does. The whole handoff is four steps:
- Game → your backendSign in. The player signs in with their gamertag and gets a session token (it lasts 30 days).
- Game → Fish-Net serverConnect and send the token once, through
Authenticator. - Fish-Net server → your backendCheck.
POST /api/server/verify-sessionwith the server's own key. The backend answers with the real gamertag, or says the token isn't valid. - Fish-Net serverLet in, or turn away:
OnAuthenticationResult(conn, true)with the confirmed name, orOnAuthenticationResult(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
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
MyGameClientwith your generated client's class name: it's your project name plusClient, e.g.StarRaidersClient.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
sessionsscope. 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"]}').keyThe 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 -nographicsOn Windows (PowerShell):
$env:UBG_BACKEND_URL = "https://api.yourgame.com" $env:UBG_SERVER_KEY = "ubgs1.xxxx.yyyy" .\MyGameServer.exe -batchmode -nographicsNever put the key (or your admin password) in a build that players download: anyone could pull it out.
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.VerifySessionfrom 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.
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 theleaderboardsorsavesscope (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
- Use https:// for
UBG_BACKEND_URLon a live server, so the key and sessions are encrypted on the way. The deploy guide sets that up. - The session crosses your game connection once. Most game transports don't encrypt by default. For a public game, switch on your transport's encryption if it has it, or use Advanced join tickets, which are worthless once used.
- Check at connect, not on every message. After the check the connection itself is the proof; there's no need to ask the backend again.
- If the backend can't be reached, the check fails and the player is turned away, so nobody gets in unchecked.
- Give each key only what it needs (
sessions, plusfleetwith tickets) and revoke a key the moment it leaks.
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