Remote Procedure Calls

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.

Register 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.

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.

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

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.registerAfterAddFriends()
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 here for a complete list of the server message names.

Only one hook may be registered for each type. If you register more than one hook, then only the last registration is used. RPC functions are unique per registered ID, and you can register the same function under multiple IDs.

Before hook

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.

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 as seen in the example.

The code example below fetches the current user’s profile and checks the metadata, which is assumed to be JSON encoded with "{level: 12}" in it. If a user’s level is too low, an error is thrown to prevent the Friend Add message from being passed onwards in the server:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
local nk = require("nakama")

local function limit_friends(context, payload)
  local user = nk.users_get_id({context.user_id})[1]
  -- Let's assume we've stored a user's level in their metadata.
  if user.metadata.level < 10 then
      error("Must reach level 10 before you can add friends.")
  end
  return payload -- important!
end

nk.register_req_before(limit_friends, "AddFriends")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func BeforeAddFriends(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error) {
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
            return nil, errors.New("Missing user ID.")
    }

    account, err := nk.UsersGetId(ctx, []string{userID})
    if err != nil {
            return nil, err
    }

    var metadata map[string]interface{}
    if err := json.Unmarshal([]byte(account.GetUser().GetMetadata()), &metadata); err != nil {
            return nil, errors.New("Corrupted user metadata.")
    }

    // Let's assume we've stored a user's level in their metadata.
    if level, ok := metadata["level"].(int); !ok || level < 10 {
            return nil, errors.New("Must reach level 10 before you can add friends.")
    }

    return in, nil
}

// Register as a before hook for the appropriate feature, this call should be in InitModule.
if err := initializer.RegisterBeforeAddFriends(BeforeAddFriends); err != nil {
    logger.Error("Unable to register: %v", err)
    return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let userAddFriendLevelCheck: nkruntime.BeforeHookFunction<AddFriendsRequest> =
function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.AddFriendsRequest): nkruntime.AddFriendsRequest {
	let userId = ctx.userId;

	let users: nkruntime.User[];
	try {
			users = nk.usersGetId([ userId ]);
	} catch (error) {
			logger.error('Failed to get user: %s', error.message);
			throw error;
	}

	// Let's assume we've stored a user's level in their metadata.
	if (users[0].metadata.level < 10) {
			throw Error('Must reach level 10 before you can add friends.');
	}

	// important!
	return data;
};

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerBeforeAddFriends(userAddFriendLevelCheck);

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 hook

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.

The example code below writes a record to a user’s storage when they add a friend. Any data returned by the function will be discarded.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
local nk = require("nakama")

local function add_reward(context, outgoing_payload, incoming_payload)
  local value = {
    user_ids = {incoming_payload.user_id}
  }

  local object = {
    collection = "rewards",
    key = "reward",
    user_id = context.user_id,
    value = value
  }

  nk.storage_write({ object })
end

nk.register_req_after(add_reward, "AddFriends")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func AfterAddFriends(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) error {
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
        return errors.New("Missing user ID.")
    }

    value, err := json.Marshal(map[string]interface{}{"user_ids": in.GetIds()})
    if err != nil {
        return err
    }

    if _, err := nk.StorageWrite(ctx, []*runtime.StorageWrite{
        &runtime.StorageWrite{
            Collection: "rewards",
            Key:        "reward",
            UserID:     userID,
            Value:      string(value),
        },
    }); err != nil {
        return err
    }

    return nil
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
if err := initializer.RegisterAfterAddFriends(AfterAddFriends); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// The AddFriends function does not return a payload, hence why the outPayload argument is null.
let afterAddFriends: nkruntime.AfterHookFunction<null, AddFriendsRequest> =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, outPayload: null, inPayload: nkruntime.AddFriendsRequest) {
	let userId = ctx.userId;
	if (!userId) {
			throw Error('Missing user ID.');
	}

	let userIds = inPayload.ids;
	let storageObj: nkruntime.StorageWriteRequest = {
			collection: 'rewards',
			key: 'reward',
			userId: userId,
			value: { userIds },
	};

	try {
			nk.storageWrite([ storageObj ]);
	} catch (error) {
			logger.error('Error writing storage object: %s', error.message);
			throw error;
	};

	return null; // Can be omitted, will return `undefined` implicitly
};

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerAfterAddFriends(afterAddFriends);

