# Defold

**URL:** https://heroiclabs.com/docs/nakama/client-libraries/defold/
**Summary:** The official Defold client is build using Lua 5.1 on top of the Defold game engine. It can also be used by other engines running Lua. Learn how to setup and use the Defold client for Nakama.
**Keywords:** defold client guide, defold client, nakama defold, authentication, sessions, events, real-world example defold
**Categories:** nakama, defold, client-libraries

---


# Nakama Defold Client Guide

This client library guide will show you how to use the core Nakama features in **Defold** by showing you how to develop the Nakama specific parts (without full game logic or UI) of an [Among Us (external)](https://www.innersloth.com/games/among-us/) inspired game called Sagi-shi (Japanese for "Imposter").

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/gameplay.png" >}} alt="Sagi-shi gameplay screen">
  <figcaption>Sagi-shi gameplay</figcaption>
</figure>

## Prerequisites

Before proceeding ensure that you have:

* [Installed Nakama server](../../getting-started/install/docker/)
* [Downloaded Defold](https://defold.com/download/)
* Installed the [Nakama Defold SDK](https://github.com/heroiclabs/nakama-defold)


### Add the client to your project

Add the URL of a [client release .zip](https://github.com/heroiclabs/nakama-defold/releases) as a library dependency to `game.project`. The client will now show up as a `nakama` folder in your project.

### Add Defold plugins

Defold projects additionally require the following modules:

- https://github.com/defold/extension-websocket/archive/3.0.0.zip


### Asynchronous programming

Many of the Nakama APIs are asynchronous and non-blocking and can be used in the Defold SDK through callback functions or Nakama sync functions.

Sagi-shi can call Nakama functions with or without a callback function:

```lua
-- blocking
local nakama.get_account(client)

-- non-blocking
nakama.get_account(client, function(account)
  print(account.user.id)
end)
```

The Nakama client provides a convenience function for creating and starting a coroutine to run multiple requests synchronously:

```lua
-- non-blocking coroutine
nakama.sync(function()
  -- blocks in the coroutine
  local client = nakama.create_client(config)
  -- blocks in the coroutine
  local nakama.get_account(client)
end)
```


### Handling errors

Network programming requires additional safeguarding against connection and payload issues.

The [server](../../getting-started/configuration/#logger) and the client can generate logs which are helpful for debugging.

To enable verbose logging from the client:

```lua
log = require "nakama.util.log"
-- enable trace logging in nakama client
log.print()
```

For production use
```lua
log.silent() -- disable trace logging in nakama client
```

API calls in Sagi-shi return a result, which could contain an error property you can test for to gracefully handle errors:

```lua
local result = nakama.get_account(client)

if result.error then
    print(result.message)
    return
end
```


## Getting started

Learn how to get started using the Nakama Client and Socket objects to start building Sagi-shi and your own game.


### Nakama Client

The Nakama Client connects to a Nakama Server and is the entry point to access Nakama features. It is recommended to have one client per server per game.

To create a client for Sagi-shi pass in your server connection details:

```lua
local defold = require "nakama.engine.defold"

local config = {
  host = 127.0.0.1,
  port = 7350,
  use_ssl = false,
  username = "defaultkey",
  engine = defold
}

local client = nakama.create_client(config)
```


### Nakama Socket

The Nakama Socket is used for gameplay and real-time latency-sensitive features such as chat, parties, matches and RPCs.

Use the client and create a socket:

```lua
local socket = nakama.create_socket(client)

nakama.sync(function()
    -- connect
    local ok, err = nakama.socket_connect(socket)

    if ok then
      -- do socket stuff
    end

    if err then
      print(err.message)
    end
end)
```


## Authentication

Nakama has many [authentication methods](../../concepts/authentication/) and supports creating [custom authentication](../../concepts/authentication/#custom) on the server.

Sagi-shi will use device and Facebook authentication, linked to the same user account so that players can play from multiple devices.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/login.png" >}} alt="Sagi-shi login screen">
  <figcaption>Login screen and Authentication options</figcaption>
</figure>


### Device authentication

Nakama [Device Authentication](../../concepts/authentication/#device) uses the physical device's unique identifier to easily authenticate a user and create an account if one does not exist.

When using only device authentication, you don't need a login UI as the player can automatically authenticate when the game launches.

Authentication is an example of a Nakama feature accessed from a Nakama Client instance.

```lua
local nakama_session = require "nakama.session"

function authenticate_with_device()
  nakama.sync(function()
    local vars = nil
    local create = true
    local result = nakama.authenticate_device(client, defold.uuid(), vars, create, "mycustomusername")

    if not result.token then
      print("Unable to login")
      return
    end

    -- store the token to use when communicating with the server
    nakama.set_bearer_token(client, result.token)

    -- store the toke on disk

    local session = nakama_session.create(token)
    print(session.user_id)
  end)
end
```


### Facebook authentication

Nakama [Facebook Authentication](../../concepts/authentication/#facebook) is an easy to use authentication method which lets you optionally import the player's Facebook friends and add them to their Nakama Friends list.

{{< note "important" >}}
Install the official [Facebook SDK for Defold (external)](https://www.defold.com/extension-facebook) to use Nakama Facebook Authentication.
{{< /note >}}

```lua
local nakama_session = require "nakama.session"

function authenticate_with_facebook()
  -- Facebook permissions
  local permissions = { "public_profile" }

  -- you don't need a publishing audience with read permissions
  local audience = facebook.AUDIENCE_NONE

  facebook.login_with_permissions(permissions, audience, function(self, data)
    local vars = nil
    local create = true
    local result = nakama.authenticate_facebook(client, facebook.access_token(), vars, create, "mycustomusername")

    if not result.token then
      print("Unable to login")
      return
    end

    -- store the token to use when communicating with the server
    nakama.set_bearer_token(client, result.token)

    -- store the token on disk

    local session = nakama_session.create(token)
    print(session.user_id)
  end)
end
```


### Custom authentication

Nakama supports [Custom Authentication](../../concepts/authentication/#custom) methods to integrate with additional identity services.

See the [Itch.io custom authentication](../snippets/custom-authentication/) recipe for an example.


### Linking authentication

Nakama allows players to [Link Authentication](../../concepts/authentication/#link-or-unlink) methods to their account once they have authenticated.


**Linking Device ID authentication**

```lua
function link_device_authentication()
  local result = nakama.link_device(client, defold.uuid())

  if result.error
    print(result.error)
    return
  end
end
```


**Linking Facebook authentication**

```lua
function link_facebook_authentication()
  -- Facebook permissions
  local permissions = { "public_profile" }

  -- you don't need a publishing audience with read permissions
  local audience = facebook.AUDIENCE_NONE

  facebook.login_with_permissions(permissions, audience, function(self, data)
    local result = nakama.link_facebook(client, facebook.access_token())

    if result.error
      print(result.error)
      return
    end
  end)
end
```


### Session variables

Nakama [Session Variables](../../concepts/session/#session-variables) can be stored when authenticating and will be available on the client and server as long as the session is active.

Sagi-shi uses session variables to implement analytics, referral and rewards programs and more.

Store session variables by passing them as an argument when authenticating:

```lua
local vars = {
  device_os = SystemInfo.operatingSystem,
  devic_model = SystemInfo.deviceModel,
  game_version = Application.version,
  inviter_user_id = "<SomeUserId>"
}

local result = nakama.authenticate_device(client, defold.uuid(), vars)
```

To access session variables on the Client use the `vars` property on the `Session` object:

```lua
local device_os = session.vars.device_os
```


### Session lifecycle

Nakama [Sessions](../../concepts/session/) expire after a time set in your server [configuration](../../getting-started/configuration/#session). Expiring inactive sessions is a good security practice.

Nakama provides ways to restore sessions, for example when Sagi-shi players re-launch the game, or refresh tokens to keep the session active while the game is being played.

Use the auth and refresh tokens on the session object to restore or refresh sessions.

Sagi-shi stores the auth token on disk:

```lua
sys.save(session.token, "token_path")
```

Restore a session without having to re-authenticate:

```lua
local nakama_session = require "nakama.session"

local token = sys.load("token_path")
local session = nakama_session.create(token)

if nakama_session.expired(session) then
  print("Session has expired, re-authenticate.")
else
  nakama.set_bearer_token(client, session.token)
end
```

Check if a session has expired or is close to expiring and refresh it to keep it alive:

```lua
local time_one_day = 60 * 60 * 24

-- check whether a session has expired or is close to expiry.
if nakama_session.expired(session) or os.difftime(session.expires, os.time()) < time_one_day then
  local result = nakama.session_refresh(client, session.token, vars)

  -- authenticate
  if result.error then
    local result = nakama.authenticate_device(client, defold.uuid(), true)
  end
end
```


### Ending sessions

Logout and end the current session:

```lua
local result = nakama.session_logout(client, session.refresh_token, session.token)
```


## User accounts

Nakama [User Accounts](../../concepts/user-accounts/) store user information defined by Nakama and custom developer metadata.

Sagi-shi allows players to edit their accounts and stores metadata for things like game progression and in-game items.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/profile.png" >}} alt="Sagi-shi player profile screen">
  <figcaption>Player profile</figcaption>
</figure>


### Get the user account

Many of Nakama's features are accessible with an authenticated session, like [fetching a user account](../../concepts/user-accounts/#fetch-account).

Get a Sagi-shi player's full user account with their basic [user information](../../concepts/user-accounts/#fetch-account) and user id:

```lua
local result = nakama.get_account(client)

if result.error then
    print(result.message)
    return
end

local user = result.user
local username = user.username
local avatar_url = user.avatar_url
local user_id = user.id
```


### Update the user account

Nakama provides easy methods to update server stored resources like user accounts.

Sagi-shi players need to be able to update their public profiles:

```lua

local newUsername = "NotTheImp0ster"
local display_name = "Innocent Dave"
local avatar_url = "https://example.com/imposter.png"
local lang_tag = "en"
local location = "Edinburgh"
local timezone = "BST"

local result = nakama.update_account(client, avatar_url, display_name, lang_tag, location, timezone, newUsername)

```


### Getting users

In addition to getting the current authenticated player's user account, Nakama has a convenient way to get a list of other players' public profiles from their ids or usernames.

Sagi-shi uses this method to display player profiles when engaging with other Nakama features:

```lua
local ids = { "userid1", "userid2" }
local usernames = { "username1", "username2" }
local facebook_ids = { "facebookid1" }

local result = nakama.get_users(client, ids, usernames, facebook_ids)

if result.error then
  print(result.message)
  return
end

local users = result.users
```


### Storing metadata

Nakama [User Metadata](../../concepts/user-accounts/#user-metadata) allows developers to extend user accounts with public user fields.

User metadata can only be updated on the server. See the [updating user metadata](../snippets/user-metadata/) recipe for an example.

Sagi-shi will use metadata to store what in-game items players have equipped:


### Reading metadata

Read the user account and their metadata will already by decoded in a table:

```lua
local result = nakama.get_account(client)

local title = result.user.metadata.title
local hat = result.user.metadata.hat
local skin = result.user.metadata.skin
```


### Wallets

Nakama [User Wallets](../../concepts/user-accounts/#virtual-wallet) can store multiple digital currencies as key/value pairs of strings/integers.

Players in Sagi-shi can unlock or purchase titles, skins and hats with a virtual in-game currency.


#### Accessing wallets

Parse the JSON wallet data from the user account:

```lua
local account = nakama.get_account(client)
local wallet = json.decode(account.wallet)
print("Wallet:")
for currency, amount in pairs(wallet) do
	print(currency .. ": " .. amount)
end
```

#### Updating wallets

Wallets can only be updated on the server. See the [user account virtual wallet](../../concepts/user-accounts/#virtual-wallet) documentation for an example.


#### Validating in-app purchases

Sagi-shi players can purchase the virtual in-game currency through in-app purchases that are authorized and validated to be legitimate on the server.

See the [In-app Purchase Validation](../../concepts/iap-validation/) documentation for examples.


## Storage Engine

The Nakama [Storage Engine](../../concepts/storage/) is a distributed and scalable document-based storage solution for your game.

The Storage Engine gives you more control over how data can be [accessed](../../concepts/storage/permissions/) and [structured](../../concepts/storage/collections/) in collections.

Collections are named, and store JSON data under a unique key and the user id.

By default, the player has full permission to create, read, update and delete their own storage objects.

Sagi-shi players can unlock or purchase many items, which are stored in the Storage Engine.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/player-items.png" >}} alt="Sagi-shi player items screen">
  <figcaption>Player items</figcaption>
</figure>



### Reading storage objects

Define a class that describes the storage object and create a new storage object id with the collection name, key and user id. Finally, read the storage objects and parse the JSON data:

```lua
local user_id = user.id

local objects = {
  {
    collection = "Unlocks",
    key = "Hats",
    userId = user_id
  }
}
local result = nakama.read_storage_objects(client, objects)

if result.error then
  print(result.message)
  return
end

for _,object in ipairs(result.objects) do
  pprint(object)
end
```

{{< note "important" >}}
To read other players' public storage object, use their user id instead.
Players can only read storage objects they own or that are public (Read permission value of `2`).
{{< /note >}}

### Writing storage objects

Nakama allows developers to write to the Storage Engine from the client and server.

Consider what adverse effects a malicious user can have on your game and economy when deciding where to put your write logic, for example data that should only be written authoritatively (i.e. game unlocks or progress).

Sagi-shi allows players to favorite items for easier access in the UI and it is safe to write this data from the client.

Create a write storage object with the collection name, key and JSON encoded data. Finally, write the storage objects to the Storage Engine:

```lua
local objects = {
  {
    collection = "Favorites",
    key = "Hats",
    value = json.encode({ "cowboy", "alien" }),
    permissionRead = 1,
    permissionWrite = 1,
    version = ""
  }
}
local result = nakama.write_storage_objects(client, objects)

if result.error then
    print(result.message)
    return
end

for _,ack in ipairs(result.acks) do
  pprint(ack)
end
```

### Conditional writes

Storage Engine [Conditional Writes](../../concepts/storage/collections/#conditional-writes) ensure that write operations only happen if the object hasn't changed since you accessed it.

This gives you protection from overwriting data, for example the Sagi-shi server could have updated an object since the player last accessed it.

To perform a conditional write, add a version to the write storage object with the most recent object version:

```lua
-- Assuming we already have a storage object (storage_object)
local objects = {
  {
    collection = storage_object.collection,
    key = storage_object.key,
    value = json.encode({ "cowboy", "alien" }),
    permissionRead = 0,
    permissionWrite = 1,
    version = storage_object.version
  }
}

local result = nakama.write_storage_objects(client, objects)

if result.error then
  print(result.message)
  return
end
```


### Listing storage objects

Instead of doing multiple read requests with separate keys you can list all the storage objects the player has access to in a collection.

Sagi-shi lists all the player's unlocked or purchased titles, hats and skins:

```lua

local user_id = user.id
local limit = 3

local result = nakama.list_storage_objects(client, "Unlocks", user_id, limit)

if result.error then
  print(result.message)
  return
end

local titles = nil
local hats = nil
local skins = nil

for k, object in pairs(result.objects)
  if object.key == "Titles" then
    local titles = object
  else if object.key == "Hats" then
    local hats = object
  else if object.key == "Skins" then
    local skins = object
  end
end
```


### Paginating results

Nakama methods that list results return a cursor which can be passed to subsequent calls to Nakama to indicate where to start retrieving objects from in the collection.

For example:
- If the cursor has a value of 5, you will get results from the fifth object.
- If the cursor is `null`, you will get results from the first object.

```lua
local next_result = nakama.list_storage_objects(client, "Unlocks", user_id, limit, result.cursor)
```


### Protecting storage operations on the server

Nakama Storage Engine operations can be protected on the server to protect data the player shouldn't be able to modify (i.e.  game unlocks or progress). See the [writing to the Storage Engine authoritatively](../snippets/authoritative-write/) recipe.


## Remote Procedure Calls

The Nakama [Server](../../server-framework/) allows developers to write custom logic and expose it to the client as [RPCs](../../server-framework/#rpc-functions).

Sagi-shi contains various logic that needs to be protected on the server, like checking if the player owns equipment before equipping it.


### Creating server logic

See the [handling player equipment authoritatively](../snippets/authoritative-read/) recipe for an example of creating a remote procedure to check if the player owns equipment before equipping it.


### Client RPCs

Nakama Remote Procedures can be called from the client and take optional JSON payloads.

The Sagi-shi client makes an RPC to securely equip a hat:

```lua
local payload = {
	item = "cowboy"
}
nakama.rpc_func(client, "EquipHat", json.encode(payload), nil, function(result)
	pprint(result)
end)
```



## Friends

Nakama [Friends](../../concepts/friends/) offers a complete social graph system to manage friendships amongst players.

Sagi-shi allows players to add friends, manage their relationships and play together.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/friends.png" >}} alt="Sagi-shi Friends screen">
  <figcaption>Friends screen</figcaption>
</figure>


### Adding friends

Adding a friend in Nakama does not immediately add a mutual friend relationship. An outgoing friend request is created to each user, which they will need to accept.

Sagi-shi allows players to add friends by their usernames or user ids:


```lua
local ids = { "<SomeUserId>", "<AnotherUserId>" }
local usernames = { "AlwaysTheImposter21", "SneakyBoi" }

local result = nakama.add_friends(client, ids, usernames)

if result.error then
  print(result.message)
  return
end
```


### Friendship states

Nakama friendships are categorized with the following [states](../../concepts/friends/#friend-state):

| Value | State |
| 
----- | ----- |
| 0 | Mutual friends |
| 1 | An outgoing friend request pending acceptance |
| 2 | An incoming friend request pending acceptance |
| 3 | Blocked |


### Listing friends

Nakama allows developers to list the player's friends based on their friendship state.

Sagi-shi lists the 20 most recent mutual friends:

```lua
local limit = 20 -- Limit is capped at 1000
local frienship_state = 0
local cursor = nil

local result = nakama.list_friends(client, limit, frienship_state, cursor)

if result.error then
  print(result.message)
  return
end

for _,friend in ipairs(result.friends) do
  pprint(friend)
end
```


### Accepting friend requests

When accepting a friend request in Nakama the player adds a [bi-directional friend relationship](../../concepts/friends/best-practices/#modeling-relationships).

Nakama takes care of changing the state from pending to mutual for both.

In a complete game you would allow players to accept individual requests.

Sagi-shi just fetches and accepts all the incoming friend requests:

```lua
local limit = 1000
local frienship_state = 2
local cursor = nil

local list_friends_result = nakama.list_friends(client, limit, frienship_state, cursor)

if list_friends_result.error then
  print(list_friends_result.message)
  return
end

local ids = {}

for _, friend in ipairs(list_friends_result.friends) do
  -- Collect all ids
  table.insert(ids, friend.id)
end

-- Accept all friends requests at once
local add_friends_result = nakama.add_friends(client, ids)
```


### Deleting friends

Sagi-shi players can decline friend requests and remove friends by their username or user id:

```lua
local ids = { "<SomeUserId>", "<AnotherUserId>" }
local usernames = { "<SomeUsername>", "<AnotherUsername>" }

local result = nakama.delete_friends(client, ids, usernames)

if result.error then
  print(result.message)
  return
end
```

### Blocking users

Sagi-shi players can block others by their username or user id:


```lua
local ids = { "<SomeUserId>", "<AnotherUserId>" }
local usernames = { "<SomeUsername>", "<AnotherUsername>" }

local result = nakama.block_friends(client, ids, usernames)

if result.error then
  print(result.message)
  return
end
```

Blocked users can listed just like [listing friends](#listing-friends) but using the corresponding friendship state (`3`).

## Status & Presence

Nakama [Status & Presence](../../concepts/status/) is has a real-time status and presence service that allows users to set their online presence, update their status message and follow other user's updates.

Players don't have to be friends with others they want to follow.

Sagi-shi uses status messages and online presences to notify players when their friends are online and share matches.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/status.png" >}} alt="Sagi-shi status update screen">
  <figcaption>Updating player status</figcaption>
</figure>


### Follow users

The Nakama real-time APIs allow developers to subscribe to events on the socket, like a status presence change, and receive them in real-time.

The method to follow users also returns the current online users, known as presences, and their status.

Sagi-shi follows a player's friends and notifies them when they are online:

```lua
socket.on_status_presence_event(function(message)
  local pressences = message.on_status_presence

  if pressence then
    -- todo: check if online
    for _, presence in ipairs(presences)
      pprint(presence.username .. " is online with status: '" .. presence.status .. "'")
    end
  end
end)

local user_ids = { "<SomeUserId>", "<AnotherUserId>" }
local result = socket.status_follow(user_ids)

if result.error then
  print(result.error.message)
  return
end

-- todo: check if list of presences received and print
```


### Unfollow users

Sagi-shi players can unfollow others:

```lua
local user_ids = { "<SomeUserId>", "<AnotherUserId>" }
local result = socket.status_unfollow(user_ids)
```


### Updating player status

Sagi-shi players can change and publish their status to their followers:

```lua
local result = socket.status_update("Viewing the Main Menu")
```


## Groups

Nakama [Groups](../../concepts/groups/) is a group or clan system with public/private visibility, user memberships and permissions, metadata and group chat.

Sagi-shi allows players to form and join groups to socialize and compete.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/groups-list.png" >}} alt="Sagi-shi groups screen">
  <figcaption>Groups list screen</figcaption>
</figure>


### Creating groups

Groups have a public or private "open" visibility. Anyone can join public groups, but they must request to join and be accepted by a superadmin/admin of a private group.

Sagi-shi players can create groups around common interests:

```lua
local avatar_url = "https://example.com/imposter.png"
local description = "A group for people who love playing the imposter."
local lang_tag = "en"
local max_count = 100
local name = "Imposters R Us"
local open = true -- public group

local result = nakama.create_group(client, avatar_url, description, lang_tag, max_count, name, open)

if result.error then
  print(result.message)
  return
end

pprint(result.name)
```


### Update group visibility

Nakama allows group superadmin or admin members to update some properties from the client, like the open visibility:

```lua
local avatar_url = "https://example.com/imposter.png"
local description = "A group for people who love playing the imposter."
local group_id = "<GroupId>"
local lang_tag = "en"
local name = "Imposters R Us"
local open = false -- private group

local result = nakama.update_group(client, group_id, avatar_url, description, group_id, lang_tag, name, open)

if result.error then
  print(result.message)
  return
end

pprint(result.open)
```


### Update group size

Other properties, like the group's maximum member size, can only be changed on the server.

See the [updating group size](../../concepts/groups/#updating-group-size) example, and the [Groups server function reference](../../server-framework/typescript-runtime/function-reference/#Groups) to learn more about updating groups on the server.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/group-edit.png" >}} alt="Sagi-shi group edit screen">
  <figcaption>Sagi-shi group edit</figcaption>
</figure>


### Listing and filtering groups

Groups can be listed like other Nakama resources and also [filtered](../../concepts/groups/#list-and-filter-groups) with a wildcard group name.

Sagi-shi players use group listing and filtering to search for existing groups to join:


```lua
local name = "imposter%"
local limit = 20
cursor = nil

local result = nakama.list_groups(client, name, cursor, limit)

if result.error then
  print(result.message)
  return
end

for _, group in ipairs(result.groups) do
  pprint(group.name .. " " .. tostring(group.open))
end

-- Get the next page of results.
local next_result = nakama.list_groups(client, name, result.cursor, limit)
```


### Deleting groups

Nakama allows group superadmins to delete groups.

Developers can disable this feature entirely, see the [Guarding APIs guide](../../guides/server-framework/guarding-apis/) for an example on how to protect various Nakama APIs.

Sagi-shi players can delete groups which they are superadmins for:

```lua
local result = nakama.delete_group(client, "<GroupId>")
```


### Group metadata

Like Users Accounts, Groups can have public metadata.

Sagi-shi uses group metadata to store the group's interests, active player times and languages spoken.

Group metadata can only be updated on the server. See the [updating group metadata](../snippets/group-metadata/) recipe for an example.

The Sagi-shi client makes an RPC with the group metadata payload:

```lua
local payload = {
	GroupId = "<GroupId>",
	Interests = { "Deception", "Sabotage", "Cute Furry Bunnies" },
	ActiveTimes = { "9am-2pm Weekdays", "9am-10pm Weekends" },
	Languages = { "English", "German" }
}

nakama.rpc_func(client, "UpdateGroupMetadata", json.encode(payload), nil, function()
	print("Successfully updated group metadata")
end)
```

### Group membership states

Nakama group memberships are categorized with the following [states](../../concepts/groups/):

| Code | Purpose | |
| ---- | ------- | - |
|    0 | Superadmin | There must at least be 1 superadmin in any group. The superadmin has all the privileges of the admin and can additionally delete the group and promote admin members. |
|    1 | Admin | There can be one of more admins. Admins can update groups as well as accept, kick, promote, demote, ban or add members. |
|    2 | Member | Regular group member. They cannot accept join requests from new users. |
|    3 | Join request | A new join request from a new user. This does not count towards the maximum group member count. |


### Joining a group

If a player joins a public group they immediately become a member, but if they try and join a private group they must be accepted by a group admin.

Sagi-shi players can join a group:

```lua
local result = nakama.join_group(client, "<GroupId>")
```


### Listing the user's groups

Sagi-shi players can list groups they are a member of:

```lua
local user_id = account.user.id
local limit = 20
local membership_state = nil -- All membership states
local cursor = nil

local result = list_user_groups(client, user_id, limit, membership_state, cursor)

if result.error then
  print(result.message)
  return
end

for _, group in ipairs(result.groups) do
  pprint(group.name .. " " .. tostring(group.state))
end
```


### Listing members

Sagi-shi players can list a group's members:

```lua
local limit = 20
local membership_state = nil -- All membership states
local cursor = nil

local result = nakama.list_group_users(client, "<GroupId>", limit, membership_state, cursor);

if result.error then
  print(result.message)
  return
end

for _, group in ipairs(result.groups) do
  pprint(group.name .. " " .. tostring(group.state))
end
```


### Accepting join requests

Private group admins or superadmins can accept join requests by re-adding the user to the group.

Sagi-shi first lists all the users with a join request state and then loops over and adds them to the group:


```lua
local limit = 20
local membership_state = 3 -- Join request
local cursor = nil

local list_group_result = nakama.list_group_users(client, "<GroupId>", limit, membership_state, cursor);

if list_group_result.error then
  print(list_group_result.message)
  return
end

local ids = {}

for _, user in ipairs(list_group_result.users) do
  -- Collect all ids
  table.insert(ids, user.id)
end

-- Accept all join requests at once
local add_friends_result = nakama.add_group_users(client, "<GroupId>", ids)
```


### Promoting members

Nakama group members can be promoted to admin or superadmin roles to help manage a growing group or take over if members leave.

Admins can promote other members to admins, and superadmins can promote other members up to superadmins.

The members will be promoted up one level. For example:

- Promoting a member will make them an admin
- Promoting an admin will make them a superadmin

```lua
local result = nakama.promote_group_users(client, "<GroupId>", { "<SomeUserId>", "<AnotherUserId>" })
```


### Demoting members

Sagi-shi group admins and superadmins can demote members:

```lua
local result = nakama.demote_group_users(client, "<GroupId>", { "<SomeUserId>", "<AnotherUserId>" })
```


### Kicking members

Sagi-shi group admins and superadmins can remove group members:

```lua
local result = nakama.kick_group_users(client, "<GroupId>", { "<SomeUserId>", "<AnotherUserId>" })
```


### Banning members

Sagi-shi group admins and superadmins can ban a user when demoting or kicking is not severe enough:

```lua
local result = nakama.ban_group_users(client, "<GroupId>", { "<SomeUserId>", "<AnotherUserId>" })
```

### Leaving groups

Sagi-shi players can leave a group:

```lua
local result = nakama.leave_group(client, "<GroupId>")
```


## Chat

Nakama Chat is a real-time chat system for groups, private/direct messages and dynamic chat rooms.

Sagi-shi uses dynamic chat during matches, for players to mislead each other and discuss who the imposters are, group chat and private/direct messages.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/chat.png" >}} alt="Sagi-shi chat screen">
  <figcaption>Sagi-shi Chat</figcaption>
</figure>


### Joining dynamic rooms

Sagi-shi matches have a non-persistent chat room for players to communicate in:

```lua
local room_name = "<MatchId>"
local persistence = false
local hidden = false
local result = socket.channel_join(room_name, socket.CHANNELTYPE_ROOM, persistence, hidden)

print("Connected to dynamic room channel: " .. result.channel.id)
```


### Joining group chat

Sagi-shi group members can have conversations that span play sessions in a persistent group chat channel:

```lua
local group_id = "<GroupId>"
local persistence = true
local hidden = false
local result = socket.channel_join(group_id, socket.CHANNELTYPE_GROUP, persistence, hidden)

print("Connected to group channel: " .. result.channel.id)
```


### Joining direct chat

Sagi-shi players can also chat privately one-to-one during or after matches and view past messages:

```lua
local user_id = "<UserId>"
local persistence = true
local hidden = false
local result = socket.channel_join(user_id, socket.CHANNELTYPE_DIRECT_MESSAGE, persistence, hidden)

print("Connected to direct message channel: " .. result.channel.id);
```


### Sending messages

Sending messages is the same for every type of chat channel. Messages contain chat text and emotes and are sent as JSON serialized data:

```lua
local channel_id = "<ChannelId>"

local content = {
  message = "I think Red is the imposter!"
}

local message_send_ack = socket.channel_message_send(channel_id, json.encode(content))

local emote_content = {
    emote = "point" ,
    emoteTarget = "<RedPlayerUserId>"
};

local emote_send_ack = socket.channel_message_send(channelId, json.encode(emote_content));
```


### Listing message history

Message listing takes a parameter which indicates if messages are received from oldest to newest (forward) or newest to oldest.

Sagi-shi players can list a group's message history:

```lua
local limit = 100
local forward = true
local group_id = "<GroupId>"
local result = nakama.list_channel_messages(client, group_id, limit, forward)

for i,message in ipairs(result.messages) do
  print(("%s:%s"):format(message.username, message.content))
end
```

Chat also has cacheable cursors to fetch the most recent messages. Store the cursor in a persistent Lua table using `sys.save()`


```lua

-- cache cursor in file 'my_settings'
local settings = sys.load("my_settings")
settings["nakama_message_cursor_" .. group_id] = result.cacheableCursor
sys.save("my_settings", settings)

-- get cached cursor
local settings = sys.load("my_settings")
local cursor = settings["nakama_message_cursor_" .. group_id]

-- use cursor to get new message
local next_result = nakama.list_channel_messages(client, group_id, limit, forward, cursor)

-- update cached cursor
settings["nakama_message_cursor_" .. group_id] = next_result.cacheableCursor
sys.save("my_settings", settings)
```


### Updating messages

Nakama also supports updating messages. It is up to you whether you want to use this feature, but in a game of deception like Sagi-shi it can add an extra element of deception.

For example a player sends the following message:

```lua
local channelId = "<ChannelId>"
local message_content = {
    message = "I think Red is the imposter!"
};

local message_send_ack = socket.channel_message_send(channel_id, json.encode(message_content))
```

They then quickly edit their message to confuse others:

```lua
local new_message_content =  {
    message = "I think BLUE is the imposter!"
}
local message_update_ack = socket.channel_message_update(channel_id, message_send_ack.message_id, json.encode(new_message_content))
```


## Matches

Nakama supports [Server Authoritative](../../concepts/multiplayer/authoritative/) and [Server Relayed](../../concepts/multiplayer/relayed/) multiplayer matches.

In server authoritative matches the server controls the gameplay loop and must keep all clients up to date with the current state of the game.

In server relayed matches the client is in control, with the server only relaying information to the other connected clients.

In a competitive game such as Sagi-shi, server authoritative matches would likely be used to prevent clients from interacting with your game in unauthorized ways.

For the simplicity of this guide, the server relayed model is used.


### Creating matches

Sagi-shi players can create their own matches and invite their online friends to join:

```lua
local result = socket.match_create()

if result.error then
  print(result.error.message)
  return
end

local match_id = result.match_create.match_id

local limit = 20
local frienship_state = 0

local result = nakama.list_friends(client, limit, frienship_state)
-- todo: get online friends

if result.error then
  print(result.error.message)
  return
end

for _,friend in ipairs(result.friends) do
  pprint(friend)
  -- todo: send match_id to online friends
end
```

### Joining matches

Sagi-shi players can try to join existing matches if they know the id:

```lua
local match_id = "<MatchId>"
local result = socket.match_join(match_id)

if result.error then
  print(result.error.message)
  return
end

if result.match then
  print("Match joined!")
end
```

Or set up a real-time matchmaker listener and add themselves to the matchmaker:

```lua
local min_players = 2
local max_players = 10
local query = "*"

local result = socket.matchmaker_add(min_players, max_players, query)

if result.error then
  print(result.error.message)
  return
end

socket.on_matchmakermatched(function(message)
  local matched = message.matchmaker_matched

  if matched then
    print(matched.match_id)
    print(matched.token)
  end
end)
```


**Joining matches from player status**

Sagi-shi players can update their status when they join a new match:

```lua
local status = {
  status = "Playing a match",
  match_id = "<MatchId>"
}

local result = socket.status_update(status)

if result.error then
  print(result.error.message)
  return
end
```

When their followers receive the real-time status event they can try and join the match:

```lua
socket.on_status_presence_event(function(message)
  local pressence = message.on_status_presence

  if pressence then
    if presence.status.match_id then
      local match_id = presence.status.match_id

      local result = socket.match_join(match_id)
    end
  end
end)
```

### Listing matches

Match Listing takes a number of criteria to filter matches by including player count, a match [label](../../concepts/multiplayer/match-listing/) and an option to provide a more complex [search query](../../concepts/multiplayer/query-syntax/).

Sagi-shi matches start in a lobby state. The match exists on the server but the actual gameplay doesn't start until enough players have joined.

Sagi-shi can then list matches that are waiting for more players:

```lua
local limit = 10
local authoritative = true
local label = ""
local min_size = 2
local max_size = 10
local query = ""

local result = nakama.list_matches(client, limit, authoritative, label, min_size, max_size, query)

if result.error then
  print(result.message)
  return
end

for _, match in ipairs(result.matches) do
  pprint(group.match_id .. ": " .. tostring(match.size) .. "/10 players")
end
```

To find a match that has a label of `"AnExactMatchLabel"`:

```lua
local label = "AnExactMatchLabel"
```

**Advanced:**

In order to use a more complex structured query, the match label must be in JSON format.

To find a match where it expects player skill level to be `>100` and optionally has a game mode of `"sabotage"`:

```lua
local query = "+label.skill:>100 label.mode:sabotage"
```


### Spawning players

The match object has a list of current online users, known as presences.

Sagi-shi uses the match presences to spawn players on the client:

```lua
local result = socket.match_join("<match_id>", account.token)

local players = {}

for _, presence in ipairs(result.match_join.presences)
  local player = add_player()
  players[presence.session_id] = player
end
```

Sagi-shi keeps the spawned players up-to-date as they leave and join the match using the match presence received event:

```lua
socket.on_matchpresence(function(message)
  -- todo: check if it's joins
  for _, presence in ipairs(message.match_presence_event.joins)
    local player = add_player()
    players[presence.session_id] = player
  end

  -- todo: check if it's leaves
  for _, presence in ipairs(message.match_presence_event.leaves)
    players[presence.session_id] = nil
  end
end)
```


### Sending match state

Nakama has real-time networking to [send](../../concepts/multiplayer/relayed/#send-data-messages) and [receive](../../concepts/multiplayer/relayed/#receive-data-messages) match state as players move and interact with the game world.

During the match, each Sagi-shi client sends match state to the server to be relayed to the other clients.

Match state contains an op code that lets the receiver know what data is being received so they can deserialize it and update their view of the game.

Example op codes used in Sagi-shi:
- 1: player position
- 2: player calling vote


**Sending player position**

Grab the player's position, set the op code and send the JSON encoded state:

```lua
local data = json.encode({
  x = player.x,
  y = player.y,
})

local op_code = 1

local result = socket.match_data_send("<match_id>", op_code, data)

if result.error then
  print(result.error.message)
  pprint(result)
end
```


**Op Codes as a table**

Sagi-shi has many networked game actions. Using a static class of constants for op codes will keep your code easier to follow and maintain:

```lua
local op_codes = {
  position = 1
  vote = 2
}

local result = socket.match_data_send("<match_id>", op_code.position, data)
```


### Receiving match state

Sagi-shi players can receive match data from the other connected clients by subscribing to the match state received event:

```lua
socket.on_matchdata(function(message)
  local match_data = message.match_data
  local op_code = tonumber(match_data.op_code)
  local data = json.decode(match_data.data)

  if op_code == op_codes.positoon then
    -- todo: check what presence comes in
    local session_id = match_data.presence.session_id

    if players[session_id] then
      local player = players[session_id]

      player.x = data.x
      player.ya = data.y
    end
  else if op_code == op_codes.vote then
    -- vote logic
  end
end)
```


## Matchmaker

Developers can find matches for players using Match Listing or the Nakama [Matchmaker](../../concepts/multiplayer/matchmaker/), which enables players join the real-time matchmaking pool and be notified when they are matched with other players that match their specified criteria.

Matchmaking helps players find each other, it does not create a match. This decoupling is by design, allowing you to use matchmaking for more than finding a game match. For example, if you were building a social experience you could use matchmaking to find others to chat with.


### Add matchmaker

Matchmaking criteria can be simple, find 2 players, or more complex, find 2-10 players with a minimum skill level interested in a specific game mode.

Sagi-shi allows players to join the matchmaking pool and have the server match them with other players:

```lua
local query = "+skill:>100 mode:sabotage"
local min_count = 2
local max_count = 10
local string_properties = json.encode({ mode = "sabotage" })
local numeric_properties = json.encode({ skill = 125 })

local result = socket.matchmaker_add(min_count, max_count, query, string_properties, numeric_properties)

if result.error then
  print(result.error.message)
end
```

After being successfully matched according to the provided criteria, players can join the match:

```lua
socket.on_matchmaker_matched(function(matched)
  pprint("Received:", matched);
  socket.match_join(matched.token)
end)
```

## Parties

Nakama [Parties](../../concepts/parties/) is a real-time system that allows players to form short lived parties that don't persist after all players have disconnected.

Sagi-shi allows friends to form a party and matchmake together.


### Creating parties

The player who creates the party is the party's leader. Parties have maximum number of players and can be open to automatically accept players or closed so that the party leader can accept incoming join requests.

Sagi-shi uses closed parties with a maximum of 4 players:

```lua
local open = false
local max_players = 4
local party = socket.party_create(open, max_players)
```

Sagi-shi shares party ids with friends via private/direct messages:

```lua
local result = nakama.list_friends(client, 100, 0)

for i,friend in ipairs(result.friends) do
  if friend.user.online then
    local content = {
      message = message = ("Hey %s, wanna join the party?!"):format(friend.user.username),
      partyId = party.party_id
    }

    local result = socket.channel_join(friend.user.id, 0)
    socket.channel_message_send(result.channel.id, json.encode(content))
  end
end
```


### Joining parties

Sagi-shi players can join parties from chat messages by checking for the party id in the message:

```lua
socket.on_channel_message(function(message)
  if message.party_id then
    socket.party_join(message.party_id)
  end
end)
```


### Promoting a member

Sagi-shi party members can be promoted to the party leader:

```csharp
var newLeader = party.Presences.Where(p => p.SessionId != party.Leader.SessionId).First();
await socket.PromotePartyMemberAsync(party.Id, newLeader);
```

### Leaving parties

Sagi-shi players can leave parties:

```lua
socket.party_leave(party.party_id)
```


### Matchmaking with parties

One of the main benefits of joining a party is that all the players can join the matchmaking pool together.

Sagi-shi players can listen to the the matchmaker matched event and join the match when one is found:

```lua
socket.on_matchmaker_matched(function(message)
  socket.match_join(message.match_id)
end)
```

The party leader will start the matchmaking for their party:

```lua
local min_players = 2
local max_players = 10
local query = ""
local ticket = socket.party_matchmaker_add("<PartyId>", min_players, max_players, query)
```

### Sending party data

Sagi-shi players can send data to other members of their party to indicate they wish to start a vote.

```lua
local data = json.encode({
  username = "<Username>",
  reason = "Emergency"
})

local vote_op_code = 6

local result = socket.party_data_send("<party_id>", vote_op_code, data)

if result.error then
  print(reuslt.error.message)
  pprint(result)
end
```


### Receiving party data

Sagi-shi players can receive party data from other party members by subscribing to the party data event.

```lua
socket.on_party_data(function(message)
  local party_data = message.party_data
  local op_code = tonumber(party_data.op_code)
  local data = json.decode(party_data.data)

  if op_code == vote_op_code then
    -- Show a UI dialogue - "<username> has proposed to call a vote for <reason>. Do you agree? Yes/No"
  end
end)
```

## Leaderboards

Nakama [Leaderboards](../../concepts/leaderboards/) introduce a competitive aspect to your game and increase player engagement and retention.

Sagi-shi has a leaderboard of weekly imposter wins, where player scores increase each time they win, and similarly a leaderboard for weekly crew member wins.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/leaderboard.png" >}} alt="Sagi-shi leaderboard screen">
  <figcaption>Sagi-shi Leaderboard</figcaption>
</figure>


### Creating leaderboards

Leaderboards have to be created on the server, see the [leaderboard](../../concepts/leaderboards/#create-a-leaderboard) documentation for details on creating leaderboards.


### Submitting scores

When players submit scores, Nakama will increment the player's existing score by the submitted score value.

Along with the score value, Nakama also has a subscore, which can be used for ordering when the scores are the same.

Sagi-shi players can submit scores to the leaderboard with contextual metadata, like the map the score was achieved on:

```lua
local metadata = json.encode({ map = "space_station" })

-- need to be strings
local score = "1"
local subscore = "0"

nakama.write_leaderboard_record(client, "<leaderboard_id>", metadata, nil, score, subscore)
```

### Listing the top records

Sagi-shi players can list the top records of the leaderboard:

```lua
local owner_ids = nil
local limit = 20
local cursor = nil
local expiry = nil

local result = nakama.list_leaderboard_records(client, "<leaderboard_id>", owner_ids, limit, cursor, expiry)

if result.error then
  print(result.error.message)
  return
end

for _, record in ipairs(result.leaderboard_records)
  print(record.owner_id .. ":" .. record.score)
end
```


**Listing records around the user**

Nakama allows developers to list leaderboard records around a player.

Sagi-shi gives players a snapshot of how they are doing against players around them:

```lua
local owner_ids = { "<SomeUserId>", "<AnotherUserId>" }
local limit = 5
local cursor = nil
local expiry = nil

local result = nakama.list_leaderboard_records_around_owner(client, "<leaderboard_id>", owner_ids, limit, cursor, expiry)

if result.error then
  print(result.error.message)
  return
end

for _, record in ipairs(result.leaderboard_records_around_owner)
  print(record.owner_id .. ":" .. record.score)
end
```

For example, if the leaderboard contains 100 records and the ownerId = "player123" with limit = 5, the result will include the specified user along with nearby records:

```json
{
  "records": [
    { "ownerId": "player120", "rank": 48, "score": 1500 },
    { "ownerId": "player121", "rank": 49, "score": 1480 },
    { "ownerId": "player123", "rank": 50, "score": 1450 },  // Your ownerId
    { "ownerId": "player125", "rank": 51, "score": 1430 },
    { "ownerId": "player127", "rank": 52, "score": 1400 }
  ]
}
```


**Listing records for a list of users**

Sagi-shi players can get their friends' scores by supplying their user ids to the owner id parameter:

```lua
nakama.list_leaderboard_records(client, "<leaderboard_id>", owner_ids)
```

The same approach can be used to get group member's scores by supplying their user ids to the owner id parameter:

```lua


local result = nakama.list_group_users(client, "<GroupId>", 100)
local user_ids = {}
for i,group_user in ipairs(result.groupUsers) do
  if group_user.state < 3 then
    table.insert(user_ids, group_user.user.id)
  end
end

local result = nakama.list_leaderboard_records(client, "weekly_imposter_wins", user_ids, 100)
for i,record in ipairs(result.records) do
  print(record.username, record.score)
end
```


### Deleting records

Sagi-shi players can delete their own leaderboard records:

```lua
local result = nakama.delete_leaderboard_record(client, "<leaderboard_id>")
```


## Tournaments

Nakama [Tournaments](../../concepts/tournaments/) are short lived competitions where players compete for a prize.

Sagi-shi players can view, filter and join running tournaments.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/tournaments.png" >}} alt="Sagi-shi tournaments screen">
  <figcaption>Sagi-shi Tournaments</figcaption>
</figure>


### Creating tournaments

Tournaments have to be created on the server, see the [tournament]../../concepts/tournaments/#create-tournament) documentation for details on how to create a tournament.

Sagi-shi has a weekly tournament which challenges players to get the most correct imposter votes. At the end of the week the top players receive a prize of in-game currency.


### Joining tournaments

By default in Nakama players don't have to join tournaments before they can submit a score, but Sagi-shi makes this mandatory:

```lua
local result = nakama.join_tournament(client, "<tournament_id>")
```

### Listing tournaments

Sagi-shi players can list and filter tournaments with various criteria:

```lua
local category_start = 1
local category_end = 2
local start_time = nil
local end_time = nil
local limit = 100
local cursor = nil

local result = nakama.list_tournaments(client, category_start, category_end, start_time, end_time, limit, cursor)

if result.error then
  print(result.error.message)
  return
end

for _, tournament in ipairs(result.tournaments)
  print(tournament.id .. ":" .. tournament.title)
end
```

{{< note "important" >}}
Categories are filtered using a range, not individual numbers, for performance reasons. Structure your categories to take advantage of this (e.g. all PvE tournaments in the 1XX range, all PvP tournaments in the 2XX range, etc.).
{{< /note >}}

### Listing records

Sagi-shi players can list tournament records:

```lua
local owner_ids = nil
local limit = 20
local cursor = nil
loca expiry = nil

local result = list_tournament_records(client, "<tournament_id>", owner_ids, limit, cursor, expiry)

if result.error then
  print(result.error.message)
  return
end

for _, record in ipairs(result.tournament_record)
  print(record.owner_id .. ":" .. record.score)
end
```


**Listing records around a user**

Similarly to leaderboards, Sagi-shi players can get other player scores around them:

```lua
local owner_ids = { "<SomeUserId>", "<AnotherUserId>" }
local limit = 20
local cursor = nil
loca expiry = nil

local result = nakama.list_tournament_records(client, "<tournament_id>", owner_ids, limit, cursor, expiry)

if result.error then
  print(result.error.message)
  return
end

for _, record in ipairs(result.tournament_record)
  print(record.owner_id .. ":" .. record.score)
end
```


### Submitting scores

Sagi-shi players can submit scores, subscores and metadata to the tournament:

```lua
local metadata = json.encode({ map = "space_station" })
local score = 1
local subscore = 0

local result = nakama.write_tournament_record(client, "<tournament_id>", metadata, nil, score, subscore)
```


## Notifications

Nakama [Notifications](../../concepts/notifications/) can be used for the game backend to broadcast real-time messages to players.

Notifications can be either persistent (remaining until a player has viewed it) or transient (received only if the player is currently online).

Sagi-shi uses Notifications to notify tournament winners about their winnings.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/notifications.png" >}} alt="Sagi-shi notification screen">
  <figcaption>Sagi-shi notifications</figcaption>
</figure>


### Receiving notifications

Notifications have to be sent from the server.

Nakama uses a code to differentiate notifications. Codes of `0` and below are [system reserved](../../concepts/notifications/#notification-codes) for Nakama internals.

Sagi-shi players can subscribe to the notification received event. Sagi-shi uses a code of `100` for tournament winnings:

```lua
socket.on_notification(function(message)
  local reward_code = 100
  local notification = message.notification

  if notification.code == reward_code then
    print("Congratulations, you won the tournament!\n" .. notification.subject .. "\n" .. notification.content)
  else
    print(notification.code .. "\n" .. notification.subject .. "\n" .. notification.content)
  end
end)
```


### Listing notifications

Sagi-shi players can list the notifications they received while offline:

```lua
local limit = 100
local cacheable_cursor = nil

local result = nakama.list_notifications(client, limit, cacheable_cursor)

if result.error then
  print(result.error.message)
  return
end

for _, notification in ipairs(result.notifications)
  print(notification.code .. "\n" .. notification.subject .. "\n" .. notification.content)
end
```


**Pagination and cacheable cursors**

Like other listing methods, notification results can be paginated using a cursor or cacheable cursor from the result.

```lua
_G.notifications = {
  cacheable_cursor = result.cacheable_cursor
}
```

The next time the player logs in the cacheable cursor can be used to list unread notifications.

```lua
local cacheable_cursor = _G.notifications.cacheable_cursor
local next_results = list_notifications(client, limit, cacheable_cursor)
```


### Deleting notifications

Sagi-shi players can delete notifications once they've read them:

```csharp
local result = nakama.delete_notifications(client, { "<notification_id_1>", "<notification_id_2>" })
```
