Heroic Labs Documentation

Introduction #

Nakama includes a fast embedded code runtime enabling you to write custom logic as a JavaScript bundle, Go plugins, and Lua modules.

The runtime framework is essential to writing server-side logic for your games or apps. Use it to write code you would not want to run on client devices or the browser. The code you deploy with the server can be used immediately by clients, allowing you to change behavior on the fly and add new features faster. This code can be used to run authoritative logic or perform validation checks as well as integrate with other services over HTTPS.

Use server-side code when you want to set rules around various features, like how many friends a user may have or how many groups they can join.

We do not recommend modifying Nakama source-code and rebuilding from source to add new features or customize behavior. The recommended approach is using the embedded runtime.

This page will cover the key concepts and functionality available in the Nakama runtime framework.

Loading modules #

Tip
Heroic Labs recommends use of the JavaScript VM.

By default the server will scan all files within the data/modules folder relative to the server file or the folder specified in the YAML configuration at startup. You can also specify the modules folder via a command flag when you start the server.

Files with the .lua, .so, and .js extensions found in the runtime path folder will be loaded and evaluated as part of the startup sequence. Each of the runtimes has access to the Nakama API to operate on messages from clients as well as execute logic on demand.

The different supported languages are loaded with a precedence order of Go -> Lua -> JavaScript. This ensures deterministic behavior if match handlers or RPC functions/hooks are registered in multiple runtimes, providing the flexibility to leverage the different runtimes as best suited and have them work seamlessly together. For example, you can define an RPC function in the JavaScript runtime to create a match with a set of match handlers written in Go.

JavaScript runtime #

The JavaScript runtime expects an index.js file. To change the name of the relative file path where the code will be loaded within the runtime path you can set it in the server YML or as a command flag.

1
nakama --runtime.js_entrypoint "some/path/index.js"

This path must be relative to the default or set runtime path.

Go runtime #

The Go runtime looks for a Go plugin .so shared object file.

To learn how you can generate this file with your custom Go runtime code follow see building the Go shared object.

Lua runtime #

The Lua runtime will interpret and load any .lua files, including those in a subdirectory. These can be referenced as modules with relative paths.

Each Lua file represents a module and all code in each module will be run and can be used to register functions.

Runtime context #

All registered functions across all runtimes receive a context as the first argument. This contains fields which depend on when and how the code is executed. You can extract information about the request or the user making it from the context:

Server
1
local user_id = context.user_id
Server
1
2
3
4
userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
if !ok {
  // User ID not found in the context.
}
Server
1
let userId = ctx.userId;

If you are writing your runtime code in Lua, the context will be a table from which you can access the fields directly. The Go runtime context is a standard context.Context type and its fields can be accessed as shown above. In JavaScript, context is a plain object with properties.

Go Context KeyGo TypeLua Context KeyJavaScript Context PropertyPurpose
RUNTIME_CTX_ENVmap[string]stringenvenvA table of key/value pairs which are defined in the YAML configuration of the server. This is useful to store API keys and other secrets which may be different between servers run in production and in development.
RUNTIME_CTX_MODEstringexecution_modeexecutionModeThe mode associated with the execution context. It’s one of these values: “run_once”, “rpc”, “before”, “after”, “match”, “matchmaker”, “leaderboard_reset”, “tournament_reset”, “tournament_end”.
RUNTIME_CTX_QUERY_PARAMSmap[string]stringquery_paramsqueryParamsQuery params that was passed through from HTTP request.
RUNTIME_CTX_SESSION.IDstringsession_idsessionIdThe user session associated with the execution context.
RUNTIME_CTX_USER_IDstringuser_iduserIdThe user ID associated with the execution context.
RUNTIME_CTX_USERNAMEstringusernameusernameThe username associated with the execution context.
RUNTIME_CTX_USER_SESSION_EXPint64user_session_expuserSessionExpThe user session expiry in seconds associated with the execution context.
RUNTIME_CTX_CLIENT_IPstringclient_ipclientIpThe IP address of the client making the request.
RUNTIME_CTX_CLIENT_PORTstringclient_portclientPortThe port number of the client making the request.
RUNTIME_CTX_MATCH_IDstringmatch_idmatchIdThe match ID that is currently being executed. Only applicable to server authoritative multiplayer.
RUNTIME_CTX_MATCH_NODEstringmatch_nodematchNodeThe node ID that the match is being executed on. Only applicable to server authoritative multiplayer.
RUNTIME_CTX_MATCH_LABELstringmatch_labelmatchLabelLabels associated with the match. Only applicable to server authoritative multiplayer.
RUNTIME_CTX_MATCH_TICK_RATEintmatch_tick_ratematchTickRateTick rate defined for this match. Only applicable to server authoritative multiplayer.

