Skip to content

Integrating Nakama

Nakama is an open-source game backend by Heroic Labs. It handles player accounts, leaderboards, matchmaking, in-app purchases, and real-time multiplayer features.

Gameye provides a native Fleet Manager implementation for Nakama, the nakama-fleetmanager Go package. When Nakama’s matchmaker finds a group of players, your MatchmakerMatched hook calls the Fleet Manager, which starts a dedicated server session via the Session API and registers the matched players with it. One helper call then sends each player the server’s IP and port as a Nakama notification.

[Nakama Matchmaker]
↓ match found
[MatchmakerMatched hook] → fleetManager.Create(...)
↓ POST /session, PUT /session/player/join
[Gameye Session API]
↓ container starts
[Create callback] → fleetmanager.NotifyConnectionInfo(...)
↓ "gameye_match" notification (host, port, session id)
[Players] ←→ [Dedicated game server]

The Fleet Manager starts sessions, joins the matched players, stops sessions it gives up on, and keeps Nakama’s view of running sessions in sync with Gameye. Gameye stops each session when its TTL runs out.

  • Nakama 3.39 or later (the package’s example is pinned to Nakama 3.41)
  • nakama-common v1.46 or later, matching your Nakama release (v1.48.0 for Nakama 3.41)
  • A Gameye account (sign up) and an API token with the session:start, session:read and session:stop scopes
  • An application in Gameye with your game server image pushed to it
  • No warm pool for that application and region if your server reads per-match environment variables (see Warm pools and env)

A Nakama Go plugin must be built with the heroiclabs/nakama-pluginbuilder image whose tag matches your heroiclabs/nakama image, and with the same versions of dependencies the server uses.

Add the package to your Nakama server module and pin nakama-common to your Nakama release:

Terminal window
go get github.com/Gameye/[email protected]
go get github.com/heroiclabs/[email protected] # Nakama 3.41
go mod vendor

The example module reads its configuration from Nakama’s runtime.env in your Nakama config file:

VariableRequiredDescription
GAMEYE_API_TOKENYesYour Gameye API token
GAMEYE_API_IMAGEYesYour application name in Gameye
GAMEYE_API_IMAGE_VERSIONYesThe image tag to run
GAMEYE_API_REGIONYesTarget region (see Region Selection)
GAMEYE_API_URLNoSession API URL. Defaults to the self-serve platform, https://api.sandbox-gameye.gameye.net. Production contracts use https://api.production-gameye.gameye.net
GAMEYE_API_TTLNoMaximum session lifetime, e.g. 30m (default) or 2h
GAMEYE_API_PORTNoContainer port players connect to, e.g. 7777/udp. Set it when your image exposes more than one port
runtime:
env:
- "GAMEYE_API_TOKEN=your-api-token"
- "GAMEYE_API_IMAGE=your-application"
- "GAMEYE_API_IMAGE_VERSION=1.0.0"
- "GAMEYE_API_REGION=europe"
- "GAMEYE_API_PORT=7777/udp"

Create the Fleet Manager in your module’s InitModule and register it:

fm, err := fleetmanager.NewGameyeFleetManager(ctx, fleetmanager.GameyeConfig{
ApiToken: env["GAMEYE_API_TOKEN"],
Image: env["GAMEYE_API_IMAGE"],
Version: env["GAMEYE_API_IMAGE_VERSION"],
Region: env["GAMEYE_API_REGION"],
Ttl: env["GAMEYE_API_TTL"],
Port: env["GAMEYE_API_PORT"],
}, logger, db, initializer, nk)
if err != nil {
return err
}
if err := initializer.RegisterFleetManager(fm); err != nil {
return err
}

The matchmaker flow has three steps.

  1. Create. In MatchmakerMatched, call fm.Create with the matched user ids. It returns {"session_id": <id>} at once and starts the session in the background.
  2. Callback. When the session is running, the callback receives runtime.CreateSuccess and the instance. The players are already joined to the session, so don’t call Join again. Nakama cancels the hook’s context when the hook returns, so use a new context in the callback.
  3. Notify. Call fleetmanager.NotifyConnectionInfo. Each player gets a notification with subject gameye_match whose content holds host, port and session_id.
initializer.RegisterMatchmakerMatched(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
userIds := make([]string, 0, len(entries))
for _, e := range entries {
userIds = append(userIds, e.GetPresence().GetUserId())
}
callback := func(status runtime.FmCreateStatus, instance *runtime.InstanceInfo, _ []*runtime.SessionInfo, _ map[string]any, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if status != runtime.CreateSuccess {
logger.Warn("no game server for %v: %v", userIds, err)
return
}
_ = fleetmanager.NotifyConnectionInfo(ctx, nk, userIds, instance, nil, true)
}
metadata := map[string]any{
fleetmanager.MetadataKeyEnv: map[string]string{"MATCH_USER_IDS": strings.Join(userIds, ",")},
}
if _, err := fm.Create(ctx, len(userIds), userIds, nil, metadata, callback); err != nil {
return "", err
}
return "", nil
})

On the client, listen for the gameye_match notification and connect to host and port. The package README covers error handling (quota, capacity and timeout), per-player notification extras and the full example module.

metadata["gameye.env"] is passed to the game server container as environment variables, and is never sent as session labels or written to Nakama storage. A session served from a warm pool runs in a container that started before the request, so it does not receive this env. Turn off the warm pool for the application and region if your server depends on per-match env. Gameye may also echo container env when sessions are read, so pass short-lived, per-match values rather than long-lived secrets.

If you’re using Unity Matchmaker alongside Nakama, allocations can be routed to Gameye without changing your matchmaking rules or ticket logic. For a dedicated Unity Matchmaker integration that does not require Nakama, see the Unity Matchmaker guide.

  • Limited session list querying. Gameye’s Session API does not paginate, so FleetManager#List ignores the query and cursor and returns every session for the configured region, application and tag.
  • No latency-based placement. Sessions start in the configured region; Create ignores player latencies.
  • No built-in retry queue. Sessions are created immediately on request. If session creation fails, retry logic must be handled at the application layer.
  • No backfill helper. Join registers more players with a running session, but finding a session with free seats is up to you.