RPC hook

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.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
local nk = require("nakama")

local function custom_rpc_func(context, payload)
  nk.logger_info(string.format("Payload: %q", payload))

  -- "payload" is bytes sent by the client we'll JSON decode it.
  local json = nk.json_decode(payload)

  return nk.json_encode(json)
end

nk.register_rpc(custom_rpc_func, "custom_rpc_func_id")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func CustomRpcFunc(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		logger.Info("Payload: %s", payload)

		// "payload" is bytes sent by the client we'll JSON decode it.
		var value interface{}
		if err := json.Unmarshal([]byte(payload), &value); err != nil {
				return "", err
		}

		response, err := json.Marshal(value)
		if err != nil {
				return "", err
		}

    return string(response), nil
}

// Register as an RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("custom_rpc_func_id", CustomRpcFunc); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let customFuncRpc: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	logger.info('payload: %q', payload);

	// "payload" is bytes sent by the client we'll JSON decode it.
	let json = JSON.parse(payload);

	return JSON.stringify(json);
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerRpc("custom_rpc_func_id", customFuncRpc);

The code above registers a function with the identifier custom_rpc_func_id. This ID 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.

Sometimes it’s useful to create HTTP REST handlers which can be used by web services and ease integration into custom server environments. This can be achieved by using the RPC hook, however this uses the Runtime HTTP Key to authenticate with the server.

Server
1
2
3
4
5
6
7
8
9
local nk = require("nakama")

local function http_handler(context, payload)
  local message = nk.json_decode(payload)
  nk.logger_info(string.format("Message: %q", message))
  return nk.json_encode({["context"] = context})
end

nk.register_rpc(http_handler, "http_handler_path")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func HttpHandler(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		var message interface{}
		if err := json.Unmarshal([]byte(payload), &message); err != nil {
				return "", err
		}

		logger.Info("Message: %v", message)

		response, err := json.Marshal(map[string]interface{}{"message": message})
		if err != nil {
				return "", err
		}

		return string(response), nil
}

// Register as an RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("http_handler_path", HttpHandler); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let customFuncRpc: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	logger.info('payload: %q', payload);

	if (ctx.userId) {
		// Reject non server-to-server call
		throw Error('Cannot invoke this function from user session');
	}

	let message = JSON.parse(payload);
	logger.info('Message: %q', message);

	return JSON.stringify({ message: message });
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerRpc("custom_rpc_func_id", customFuncRpc);

The registered RPC Functions can be invoked with any HTTP client of your choice. For example, with cURL you could execute the function with the server as follows.

1
2
3
4
curl "http://127.0.0.1:7350/v2/rpc/http_handler_path?http_key=defaulthttpkey" \
	-d '"{\"some\": \"data\"}"' \
	-H 'Content-Type: application/json' \
	-H 'Accept: application/json'

Notice that the JSON payload is escaped and wrapped inside a string. This is by design due to gRPC not having a type that would map between a Protobuf type and a JSON object at the time the RPC API was designed. Support for JSON has since been added to gRPC but we have kept it this way to not break the API contract and ensure compatibility.

An unwrap query parameter is supported which allows you to invoke RPC functions with raw JSON data in the payload:

1
2
3
4
curl "http://127.0.0.1:7350/v2/rpc/http_handler_path?http_key=defaulthttpkey&unwrap" \
	-d '{"some": "data"}' \
	-H 'Content-Type: application/json' \
	-H 'Accept: application/json'

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.

In Lua, you should use the following request names for register_req_before and register_req_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, have a look at “apigrpc.proto”.

You should use the following message names for register_rt_before and register_rt_after hooks:

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 “real-time.proto”.

Examples

In this section we’ll provide a few code examples in the programming languages that can be interpreted by the server runtimes.

RPC example

The following example will show you how to create and register code to be run by a client as an RPC call.

In the Lua example create a module called “example.lua”. Import the "nakama" module which is embedded within the server and contains lots of server-side functions which are helpful as you build your code. You can see all available functions in the reference.

In the Go example we import the runtime package and use the NakamaModule which has all the same functions as in the reference.

The TypeScript example follows a similar pattern to the Go runtime; all custom code must be within the scope of a globally defined InitModule function.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
local nk = require("nakama")