Go context #

The runtime context is distinct from the Go context. It is important that a Context type be included in all server requests to avoid a potential overloading of the server with dead requests (i.e. requests from users who have since disconnected).

Inclusion of the Context allows for the context cancellation - when a user’s HTTP connection to the server is closed - to be propagated across the entire chain of requests and avoid the processing and/or buildup of such dead requests.

Database handler #

The runtime includes a database object that can be used to access the underlying game database. This enables you to include custom SQL queries as part of your game design and logic.

The database handler has a limit on the number of available connections to the database. This can lead to slowed response time for your users along with other errors. To avoid such issues you must ensure that your custom SQL queries properly release the connection once finished with the relevant row(s).

If using db.QueryContext() or db.Query(), you must call row.Close() after you are finished with the database rows data.

If using db.QueryRow() or db.QueryRowContext(), you must call either row.Scan or row.Close() after you are finished with the database rows data.

Note that using custom SQL should be avoided wherever possible in favor of using the built-in features of Nakama. You should also avoid the creation of custom tables. If your game design requires either of these options, please contact Heroic Labs before proceeding.

Logger #

A logger instance included in the server runtime enables you to write and access log messages in your server code using the following severities: INFO, WARN, ERROR, and DEBUG.

See an example for the TypeScript, Go, and Lua runtime.

Nakama module #

The Nakama module is included in the code runtime built into the server. This module provides access to a range of functions for implementing custom logic and behavior.

See the function reference for your preferred language to learn about the available functions:

Functionality #

RPC functions #

Remote Procedure Calls (RPCs) let you call functions registered in your runtime code to operate on messages received from clients or execute custom logic on demand, for example a profanity filter for chat messages.

RPC functions can be called both from clients and through server-to-server calls.

Hooks #

All runtime code is evaluated at server startup and can be used to register functions - these functions are called hooks. You can register before hooks to intercept and act on client messages, after hooks to call a function after an event has been processed, and custom RPC hooks which can be called by clients.

There are multiple ways to register a function within the runtime, each of which is used to handle specific behavior between client and server. For example:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- NOTE: Function arguments have been omitted in the example.
-- If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
nk.register_rt_before()
nk.register_rt_after()

-- Otherwise use this.
nk.register_req_after()
nk.register_req_before()

-- If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
nk.register_matchmaker_matched()

-- If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
nk.register_leaderboard_reset()
nk.register_tournament_reset()

-- Similarly, you can run server code when the tournament ends.
nk.register_tournament_end()
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// NOTE: All Go runtime registrations must be made in the module's InitModule function.
//       Function arguments have been omitted in the example.

// If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
initializer.RegisterBeforeRt()
initializer.RegisterAfterRt()

// Otherwise use the relevant before / after hook, e.g.
initializer.RegisterBeforeAddFriends()
initializer.RegisterAfterAddFriends()
// (...)

// If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
initializer.RegisterMatchmakerMatched()

// If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
initializer.RegisterLeaderboardReset()
initializer.RegisterTournamentReset()

// Similarly, you can run server code when the tournament ends.
initializer.RegisterTournamentEnd()
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// NOTE: All JavaScript runtime registrations must be made in the bundle's InitModule function.
//       Function arguments have been omitted in the example.

