Using Hooks #

Hooks are a feature of the Nakama server runtime that allow you to run custom server code before or after certain server events happen. This can be useful in a number of scenarios, some of which are given below as examples.

All registration calls below are to be run inside the InitModule function.

For more information on hooks and how they can be used see the hooks documentation.

Message names #

Provided here is a full list of server messages that can benefit from hooks.

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.
DeleteAccountDelete the current user’s account.
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.
PartyCreateA client to server request to create a party.
PartyJoinA client to server request to join a party.
PartyLeaveA client to server request to leave a party.
PartyPromoteA client to server request to promote a party member.
PartyAcceptA client to server request to accept a party join request.
PartyRemoveA client to server request to remove a party member.
PartyCloseA client to server request to close a party.
PartyJoinRequestListA client to server request to list all join requests for a particular party.
PartyMatchmakerAddSubmit a new party matchmaking process request.
PartyMatchmakerRemoveCancel a party matchmaking process using a ticket.
PartyDataSendA client to server request to send data to a party.
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.

Before hooks #

These hooks run before a particular server runtime event occurs. This can be used to modify the input to a particular function or take additional actions before it occurs.

Before creating a group #

This hook checks that a group name does not contain profanity before allowing it to be created. Note that profanity checking functionality is not provided by Nakama.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
initializer.registerBeforeCreateGroup(beforeCreateGroup);

