Skip to content

Notifications

tickBit edited this page Jul 8, 2026 · 11 revisions

About notifications

The server is capable of sending notifications. These notifications can be various, but mostly they contain information about certain events happening and registered on the server side such as player has completed a daily task or a new voting has been started.

MQTT protocol

For the notifications mechanism to work a MQTT protocol is used. The MQTT protocol has 2 key components: publisher (=server) and subscriber (=client). The publisher can publish a message (= send notification) and the subscribers can listen and wait for the message to come.

The subscribers can listen to messages by using topics (= endpoints). When some message come to a specific topic, subscriber gets it. Topic is a unique string usually containing slashes (/) - separators. The separators are used for organizing the structure of the topics overall.

Subscriber can listen to a specific topic and also it can use a wildcard, for example to subscribe to all of the topics starting with a specific string, i.e. with "/clan" to listen to all notifications related to clans.

The messages coming from a topics can have any format, there are not restrictions for that.

More detailed description about MQTT can be found here

Notification mechanism on server

The notifications mechanism with topics structure and more built on the server can be found here

Supported MQTT topics

Below are presented all the currently implemented topics (more to come).

Jukebox

1. Song Update Notification

Topic URL: /clan/{clanId}/jukebox/song/update

Where:

  • {clandId} represents the clanId

When triggers

  • player adds a song to an empty Jukebox, it starts playing immediately
  • the current song finishes playing and the next song is pulled from the queue

Payload example

{
  topic: `/clan/7a019315ee393ee3d344e197/jukebox/song/update`,
  song: {
    songId: "67dee567c34dc94aa4f09277", // Client-side identifier for the song
    startedAt: 1761207021503            // timestamp in milliseconds
  }
}

Subscription: /clan/{clanId}/jukebox/song/update

2. Playlist Update Notification

Topic URL: /clan/{clanId}/jukebox/playlist/update

When triggers

  • a player adds a new song (it is either appended to the queue or becomes the current song)
  • a song starts playing (updating the queue and the current playing song)
  • a player removes a song they added from the queue
  • the last song finishes playing, clearing the playlist

Payload example

{
  topic: "/clan/7a019315ee393ee3d344e197/jukebox/playlist/update",
  playlist: {
    clanId: "68e7777139519eaf400a0e3d",
    currentSong: {
      id: "67dee567c34dc94aa4f09275",
      songId: "67dee567c34dc94aa4f09277",
      songDurationSeconds: 123,
      playerId: "6a043d92aaacc61e8681495a",
      startedAt: 1761207021503
    },
    songQueue: [
      {
        id: "67dee567c34dc94aa4f09271",
        songId: "67dee567c34dc94aa4f09277",
        songDurationSeconds: 123,
        playerId: "6a043d92aaacc61e8681495a"
      }
    ]
  }
}

Subscription: /clan/${clanId}/jukebox/playlist/update

Voting

Topic URL: /clan/{clanId}/voting/{votingType}/{status}

Where:

  • {clanId} is voting.organizer.clan_id
  • {votingType} is one value from VotingType
  • {status} is one of new, update, end

Current voting types:

  • flea_market_sell_item
  • flea_market_buy_item
  • change_item_price
  • shop_buy_item
  • set_clan_role
  • clan_governance_update

When triggers

  • when a new voting begins
  • when voting is updated (a new vote is given)
  • when voting is completed
  • if there's an error in voting

Payload

All voting notifications use this payload shape:

{
  topic: `/clan/${clanId}/voting/${votingId}`,
  status: 'new' | 'update' | 'end',
  voting_id: string,
  type: VotingType,
  entity: unknown,
  organizer?: PlayerDto,
  voter?: PlayerDto,
  votes?: Vote[],
  endedAt?: Date
}

Recommended frontend flow:

  1. Subscribe to /clan/{clanId}/voting/+/+.
  2. Use payload.status to route lifecycle behavior.
  3. Use payload.voting_id as the stable voting id.
  4. Use payload.type to narrow payload.entity.
  5. Treat payload.topic as the voting-instance channel/id: /clan/{clanId}/voting/{votingId}.

payload.topic is a voting-instance identifier for the frontend. It is not the MQTT broker topic. The MQTT broker topic remains /clan/{clanId}/voting/{votingType}/{status}.

Daily tasks

Topic URLs:

