# Using Hooks

**URL:** https://heroiclabs.com/docs/nakama/guides/server-framework/using-hooks/
**Summary:** Summary
**Keywords:** using hooks, nakama
**Categories:** nakama, using-hooks, server-framework

---


# 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](../../../server-framework/introduction/#hooks).

## Message names

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

{{< note "important" >}}
If your runtime code is in Go, refer to [the interface definition](https://github.com/heroiclabs/nakama/blob/master/server/runtime.go) for a full list of hooks that are available in the runtime package.
{{< / note >}}

Use the following request names for registering your [Before](#before-hooks) and [After](#after-hooks) hooks:

{{< table name="nakama.server-framework.basics.request-names" >}}

Names are case-insensitive. For more information, see [`apigrpc.proto`](https://github.com/heroiclabs/nakama/blob/master/apigrpc/apigrpc.proto).

For real-time before and after hooks, use the following message names:

{{< table name="nakama.server-framework.basics.message-names" >}}

Names are case-insensitive. For more information, have a look at [`realtime.proto`](https://github.com/heroiclabs/nakama-common/blob/master/rtapi/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.

{{< code type="server" >}}
```typescript
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;
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function beforeCreateGroup(context, logger, nk, data)
    -- Check the group name does not contain profanity (containsProfanity implementation not provided)
    if containsProfanity(data.name) then
        error("Profanity detected.")
    end

    return data
end

nk.register_req_before(beforeCreateGroup, "CreateGroup")
```
{{< / code >}}
### Before deleting a group

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

{{< code type="server" >}}
```typescript
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;
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function beforeDeleteGroup(context, logger, nk, data)
    -- Deny the delete request if the group still has more than one user
    local result, err = nk.group_users_list(data.groupId, nil, nil, nil)
    if err then
        error("Error retrieving group users.")
    end

    if #result > 1 then
        error("Group still has users.")
    end

    return data
end

nk.register_req_before(beforeDeleteGroup, "DeleteGroup")

```
{{< / code >}}

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

{{< code type="server" >}}
```typescript
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);
  });
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function afterAddFriends(context, logger, nk, data, request)
    -- Notify each user that they have received a friend request
    for _, id in ipairs(request.ids) do
        local content = { message = "You have received a friend request from " .. context.username }
        nk.notification_send(id, "Friend Request Received", content, 1, nil, true)
    end
end

nk.register_after_add_friends(afterAddFriends)
```
{{< / code >}}

### After leaving a group

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

{{< code type="server" >}}
```typescript
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);
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function afterLeaveGroup(context, logger, nk, data, request)
    -- Send a message to the group to say the player left
    local channelId = nk.channel_id_build(nil, request.groupId, nk.channel_type_group)
    local content = { message = context.username .. " left the group." }
    nk.channel_message_send(channelId, content, nil, nil, true)
end

nk.register_req_after(afterLeaveGroup, "LeaveGroup")
```
{{< / code >}}
### After authenticating by device ID

This hook rewards a player with 10 coins (in their [virtual wallet](../../../concepts/user-accounts/#virtual-wallet) whenever they authenticate by device ID.

{{< code type="server" >}}
```typescript
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);
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function afterAuthenticateDevice(context, logger, nk, data, request)
    -- Give the player 10 coins for logging in
    local changeset = { coins = 10 }
    nk.wallet_update(context.user_id, changeset, nil, true)
end

nk.register_req_after(afterAuthenticateDevice, "AuthenticateDevice")
```
{{< / code >}}
## 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.

{{< code type="server" >}}
```typescript
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);
  });
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function onLeaderboardReset(context, logger, nk, leaderboard, reset)
    -- Reward top 3 players with 1,000 coins and send them a notification telling them what their rank was
    local limit = 3
    local cursor = nil
    local expiry = nil
    local ownerId = nil
    local result, err = nk.leaderboard_records_list(leaderboard.id, ownerId, limit, cursor, expiry)
    if err then
        logger.error("Error getting leaderboard records: " .. err.message)
        return
    end

    for _, record in ipairs(result.records) do
        local changeset = { coins = 1000 }
        nk.wallet_update(record.owner_id, changeset, nil, true)
        local content = { message = "Well done, you ranked " .. record.rank .. "!" }
        nk.notification_send(record.owner_id, "Congratulations", content, 2, nil, true)
    end
end

nk.register_leaderboard_reset(onLeaderboardReset)
```
{{< / code >}}

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

{{< code type="server" >}}
```typescript
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]);
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function onTournamentEnd(context, logger, nk, tournament, end, reset)
    -- Reward tournament leader with 10,000 coins and a rare item
    local limit = 1
    local cursor = nil
    local expiry = nil
    local result, err = nk.tournament_records_list(tournament.id, nil, limit, cursor, expiry)
    if err then
        logger.error("Error getting tournament records: " .. err.message)
        return
    end

    if #result.records == 0 then
        logger.info("No records found for the tournament.")
        return
    end

    local winnerId = result.records[1].owner_id

    -- Give the player 10,000 coins
    local changeset = { coins = 10000 }
    local updateErr = nk.wallet_update(winnerId, changeset, nil, true)
    if updateErr then
        logger.error("Error updating wallet: " .. updateErr.message)
        return
    end

    -- Get the player's existing inventory
    local storageRead = {
        collection = "inventory",
        key = winnerId,
        user_id = winnerId
    }

    local storageResult, storageErr = nk.storage_read({ storageRead })
    if storageErr then
        logger.error("Error reading player inventory: " .. storageErr.message)
        return
    end

    local inventory = {}
    if #storageResult > 0 then
        inventory = storageResult[1].value
    end

    -- Add the rare item or increase quantity
    if not inventory["rare-sword"] then
        inventory["rare-sword"] = 1
    else
        inventory["rare-sword"] = inventory["rare-sword"] + 1
    end

    -- Write the updated inventory to the storage engine
    local storageWrite = {
        collection = "inventory",
        key = winnerId,
        user_id = winnerId,
        permission_write = 0,
        permission_read = 1,
        value = inventory
    }

    local writeErr = nk.storage_write({ storageWrite })
    if writeErr then
        logger.error("Error writing inventory: " .. writeErr.message)
        return
    end
end

nk.register_tournament_end(onTournamentEnd)
```
{{< / code >}}

## Before realtime hooks

These hooks run _before_ a specific [realtime message](../../../server-framework/introduction/hooks/#message-names) 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.

{{< code type="server" >}}
```typescript
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;
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function beforeChannelJoin(context, logger, nk, envelope)
    -- If the channel join is a DirectMessage type, check to see if the user is friends with the recipient first
    if envelope.channel_join.type == nk.channel_type_direct_message then
        local friends, err = nk.friends_list(context.user_id, nil, 0, nil)
        if err then
            logger.error("Error getting friends list: " .. err.message)
            return
        end

        local isFriend = false
        for _, friend in ipairs(friends) do
            if friend.user_id == envelope.channel_join.target then
                isFriend = true
                break
            end
        end

        if not isFriend then
            error("You cannot direct message someone you are not friends with.")
        end
    end

    return envelope
end

nk.register_rt_before("ChannelJoin", ChannelJoin)

```
{{< / code >}}
## After realtime hooks

These hooks run _after_ a specific [realtime message](../../../server-framework/introduction/hooks/#message-names) 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.

{{< code type="server" >}}
```typescript
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);
};
```
{{< / code >}}

{{< code type="server" >}}
```go
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 >}}

{{< code type="server" >}}
```lua
local function afterChannelLeave(context, logger, nk, output, input)
    -- Send a message to the channel after leaving, notifying others that the user left
    local content = { message = context.user_id .. " left the channel." }
    nk.channel_message_send(input.channel_leave.channel_id, content, "", "", true)
end

nk.register_req_after(afterChannelLeave, "ChannelLeave")
```
{{< / code >}}