let beforeCreateGroup : nkruntime.BeforeHookFunction<nkruntime.CreateGroupRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.CreateGroupRequest) : nkruntime.CreateGroupRequest {
  // Check the group name does not contain profanity (containsProfanity implementation not provided)
  if (containsProfanity(data.name)) {
    throw new Error("Profanity detected.")
  }

  return data;
};
Server
1
2
3
4
5
6
7
initializer.RegisterBeforeCreateGroup(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error) {
  if containsProfanity(in.Name) {
    return nil, runtime.NewError("profanity detected", 3)
  }
  
  return in, nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Before deleting a group #

This hook denies an attempt to delete a group that still contains more than 1 user.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
initializer.registerBeforeDeleteGroup(beforeDeleteGroup);

let beforeDeleteGroup : nkruntime.BeforeHookFunction<nkruntime.DeleteGroupRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.DeleteGroupRequest) : nkruntime.DeleteGroupRequest {
  // Deny the delete request if the group still has more than one user
  const result = nk.groupUsersList(data.groupId, null, null, null);
  if (result.groupUsers.length > 1) {
      throw new Error("Group still has users.")
  }

  return data;
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
initializer.RegisterBeforeDeleteGroup(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error) {
  groupUsers, _, err := nk.GroupUsersList(ctx, in.GroupId, 100, nil, "")
  if err != nil {
    return nil, runtime.NewError("error retrieving group users", 13)
  }

  if len(groupUsers) > 1 {
    return nil, runtime.NewError("group still has users", 9)
  }
  
  return in, nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

After hooks #

These hooks run after a particular server runtime event occurs. This can be used to respond to an event appropriately after it has happened.

After adding friends #

This hook sends a notification to each user that has been added as a friend by another user, letting them know that they have received a friend request.

Server
1
2
3
4
5
6
7
8
initializer.registerAfterAddFriends(afterAddFriends);

let afterAddFriends: nkruntime.AfterHookFunction<void, nkruntime.AddFriendsRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: void, request: nkruntime.AddFriendsRequest) {
  // Notify each user that they have received a friend request
  request.ids.forEach(function (id) {
    nk.notificationSend(id, 'Friend Request Received', { message: `You have received a friend request from ${ctx.username}.` }, 1, null, true);
  });
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
initializer.RegisterAfterAddFriends(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) error {
  username, ok := ctx.Value(runtime.RUNTIME_CTX_USERNAME).(string)
  if !ok {
    return runtime.NewError("error getting username", 13)
  }
  
  for _, friendId := range in.Ids {
    content := map[string]interface{} {
      "message": fmt.Sprintf("You have received a friend request from %s", username),
    }
    
    nk.NotificationSend(ctx, friendId, "Friend Request Received", content, 1, nil, true)
  }
  
  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

After leaving a group #

This hook sends a message to a group notifying the group users that a particular user has left.

Server
1
2
3
4
5
6
7
initializer.registerAfterLeaveGroup(afterLeaveGroup);

let afterLeaveGroup: nkruntime.AfterHookFunction<void, nkruntime.LeaveGroupRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: void, request: nkruntime.LeaveGroupRequest) {
  // Send a message to the group to say the player left
  const channelId = nk.channelIdBuild(null, request.groupId, nkruntime.ChanType.Group);
  nk.channelMessageSend(channelId, { message: `${ctx.username} left the group.` }, null, null, true);
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
initializer.RegisterAfterLeaveGroup(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.LeaveGroupRequest) error {
  username, ok := ctx.Value(runtime.RUNTIME_CTX_USERNAME).(string)
  if !ok {
    return runtime.NewError("error getting username", 13)
  }
  
  channelId, err := nk.ChannelIdBuild(ctx, nil, in.GroupId, runtime.Group)
  if err != nil {
    return runtime.NewError("error getting channel id", 13)
  }
  
  content := map[string]interface{}{
    "message": fmt.Sprintf("%s left the group.", username),
  }
  
  nk.ChannelMessageSend(ctx, channelId, content, nil, nil, true)
  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

After authenticating by device ID #

This hook rewards a player with 10 coins (in their virtual wallet whenever they authenticate by device ID.

Server
1
2
3
4
5
6
initializer.registerAfterAuthenticateDevice(afterAuthenticateDevice);

let afterAuthenticateDevice: nkruntime.AfterHookFunction<nkruntime.Session, nkruntime.AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.Session, request: nkruntime.AuthenticateDeviceRequest) {
  // Give the player 10 coins for logging in
  nk.walletUpdate(ctx.userId, { coins: 10 }, null, true);
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
initializer.RegisterAfterAuthenticateDevice(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session, in *api.AuthenticateDeviceRequest) error {
  // Give the player 10 coins for logging in
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    return runtime.NewError("error getting userId", 13)
  }
  
  changeset := map[string]int64{
    "coins": 10,
  }
  
  nk.WalletUpdate(ctx, userId, changeset, nil, true)
  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Event hooks #

These hooks run after a particular event has happened, such as a leaderboard being reset or a tournament ending.

On leaderboard reset #

This hook rewards the top 3 scoring players in a leaderboard with 1,000 coins whenever the leaderboard resets.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
initializer.registerLeaderboardReset(onLeaderboardReset);

let onLeaderboardReset : nkruntime.LeaderboardResetFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, leaderboard: nkruntime.Leaderboard, reset: number) {
  // Reward top 3 players with 1,000 coins and send them a notification telling them what their rank was
  const topRecords = nk.leaderboardRecordsList(leaderboard.id, null, 3, null, null);
  topRecords.records.forEach(function (record) {
    nk.walletUpdate(record.ownerId, { coins: 1000 }, null, true);
    nk.notificationSend(record.ownerId, 'Congratulations', { message: `Well done, you ranked ${record.rank}!` }, 2, null, true);
  });
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
initializer.RegisterLeaderboardReset(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, leaderboard *api.Leaderboard, reset int64) error {
  // Reward top 3 players with 1,000 coins and send them a notification telling them what their rank was
  topRecords, _, _, _, err := nk.LeaderboardRecordsList(ctx, leaderboard.Id, nil, 3, "", 0)
  if err != nil {
    return runtime.NewError("error getting leaderboard records", 13)
  }

  for _, record := range topRecords {
    changeset := map[string]int64 {
      "coins": 1000,
    }
    
    content := map[string]interface{} {
      "message": fmt.Sprintf("Well done, you ranked %d!", record.Rank),
    }
    
    nk.WalletUpdate(ctx, record.OwnerId, changeset, nil, true)
    nk.NotificationSend(ctx, record.OwnerId, "Congratulations", content, 2, "", true)
  }
  
  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

On tournament end #

This hook rewards the winner of a tournament with 10,000 coins and gives them a rare item when the tournament ends.

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
initializer.registerTournamentEnd(onTournamentEnd);

let onTournamentEnd : nkruntime.TournamentEndFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, tournament: nkruntime.Tournament, end: number, reset: number) {
  // Reward tournament leader with 10,000 coins and a rare item
  const topRecord = nk.tournamentRecordsList(tournament.id, null, 1, null, null);
  const winnerId = topRecord.records[0].ownerId;

  // Give the player 10,000 coins
  nk.walletUpdate(winnerId, { coins: 10000 }, null, true);

  // Get the player's existing inventory
  const storageRead : nkruntime.StorageReadRequest = {
    collection: "inventory",
    key: winnerId,
    userId: winnerId
  };

  const result = nk.storageRead([storageRead]);
  let inventory = {};

  if (result.length > 0) {
    inventory = result[0].value;
  }

  // Add the rare item or increase quantity
  if (!inventory["rare-sword"]) {
    inventory["rare-sword"] = 1;
  } else {
    inventory["rare-sword"]++;
  }

  // Write the updated inventory to the storage engine
  const storageWrite : nkruntime.StorageWriteRequest = {
    collection: "inventory",
    key: winnerId,
    userId: winnerId,
    permissionWrite: 0,
    permissionRead: 1,
    value: inventory
  }

  nk.storageWrite([storageWrite]);
};
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
initializer.RegisterTournamentEnd(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, tournament *api.Tournament, end, reset int64) error {
  // Reward tournament leader with 10,000 coins and a rare item
  topRecord, _, _, _, err := nk.TournamentRecordsList(ctx, tournament.Id, nil, 1, "", 0)
  if err != nil {
    return runtime.NewError("error getting tournament records", 13)
  }

  winnerId := topRecord[0].OwnerId

  // Give the player 10,000 coins
  changeset := map[string]int64 {
    "coins": 10000,
  }
  nk.WalletUpdate(ctx, winnerId, changeset, nil, true)

  // Get the player's existing inventory
  storageRead := &runtime.StorageRead{
    Collection: "inventory",
    Key: winnerId,
    UserID: winnerId,
  }

  result, err := nk.StorageRead(ctx, []*runtime.StorageRead { storageRead })
  if err != nil {
    return runtime.NewError("error reading player inventory", 13)
  }

  var inventory map[string]int

  if len(result) > 0 {
    if err := json.Unmarshal([]byte(result[0].Value), &inventory); err != nil {
      logger.Error("error unmarshaling inventory", err)
      return runtime.NewError("error unmarshaling inventory", 13)
    }
  } else {
    inventory = make(map[string]int)
  }

  // Add the rare item or increase quantity
  if _, ok := inventory["rare-sword"]; !ok {
    inventory["rare-sword"] = 1
  } else {
    inventory["rare-sword"] += 1
  }
  
  // Write the updated inventory to the storage engine
  inventoryJson, err := json.Marshal(inventory)
  if err != nil {
    return runtime.NewError("error marshaling inventory", 13)
  }
  
  storageWrite := &runtime.StorageWrite{
    Collection: "inventory",
    Key: winnerId,
    UserID: winnerId,
    PermissionWrite: 0,
    PermissionRead: 1,
    Value: string(inventoryJson),
  }
  
  _, err = nk.StorageWrite(ctx, []*runtime.StorageWrite { storageWrite })
  if err != nil {
    return runtime.NewError("error writing inventory", 13)
  }

  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Before realtime hooks #

These hooks run before a specific realtime message is processed by the server.

Before channel join #

This hook checks to see if a user is trying to join a Direct Message channel for a non-friend. If so, it blocks their request to join the channel.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
initializer.registerRtBefore("ChannelJoin", beforeChannelJoin);

let beforeChannelJoin : nkruntime.RtBeforeHookFunction<nkruntime.EnvelopeChannelJoin> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.EnvelopeChannelJoin) : nkruntime.EnvelopeChannelJoin | void {
  // If the channel join is a DirectMessage type, check to see if the user is friends with the recipient first
  if (envelope.channelJoin.type == nkruntime.ChanType.DirectMessage) {
    const result = nk.friendsList(ctx.userId, null, 0, null);
    const filtered = result.friends.filter(function (friend) {
      return friend.user.userId == envelope.channelJoin.target;
    });

    if (filtered.length == 0) {
      throw new Error("You cannot direct message someone you are not friends with.");
    }
  }

  return envelope;
};
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
initializer.RegisterBeforeRt("ChannelJoin", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *rtapi.Envelope) (*rtapi.Envelope, error) {
  envelope, ok := in.Message.(*rtapi.Envelope_ChannelJoin)
  if !ok {
    return nil, runtime.NewError("error getting envelope as ChannelJoin envelope", 13)
  }

  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    return nil, runtime.NewError("error getting userId", 13)
  }

  // If the channel join is a DirectMessage type, check to see if the user is friends with the recipient first
  if envelope.ChannelJoin.Type == 2 {
    state := 0
    friends, _, err := nk.FriendsList(ctx, userId, 100, &state, "")
    if err != nil {
      return nil, runtime.NewError("error getting friends list", 13)
    }

    isFriends := false
    for _, friend := range friends {
      if friend.User.Id == envelope.ChannelJoin.Target {
        isFriends = true
        break
      }
    }

    if !isFriends {
      return nil, runtime.NewError("you cannot direct message someone you are not friends with.", 9)
    }
  }

  return in, nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

After realtime hooks #

These hooks run after a specific realtime message is processed by the server.

After channel leave #

This hook lets a recipient know that the other user has left the channel after that user has left.

Server
1
2
3
4
5
6
initializer.registerRtAfter("ChannelLeave", afterChannelLeave);

let afterChannelLeave : nkruntime.RtAfterHookFunction<nkruntime.EnvelopeChannelLeave> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, output: nkruntime.EnvelopeChannelLeave | null, input: nkruntime.EnvelopeChannelLeave) {
  // Send a message to the channel after leaving, notifying others that the user left
  nk.channelMessageSend(input.channelLeave.channelId, { message: `${ctx.userId} left the channel.` }, "", "", true);
};
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
initializer.RegisterAfterRt("ChannelLeave", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, out, in *rtapi.Envelope) error {
  envelope, ok := in.Message.(*rtapi.Envelope_ChannelLeave)
  if !ok {
    return runtime.NewError("error getting envelope as ChannelLeave envelope", 13)
  }

  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    return runtime.NewError("error getting userId", 13)
  }

  // Send a message to the channel after leaving, notifying others that the user left
  content := map[string]interface{}{
    "message": fmt.Sprintf("%s left the channel.", userId),
  }

  nk.ChannelMessageSend(ctx, envelope.ChannelLeave.ChannelId, content, "", "", true)

  return nil
})
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.