Method               | MQTT topic path                                   | Description                                    | Payload
-------------------------------------------------------------------------------------------------------------------------------------------
taskReceived         | /player/{player_id}/daily_task/{task_type}/new    | Player got new daily task.                     | TTask
taskUpdated          | /player/{player_id}/daily_task/{task_type}/update | Daily task was advanced (some part of a task). | TTask
taskError            | /player/{player_id}/daily_task/{task_type}/error  | Error in task.                                 | APIError
taskCompleted        | /player/{player_id}/daily_task/{task_type}/end    | Player completed a task.                       | TTask
taskCompletedForClan | /clan/{clan_id}/daily_task/{task_type}/end        | Member of a clan completed a clan's task       | { task: TTask, completedByPlayerId: string }`
milestoneReached     | /clan/{clan_id}/daily_task/milestone/update       | A clan achieved new milestone                  | { task: TTask, completedByPlayerId: string, reachedMilestones: number[] }

Where {task_type} is a task from the following enum:

When triggers

  • when a player successfully reserves a new daily task from the clan's available tasks
  • when a player progresses in reserved task (e.g. increments progress but does not finish it yet)
  • if an error occurs during task execution or validation
  • when player's task is fully completed (amountLeft reaches 0)
  • when a clan member successfully completes an active task
  • when clan's progression milestones are unlocked as a result of completing a daily task

Payload examples

  • reserveTask:
{
  "task": {
    "_id": "665af23e5e982f0013aa334b",
    "clan_id": "665af23e5e982f0013aa1122",
    "player_id": "665af23e5e982f0013aa4455",
    "title": { "fi": "Kirjoita klaanichattiin viesti" },
    "type": "write_chat_message_clan",
    "points": 50,
    "coins": 100,
    "startedAt": "2026-06-08T07:44:24.000Z",
    "amount": 1,
    "amountLeft": 0,
    "timeLimitMinutes": 2
  },
  "completedByPlayerId": "665af23e5e982f0013aa4455"
}
  • a milestone reached:
{
  "task": {
    "_id": "665af23e5e982f0013aa334b",
    "clan_id": "665af23e5e982f0013aa1122",
    "player_id": "665af23e5e982f0013aa4455",
    "title": { "fi": "Kirjoita klaanichattiin viesti" },
    "type": "write_chat_message_clan",
    "points": 50,
    "coins": 100,
    "startedAt": "2026-06-08T07:44:24.000Z",
    "amount": 1,
    "amountLeft": 0,
    "timeLimitMinutes": 2
  },
  "completedByPlayerId": "665af23e5e982f0013aa4455",
  "reachedMilestones": [1, 2] // List of milestones reached
}
  • error case:
{
  "name": "APIError",
  "reason": "Unexpected error",
  "statusCode": 500,
  "message": "Error description",
  "field": null,
  "value": null,
  "additional": null
}
  • successfully completed task:
{
  "task": {
    "_id": "665af23e5e982f0013aa334b",
    "clan_id": "665af23e5e982f0013aa1122",
    "player_id": "665af23e5e982f0013aa4455",
    "title": { "fi": "Kirjoita klaanichattiin viesti" },
    "type": "write_chat_message_clan",
    "points": 50,
    "coins": 100,
    "startedAt": "2026-06-08T07:44:24.000Z",
    "amount": 1,
    "amountLeft": 0,
    "timeLimitMinutes": 2
  },
  "completedByPlayerId": "665af23e5e982f0013aa4455"
}

Recommended Frontend Handling

  1. Subscriptions:

    • Subscribe to /player/{playerId}/daily_task/+/+ on player profile/app initialization.
    • Subscribe to /clan/{clanId}/daily_task/+/+ upon entering the clan lobby or relevant dashboard.
  2. Personal HUD & Notifications:

    • On /new, add the task to the player's active daily task slot.
    • On /update, animate the task's progress bar in the UI.
    • On /end, trigger a completion celebration/reward overlay and clear the active task slot.
  3. Clan Activity Feed:

    • On /clan/{clanId}/daily_task/{taskType}/end, display a notification in the clan feed (e.g., "User X completed a task!").
    • On /clan/{clanId}/daily_task/milestone/update, update the clan progression progress bar and show milestone-unlocked popups.

Changes in clan status

Topic URL: /clan/{clanId}/member/{event}

Where:

  • clanId is the MongoDB id of the clan
  • event is join or leave

When triggers

  • when a new member joins a clan
  • when a member leaves a clan
  • when a member is removed from a clan

Payload example: (join)

{
  "topic": "/clan/6a19adf07c176746090d52a1/member/join",
  "playerId": "6a19adf07c176746090d52a2",
  "event": "join",
  "ts": 1719320000000
}

Subscription: /clan/{clanId}/member/+

Matchmaking MQTT Notifications

The backend publishes matchmaking invite and match lifecycle events through MQTT. The implementation lives in src/matchmaking/matchmaking.notifier.ts, and the state changes that trigger the events are handled by src/matchmaking/matchmaking.service.ts.

Matchmaking topics currently do not start with /. Frontend subscriptions must use the exact topic strings shown below.

Subscribe Topics