// If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
initializer.registerRtBefore()
initializer.registerRtAfter()

// Otherwise use the relevant before / after hook, e.g.
initializer.registerBeforeAddFriends()
initializer.registerAfterAddFriends()
// (...)

// If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
initializer.registerMatchmakerMatched()

// If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
initializer.registerLeaderboardReset()
initializer.registerTournamentReset()

// Similarly, you can run server code when the tournament ends.
initializer.registerTournamentEnd()

See Message Names for the full list of available hooks.

Before hooks #

Any function may be registered to intercept a message received from a client and operate on it (or reject it) based on custom logic. This is useful to enforce specific rules on top of the standard features in the server, or to replace what would otherwise be an invalid input.

Input validation does not apply until after execution of any before hooks, meaning clients can send larger (or otherwise invalid) inputs than the server would normally allow so long as the before hook replaces the input with a valid one. For example, given custom authentication IDs must be between 6-128 bytes, if your external authentication provider returns a longer ID use a before hook to replace that input with a valid ID.

In Go, each hook will receive the request input as a struct containing the data that will be processed by the server for that request, if that feature is expected to receive an input. In Lua, the second argument will be the incoming payload containing data received that will be processed by the server. In JavaScript the payload is the fourth argument.

You must remember to return the payload at the end of your function in the same structure as you received it.

If you choose to return nil (Lua) or null|undefined (JavaScript) instead of the payload (or a non-nil error in Go) the server will halt further processing of that message. This can be used to stop the server from accepting certain messages or disabling/blacklisting certain server features.

After hooks #

Similar to Before hook you can attach a function to operate on a message. The registered function will be called after the message has been processed in the pipeline. The custom code will be executed asynchronously after the response message has been sent to a client.

The second argument is the “outgoing payload” containing the server’s response to the request. The third argument contains the “incoming payload” containing the data originally passed to the server for this request.

After hooks cannot change the response payload being sent back to the client and errors do not prevent the response from being sent.

RPC hooks #

Some logic between client and server is best handled as RPC functions which clients can execute. For this purpose Nakama supports the registration of custom RPC hooks.

The ID of your registered RPC can be used within client code to send an RPC message to execute the function on the server and return the result.

From Go runtime code, the result is returned as (string, error). From Lua runtime code, results are always returned as a Lua string (or optionally nil). From the JavaScript runtime code, results should always be a string, null or omitted (undefined).

Server to server #

You can check if the context has a user ID to see if an RPC function is a client or server-to-server call. Server to server calls will never have a user ID. If you want to scope functions to never be accessible from the client just return an error if you find a user ID in the context.

See the server runtime examples.

Run once #

The runtime environment allows you to run code that must only be executed only once. This is useful if you have custom SQL queries that you need to perform or to register with third party services.

See the implementation examples for TypeScript, Go, or Lua for an example.

Message names #

If your runtime code is in Go, refer to the interface definition for a full list of hooks that are available in the runtime package.

Use the following request names for registering your Before and After hooks:

Request NameDescription
AddFriendsAdd friends by ID or username to a user’s account.
AddGroupUsersAdd users to a group.
AuthenticateCustomAuthenticate a user with a custom id against the server.
AuthenticateDeviceAuthenticate a user with a device id against the server.
AuthenticateEmailAuthenticate a user with an email+password against the server.
AuthenticateFacebookAuthenticate a user with a Facebook OAuth token against the server.
AuthenticateGameCenterAuthenticate a user with Apple’s GameCenter against the server.
AuthenticateGoogleAuthenticate a user with Google against the server.
AuthenticateSteamAuthenticate a user with Steam against the server.
BlockFriendsBlock one or more users by ID or username.
CreateGroupCreate a new group with the current user as the owner.
DeleteFriendsDelete one or more users by ID or username.
DeleteGroupDelete one or more groups by ID.
DeleteLeaderboardRecordDelete a leaderboard record.
DeleteNotificationsDelete one or more notifications for the current user.
DeleteStorageObjectsDelete one or more objects by ID or username.
GetAccountFetch the current user’s account.
GetUsersFetch zero or more users by ID and/or username.
HealthcheckA healthcheck which load balancers can use to check the service.
ImportFacebookFriendsImport Facebook friends and add them to a user’s account.
JoinGroupImmediately join an open group, or request to join a closed one.
KickGroupUsersKick a set of users from a group.
LeaveGroupLeave a group the user is a member of.
LinkCustomAdd a custom ID to the social profiles on the current user’s account.
LinkDeviceAdd a device ID to the social profiles on the current user’s account.
LinkEmailAdd an email+password to the social profiles on the current user’s account.
LinkFacebookAdd Facebook to the social profiles on the current user’s account.
LinkGameCenterAdd Apple’s GameCenter to the social profiles on the current user’s account.
LinkGoogleAdd Google to the social profiles on the current user’s account.
LinkSteamAdd Steam to the social profiles on the current user’s account.
ListChannelMessagesList a channel’s message history.
ListFriendsList all friends for the current user.
ListGroupsList groups based on given filters.
ListGroupUsersList all users that are part of a group.
ListLeaderboardRecordsList leaderboard records.
ListMatchesFetch a list of running matches.
ListNotificationsFetch a list of notifications.
ListStorageObjectsList publicly readable storage objects in a given collection.
ListUserGroupsList groups the current user belongs to.
PromoteGroupUsersPromote a set of users in a group to the next role up.
DemoteGroupUsersDemote a set of users in a group to a lower role.
ReadStorageObjectsGet storage objects.
UnlinkCustomRemove the custom ID from the social profiles on the current user’s account.
UnlinkDeviceRemove the device ID from the social profiles on the current user’s account.
UnlinkEmailRemove the email+password from the social profiles on the current user’s account.
UnlinkFacebookRemove Facebook from the social profiles on the current user’s account.
UnlinkGameCenterRemove Apple’s GameCenter from the social profiles on the current user’s account.
UnlinkGoogleRemove Google from the social profiles on the current user’s account.
UnlinkSteamRemove Steam from the social profiles on the current user’s account.
UpdateAccountUpdate fields in the current user’s account.
UpdateGroupUpdate fields in a given group.
WriteLeaderboardRecordWrite a record to a leaderboard.
WriteStorageObjectsWrite objects into the storage engine.

Names are case-insensitive. For more information, see apigrpc.proto.

For real-time before and after hooks, use the following message names:

Message NameDescription
ChannelJoinJoin a realtime chat channel.
ChannelLeaveLeave a realtime chat channel.
ChannelMessageSendSend a message to a realtime chat channel.
ChannelMessageUpdateUpdate a message previously sent to a realtime chat channel.
ChannelMessageRemoveRemove a message previously sent to a realtime chat channel.
MatchCreateA client to server request to create a realtime match.
MatchDataSendA client to server request to send data to a realtime match.
MatchJoinA client to server request to join a realtime match.
MatchLeaveA client to server request to leave a realtime match.
MatchmakerAddSubmit a new matchmaking process request.
MatchmakerRemoveCancel a matchmaking process using a ticket.
StatusFollowStart following some set of users to receive their status updates.
StatusUnfollowStop following some set of users to no longer receive their status updates.
StatusUpdateSet the user’s own status.

Names are case-insensitive. For more information, have a look at realtime.proto.

Restrictions #

See the TypeScript, Go, and Lua pages for runtime specific restrictions.

Background jobs #

To avoid “dead work” - done when the user is not present - and the unnecessary server load, background jobs should be avoided in favor of an event driven route. In this approach, the client makes an RPC call when the user returns, and in that called function any updates required by your game logic and use case are performed.

Where a scheduled background job would needlessly perform these updates across the entire user base, regardless of a user’s inactivity, this approach ensures that work is only performed for users still active in the game.

Use of background jobs can cause further problems as any job would be limited to one Nakama instance, requiring either duplication across all instances or a non-homogenous workload across instances.

Related Pages