Server Framework

The server includes a fast embedded code runtime where you can write custom logic with Go plugins, Lua modules, or as a JavaScript bundle.

Go plugins must be compiled before they can be loaded by Nakama. The build process is different for binary and Docker-based Nakama installations. You can find out more in these instructions.

The runtime framework is essential to write server-side game logic for your games or apps. You can write code you would not want to run on client devices or the browser. This code you deploy with the server can be used immediately by clients so you can change behavior on the fly and add new features faster.

You should 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 can be joined. It can be used to run authoritative logic or perform validation checks as well as integrate with other services over HTTPS.

Load modules

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.

1
nakama --runtime.path "$HOME/some/path/"

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 followed by Lua and finally JavaScript which ensures deterministic behavior if match handlers or RPC functions/hooks are registered in multiple runtimes. This approach gives the developer the flexibility to leverage the different runtimes as best suited and have them work seamlessly together. As an example, you can define an RPC function in the JavaScript runtime to create a match with a set of match handlers written in Go.

Go

The Go runtime looks for a Go plugin “.so” shared object file, either in the default file path or in the runtime path set. To learn how you can generate the “.so” file with your custom Go runtime code follow these steps.

Lua

The Lua runtime will interpret and load any “.lua” files in the default file path, or in the runtime path set. Each Lua file represents a module and all code in each module will be run and can be used to register functions.

JavaScript

The JavaScript runtime expects an “index.js” file in the default file path, or in the runtime path set. 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/foo.js"

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

We provide a guide on how to get started with the JavaScript runtime by writing your code in TypeScript and using the compiler to generate a “.js” bundle that can be interpreted by Nakama. The server support for JavaScript has been built to directly consider the use of TypeScript for your code and is the recommended way to develop your JavaScript code.

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 (like creating a new table) or to register with third party services.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
nk.run_once(function(context)
  -- This is to create a system ID that cannot be used via a client.
  local system_id = context.env["SYSTEM_ID"]

  nk.sql_exec([[
INSERT INTO users (id, username)
VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
  ]], { system_id, "system_id" })
end)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // This is to create a system ID that cannot be used via a client.
  var systemId string
  if env, ok := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string); ok {
    systemId = env["SYSTEM_ID"]
  }

  _, err := db.ExecContext(ctx, `
INSERT INTO users (id, username)
VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
  `, systemId, "sysmtem_id")
  if err != nil {
    logger.Error("Error: %s", err.Error())
  }

  return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let InitModule: nkruntime.InitModule =
        function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
    let systemId: string = ctx.env["SYSTEM_ID"]

    nk.sqlExec(`
INSERT INTO users (id, username)
VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
    `, { systemId, "system_id" })

    logger.Info('system id: %s', systemId)
}

Errors and logs

Error handling in Go follows the standard pattern of returning an error value as the last argument of a function call. If the error is nil then the call was successful.

Lua error handling uses raised errors rather than error return values. If you want to trap the error which occurs in the execution of a function you’ll need to execute it via pcall as a “protected call”.

JavaScript uses exceptions to handle errors. When an error occurs, an exception is thrown. To handle an exception thrown by a custom function or one provided by the runtime, you must wrap the code in a try catch block.

Server
1
2
3
4
5
6
7
8
9
local function will_error()
  error("This function will always throw an error!")
end

if pcall(will_error) then
  -- No errors with "will_error".
else
  -- Handle errors.
end
Server
1
2
3
4
5
6
7
8
9
func willError() error {
  return errors.New("This function will always throw an error!")
}

if err := willError(); err != nil {
  // Handle errors.
} else {
  // No errors with "willError".
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function throws(): void {
    throw Error("I'm an exception");
}

try {
    throws();
} catch(error) {
    // Handle error.
    logger.error('Caught exception: %s', error.message);
}

The function will_error uses the error function in Lua to throw an error with a reason message. The pcall will invoke the will_error function and trap any errors. We can then handle the success or error cases as needed. We recommend you use this pattern with your Lua code.

Unhandled exceptions in JavaScript are caught and logged by the runtime, except if they are not handled during initialization (when the runtime invokes the InitModule function at startup) - these will halt the server. We recommend you use this pattern and wrap all runtime API calls for error handling and inspection.

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

local status, result = pcall(nk.users_get_username, {"22e9ed62"})
if (not status) then
  nk.logger_error(string.format("Error occurred: %q", result))
else
  for _, u in ipairs(result)
  do
    local message = string.format("id: %q, display name: %q", u.id, u.display_name)
    nk.logger_info(message) -- Will appear in logging output.
  end
end
Server
1
2
3
4
5
6
7
8
users, err := nk.UsersGetUsername([]string{"22e9ed62"})
if err != nil {
  logger.Error("Error occurred: %v", err.Error())
} else {
  for _, u := range users {
    logger.Info("id: %v, display name: %v", u.Id, u.DisplayName) // Will appear in logging output.
  }
}
Server
1
2
3
4
5
6
try {
    // Will throw an exception because this function expects a valid user ID.
    nk.accountsGetId([ 'invalid_id' ]);
} catch(error) {
    logger.error('An error has occurred: %s', error.message);
}

The JavaScript logger is a wrapper around the server logger, in the examples you’ve seen formatting “verbs” (e.g. “%s”) in the output strings, followed by the arguments that will replace them. If you wish to better log and inspect the underlying Go structs used by the JavaScript VM you can use verbs such as “%#v”. The full reference can be found here.

If the server logger level is set to info (default level) or below, the server will return Lua stack traces to the client. This is useful for debugging but should be disabled for production.