A frontend client should subscribe to player-specific channels for the logged-in player and to match-specific channels after a match has been created.

/matchmaking/invites/player/{playerId}
/matchmaking/matches/player/{playerId}
/match/{matchId}

Where:

  • {playerId} is the authenticated player's player id.
  • {matchId} is the match id returned in a MATCH_FOUND or MATCH_STARTED payload.

Common Payload Shape

All matchmaking MQTT messages use the same wrapper shape:

{
  type: string,
  payload: unknown
}

The type field identifies the event. The payload field contains either a MatchmakingInviteDto or a MatchmakingMatchDto, depending on the event.

1. Invite Update Notification

Topic
/matchmaking/invites/player/{playerId}
Event Type
INVITE_UPDATED
Publish Triggers
  • A player creates a matchmaking invite.
  • A player joins an invite.
  • An invite is moved forward by matchmaking and its status changes.
  • An invite is matched into an active match.
  • An invite is cancelled by its owner.

The notification is sent to every real player currently attached to the invite. Bots do not receive MQTT notifications.

Payload Shape
{
  type: 'INVITE_UPDATED',
  payload: {
    id: string,
    matchType: 'RANDOM' | 'CLAN' | 'CUSTOM',
    status: 'OPEN' | 'READY' | 'QUEUED' | 'MATCHED' | 'CANCELLED',
    ownerPlayerId: string,
    clanId?: string,
    roomId?: string,
    players: string[],
    bots: [
      {
        botId: string,
        displayName: string,
        isBot: true
      }
    ],
    teamSize: 1 | 2,
    allowBots: boolean,
    createdAt: string,
    updatedAt: string,
    readyAt?: string,
    matchId?: string
  }
}

2. Player Match Found Notification

Topic
/matchmaking/matches/player/{playerId}
Event Type
MATCH_FOUND
Publish Triggers
  • Two READY RANDOM invites are paired.
  • Two READY CLAN invites from different clans are paired.
  • A CLAN invite reaches the opponent timeout and receives a bot opponent team.
  • A CUSTOM invite starts a match from its room settings.

This notification is sent once to each real player in the created match. Bots are included in the match payload but do not receive player-specific notifications.

Payload Shape
{
  type: 'MATCH_FOUND',
  payload: MatchmakingMatchDto
}

MatchmakingMatchDto has this shape:

{
  id: string,
  matchType: 'RANDOM' | 'CLAN' | 'CUSTOM',
  status: 'ACTIVE' | 'FINISHED',
  teamSize: 1 | 2,
  teams: [
    {
      side: 'A' | 'B',
      clanId?: string,
      players: [
        {
          playerId: string,
          isBot: false
        }
      ],
      bots: [
        {
          botId: string,
          displayName: string,
          isBot: true
        }
      ]
    }
  ],
  startedAt: string,
  finishedAt?: string,
  result?: {
    winningSide: 'A' | 'B'
  }
}

3. Match Started Event

Topic
/match/{matchId}
Event Type
MATCH_STARTED
Publish Triggers
  • Any active match is created and persisted in Redis.

The payload is the same MatchmakingMatchDto that is sent with MATCH_FOUND. This topic is useful for clients that have already switched from the player-level matchmaking channel to a match-level game channel.

Payload Shape
{
  type: 'MATCH_STARTED',
  payload: MatchmakingMatchDto
}

4. Match Finished Event

Topic
/match/{matchId}
Event Type
MATCH_FINISHED
Publish Triggers
  • A participant finishes an active match through POST /matchmaking/matches/{matchId}/finish.
  • The backend accepts the finish request, updates leaderboards, stores the finished match with the finished-match TTL, and invalidates leaderboard caches.

The payload contains the finished match with status: 'FINISHED', finishedAt, and result.

Payload Shape
{
  type: 'MATCH_FINISHED',
  payload: MatchmakingMatchDto
}

Frontend Handling

Recommended frontend flow:

  1. Subscribe to /matchmaking/invites/player/{playerId} after login.
  2. Subscribe to /matchmaking/matches/player/{playerId} while the player is in matchmaking.
  3. Use INVITE_UPDATED to keep lobby and invite UI synchronized.
  4. When MATCH_FOUND is received, read payload.id and subscribe to /match/{matchId}.
  5. Use MATCH_STARTED to initialize the match scene if the client did not enter from the player-specific MATCH_FOUND event.
  6. Use MATCH_FINISHED to show the result screen and refresh leaderboard views.

Notes

  • MQTT payloads are JSON strings produced with JSON.stringify.
  • Invite and match data are stored in Redis, so clients should treat these events as real-time state notifications rather than durable history.
  • CLAN match finishes update both clan leaderboards and personal leaderboards for participating clan players. RANDOM and CUSTOM finishes update only personal leaderboards.

Clone this wiki locally