local function some_example(context, payload)
	-- we'll assume the payload was sent as JSON and decode it.
	local json = nk.json_decode(payload)

	-- log data sent to RPC call.
	nk.logger_info(string.format("Payload: %q", json))

	local id = nk.uuid_v4()
	-- create a leaderboard with the json as metadata.
	nk.leaderboard_create(id, "desc", "best", "0 0 * * 1", json, false)

	return nk.json_encode({["id"] = id})
	-- will return "{'id': 'some UUID'}" (JSON) as bytes
end

nk.register_rpc(some_example, "my_unique_id")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
    "context"
    "database/sql"
    "encoding/json"
    "github.com/heroiclabs/nakama-common/runtime"
)

// All Go modules must have a InitModule function with this exact signature.
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
    // Register the RPC function.
    if err := initializer.RegisterRpc("my_unique_id", SomeExample); err != nil {
        logger.Error("Unable to register: %v", err)
        return err
    }
    return nil
}

func SomeExample(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
    meta := make(map[string]interface{})
    // Note below, json.Unmarshal can only take a pointer as second argument
    if err := json.Unmarshal([]byte(payload), &meta); err != nil {
        // Handle error
        return "", err
    }

    id := "SomeId"
    authoritative := false
    sort := "desc"
    operator := "best"
    reset := "0 0 * * 1"

    if err := nk.LeaderboardCreate(ctx, id, authoritative, sort, operator, reset, meta); err != nil {
        // Handle error
        return "", err
    }

    return "Success", nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let InitModule: nkruntime.InitModule =
function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
	initializer.registerRpc('my_unique_id', createLeaderboardFn);
}

let createLeaderboardRpc: nkruntime.RpcFunction =
function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
	let json = JSON.parse(payload);

	logger.debug('user_id: %s, payload: %q', ctx.userId, json);

	let id = 'level1';
	let authoritative = false;
	let sort = nkruntime.SortOrder.ASCENDING;
	let operator = nkruntime.Operator.BEST;
	let reset = '0 0 * * 1';

	try {
		nk.leaderboardCreate(id, authoritative, sort, operator, reset, json);
	} catch (error) {
		logger.error('Failed to create leaderboard: %s', error.message);
		return JSON.stringify(error);
	}

	logger.info('Leaderboard with id: %s created', id);
	return JSON.stringify({ id });
}

Example module

As a fun example let’s use the Pokéapi and build a helpful module named “pokeapi.lua”.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
local nk = require("nakama")

local M = {}

local API_BASE_URL = "https://pokeapi.co/api/v2"

function M.lookup_pokemon(name)
  local url = string.format("%s/pokemon/%s", API_BASE_URL, name)
  local method = "GET"
  local headers = {
    ["Content-Type"] = "application/json",
    ["Accept"] = "application/json"
  }

  local success, code, _, body = pcall(nk.http_request, url, method, headers, nil)

	if (not success) then
    nk.logger_error(string.format("Failed request %q", code))
    error(code)
  elseif (code >= 400) then
    nk.logger_error(string.format("Failed request %q %q", code, body))
    error(body)
  else
    return nk.json_decode(body)
  end
end

return M

-- We can import the code up to this point into another module we'll call "pokemon.lua" which will register an RPC call.
local nk = require("nakama")
local pokeapi = require("pokeapi")

local function get_pokemon(_, payload)
  -- We'll assume payload was sent as JSON and decode it.
  local json = nk.json_decode(payload)
  local success, result = pcall(pokeapi.lookup_pokemon, json.PokemonName)

	if (not success) then
    error("Unable to lookup pokemon.")
  else
    local pokemon = {
      name = result.name,
      height = result.height,
      weight = result.weight,
      image = result.sprites.front_default
    }

    return nk.json_encode(pokemon)
  end
end

nk.register_rpc(get_pokemon, "get_pokemon")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import (
		"context"
		"database/sql"
		"encoding/json"
		"errors"
		"io/ioutil"
		"net/http"

		"github.com/heroiclabs/nakama-common/runtime"
)

const apiBaseUrl = "https://pokeapi.co/api/v2"

// All Go modules must have a InitModule function with this exact signature.
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
		// Register the RPC function.
		if err := initializer.RegisterRpc("get_pokemon", GetPokemon); err != nil {
				logger.Error("Unable to register: %v", err)
				return err
		}

		return nil
	}

