Heroic Labs Documentation

Server Runtime Examples #

Server to server #

Unlike client calls, server-to-server calls will never have a user ID, so for any functions you do not want accessible from clients just return an error if a user ID is found in the context.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func ServerRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)

	if ok && userId != "" {
		logger.Error("rpc was called by a user")
		return "", runtime.NewError("rpc is only callable via server to server", 7)
	}

	// Valid server to server RPC call, continue executing the RPC...
	return "<JsonResponse>", nil
}
Server
1
2
3
4
5
6
7
8
9
const serverRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) : string | void {
  if (ctx.userId != "") {
    logger.error("rpc was called by a user");
    return null;
  }
  
  // Valid server to server RPC call, continue executing the RPC...
  return "<JsonResponse>";
}
Server
1
2
3
4
5
6
7
8
9
local server_rpc = function(context, payload)
    if context.user_id and not context.user_id == "" then
        nk.logger_error("rpc was called by a user")
        return nil
    end

    -- Valid server to server RPC call, continue executing the RPC...
    return "<JsonResponse>"
end

Additionally, it can be 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 an RPC hook and 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'

Initialize a user #

User a register hook to write records for the new user after their registration has completed.

The “register_after” hook can be used with one of the "authenticaterequest_*" message types to tell the server to run a function after that message has been processed. It’s important to note that the server does not distinguish between register and login messages so we use a conditional write to store the records.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
local function initialize_user(context, payload)
  if payload.created then
    -- Only run this logic if the account that has authenticated is new.
    local changeset = {
      coins = 10,   -- Add 10 coins to the user's wallet.
      gems = 5      -- Add 5 gems to the user's wallet.
      artifacts = 0 -- No artifacts to start with.
    }
    local metadata = {}
    nk.wallet_update(context.user_id, changeset, metadata, true)
  end
end

-- change to whatever message name matches your authentication type.
nk.register_req_after(initialize_user, "AuthenticateDevice")
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
func InitializeUser(ctx context.Context, logger Logger, db *sql.DB, nk NakamaModule, out *api.Session, in *api.AuthenticateDeviceRequest) error {
  if out.Created {
    // Only run this logic if the account that has authenticated is new.
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
      return "", errors.New("Invalid context")
    }
    changeset := map[string]interface{}{
      "coins": 10,    // Add 10 coins to the user's wallet.
      "gems":  5,     // Add 5 gems to the user's wallet.
      "artifacts": 0, // No artifacts to start with.
    }
    var metadata map[string]interface{}
    if err := nk.WalletUpdate(ctx, userID, changeset, metadata, true); err != nil {
      // Handle error.
    }
  }
}

// Register as after hook, this call should be in InitModule.
if err := initializer.RegisterAfterAuthenticateDevice(InitializeUser); 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
let initializeUser : nkruntime.AfterHookFunction<nkruntime.Session, nkruntime.AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, out: nkruntime.Session, data: nkruntime.AuthenticateDeviceRequest) : nkruntime.Session {
  const changeset = {
    "coins": 10, // Add 10 coins to the user's wallet
    "gems": 5, // Add 5 gems to the user's wallet
    "artifacts": 0 // No artifacts to start with
  };
  
  nk.walletUpdate(ctx.userId, changeset, null, true);
  return out;
};

// Register as after hook, this call should be in InitModule.
initializer.registerAfterAuthenticateDevice(initializeUser);

Storage #

Writing to storage #

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 AuthoritativeWriteRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, _ := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)

	data := map[string]interface{}{
		"achievementPoints": 100,
		"unlockedAchievements": []string{"max-level", "defeat-boss-2", "equip-rare-gear"},
	}

	bytes, err := json.Marshal(data)
	if err != nil {
		return "", runtime.NewError("error marshaling data", 13)
	}

	write := &runtime.StorageWrite{
		Collection:      "Unlocks",
		Key:             "Achievements",
		UserID:          userID,
		Value:           string(bytes),
		PermissionRead:  1, // Only the server and owner can read
		PermissionWrite: 0, // Only the server can write
	}

	_, err = nk.StorageWrite(ctx, []*runtime.StorageWrite{write})
	if err != nil {
		return "", runtime.NewError("error saving data", 13)
	}
	
	return "<JsonResponse>", nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let authoritativeWriteRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) : string | void {
  const data = {
    achievementPoints: 100,
    unlockedAchievements: ['max-level', 'defeat-boss-2', 'equip-rare-gear']
  };

  const write : StorageWriteRequest = {
    collection: 'Unlocks',
    key: 'Achievements',
    userId: ctx.userId,
    value: data,
    permissionRead: 1, // Only the server and owner can read
    permissionWrite: 0 // Only the server can write
  };

  nk.storageWrite([write]);

  return "<JsonResponse>";
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
local authoritative_write_rpc = function(context, payload)
    local data = {
        ["achievementPoints"] = 100,
        ["unlockedAchievements"] = { "max-level", "defeat-boss-2", "equip-rare-gear" }
    }

    local write = {
        collection = "Unlocks",
        key = "Achievements",
        user_id = context.user_id,
        value = data,
        permission_read = 1,
        permission_write = 0
    }
    
    nk.storage_write({ write })

    return "<JsonResponse>"
end

Related Pages