Initialize a New User #

It’s often useful when a new user registers to have a bunch of records setup for them. In games this could be needed for a user’s virtual wallet, initial inventory items, etc. In this tutorial we’ll cover a few different ways to handle this use case.

After register callback #

The simplest approach is to write records in the success callback for the register function in a client.

This code demonstrates how to do it with a condensed example. In real application code you’ll break up the authentication and connect logic from the storage writes based on how you manage connect and reconnect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var deviceId = SystemInfo.deviceUniqueIdentifier;
var session = await client.AuthenticateDeviceAsync(deviceId);

var json = "{\"coins\": 100, \"gems\": 10, \"artifacts\": 0}";
var object = new WriteStorageObject = {
  "collection" = "wallets",
  "key" = "mywallet",
  "value" = json
};
const storageWriteAck = await client.WriteStorageObjectsAsync(session, objects);
Debug.Log("Successfully setup new user's records.");

This code has trade-offs which should be noted. A disconnect can happen before the records are written to storage. This may leave the setup of the user incomplete and the application in a bad state.

This option is only worth choosing when you want to avoid writing server-side code or have built retry logic on top of a client.

Server-side hook #

Another way to write records for the new user is to run server-side code after registration has completed. This can be done with a register hook.

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 from 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 from 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
}
Code snippet for this language TypeScript has not been found. Please choose another language to show equivalent examples.

This approach avoids the trade-off with client disconnects but requires a database write to happen after every login or register message. This could be acceptable depending on how frequently you write data to the storage engine and can be minimized if you cache a user’s session for quick reconnects.