func LookupPokemon(logger runtime.Logger, name string) (map[string]interface{}, error) {
		resp, err := http.Get(apiBaseUrl + "/pokemon/" + name)

		if err != nil {
				logger.Error("Failed request %v", err.Error())
				return nil, err
		}

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)

		if err != nil {
				logger.Error("Failed to read body %v", err.Error())
				return nil, err
		}

		if resp.StatusCode >= 400 {
				logger.Error("Failed request %v %v", resp.StatusCode, body)
				return nil, errors.New(string(body))
		}

		var result map[string]interface{}
		err = json.Unmarshal(body, &result)

		return result, err
}

func GetPokemon(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		// We'll assume payload was sent as JSON and decode it.
		var input map[string]string
		err := json.Unmarshal([]byte(payload), &input)

		if err != nil {
				return "", err
		}

		result, err := LookupPokemon(logger, input["PokemonName"])

		if err != nil {
				return "", err
		}

		response, err := json.Marshal(result)

		if err != nil {
				return "", err
		}

		return string(response), nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const apiBaseUrl = 'https://pokeapi.co/api/v2';

let InitModule: nkruntime.InitModule =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
	initializer.registerRpc('get_pokemon', getPokemon);
};

function lookupPokemon(nk: nkruntime.Nakama, name: string) {
	let url = apiBaseUrl + '/pokemon/' + name;
	let headers = { 'Accept': 'application/json' };
	let response = nk.httpRequest(url, 'get', headers);

	return JSON.parse(response.body);
}

let getPokemon: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	// We'll assume payload was sent as JSON and decode it.
	let json = JSON.parse(payload);
	let pokemon;

	try {
		pokemon = lookupPokemon(nk, json['PokemonName']);
	} catch (error) {
		logger.error('An error occurred looking up pokemon: %s', error.message);
		throw error;
	}

	let result = {
		name: pokemon.name,
		height: pokemon.height,
		weight: pokemon.weight,
		image: pokemon.sprites.front_default,
	}

	return JSON.stringify(result);
}

We can now make an RPC call for a Pokémon from a client:

Client
1
2
3
curl "http://127.0.0.1:7350/v2/rpc/get_pokemon" \
  -H 'authorization: Bearer <session token>'
  -d '"{\"PokemonName\": \"dragonite\"}"'
Client
1
2
3
4
const payload = { "PokemonName": "dragonite"};
const rpcid = "get_pokemon";
const pokemonInfo = await client.rpc(session, rpcid, payload);
console.log("Retrieved pokemon info: %o", pokemonInfo);
Client
1
2
3
4
var payload = JsonWriter.ToJson(new { PokemonName = "dragonite" });
var rpcid = "get_pokemon";
var pokemonInfo = await client.RpcAsync(session, rpcid, payload);
System.Console.WriteLine("Retrieved pokemon info: {0}", pokemonInfo);
Client
1
2
3
4
5
6
7
8
auto successCallback = [](const NRpc& rpc)
{
  	std::cout << "Retrieved pokemon info: " << rpc.payload << std::endl;
};

string payload = "{ \"PokemonName\": \"dragonite\" }";
string rpcid = "get_pokemon";
client->rpc(session, rpcid, payload, successCallback);
Client
1
2
3
4
5
6
Map<String, String> payloadData = new HashMap<>();
payloadData.put("PokemonName", "dragonite");
String payload = new Gson().toJson(payloadData, payloadData.getClass());
String rpcid = "get_pokemon";
Rpc pokemonInfo = client.rpc(session, rpcid, payload);
System.out.format("Retrieved pokemon info: %s", pokemonInfo.getPayload());
Client
1
2
3
4
5
6
7
var payload = {"PokemonName": "dragonite"}
var rpc_id = "get_pokemon"
var pokemon_info : NakamaAPI.ApiRpc = yield(client.rpc_async(session, rpc_id, JSON.print(payload)), "completed")
if pokemon_info.is_exception():
	print("An error occurred: %s" % pokemon_info)
	return
print("Retrieved pokemon info: %s" % [parse_json(pokemon_info.payload)])
Client
1
2
3
4
5
6
7
8
POST /v2/rpc/get_pokemon
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "PokemonName": "dragonite"
}
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.