Skip to content

Nakama Global Chat Mute — Implementation Handoff

Goal

Add a persistent, account-wide chat mute. A muted player may continue to play and receive chat, but Nakama rejects every attempted channel-message send.

This seed deliberately uses session-cached moderation state. It does not require a live override cache or an immediate cross-node invalidation path.

Accepted enforcement model

  • Moderation writes the offender's authoritative record as muted = true.
  • Authentication/session refresh loads that record and writes the reserved, server-owned session variable server_chat_muted.
  • Realtime chat-send hooks read only server_chat_muted from the authenticated runtime context.
  • Already-connected realtime sockets keep the value with which they authenticated. A moderation write does not change a live socket in place.
  • The mute becomes effective for that client after a new/renewed session has the updated claim and the realtime socket reconnects with that session.
  • Unmuting has the same delayed propagation behavior.

This is intentionally not the hybrid design. Do not add a live in-memory override map, per-message storage reads, or cluster invalidation in this implementation.

Persistent moderation record

Use one server-owned storage object per user, keyed directly by user ID so session issuance performs one deterministic read rather than a collection scan.

Suggested shape:

{
  "muted": true,
  "updated_at": 1787241600,
  "updated_by": "operator-user-id",
  "reason": "optional operator note"
}

Only trusted server/operator code may write this object. Absence of the object or muted = false means chat is enabled. Fail closed only when an existing record cannot be decoded safely; document and log that condition.

Session claim ownership

server_chat_muted is reserved server state. Never trust or preserve a client-supplied value for this key.

At every supported session-issuance path:

  1. Resolve the authenticated user ID.
  2. Read the keyed moderation object.
  3. Overwrite server_chat_muted with the authoritative result.
  4. Return/sign the session with the authoritative variable.

For session refresh specifically, do not copy the old claim forward as authority. The refresh path must reload storage and overwrite it. If Nakama's before-refresh hook is used, derive the user ID from the refresh token and treat that token only as an identifier until Nakama completes its own validation; do not accept refresh-request vars as moderation state.

Initial authentication must also receive the authoritative claim before the first realtime socket connects. If the current auth hooks cannot add this claim directly, require an immediate authoritative session refresh between authentication and realtime connection.

Chat enforcement

Register the existing before-channel-message-send guard for all chat channels. The guard should:

  • allow sends when server_chat_muted is absent or false;
  • reject sends when server_chat_muted is true;
  • return a stable application error/code that Unreal can map to the muted UI state;
  • leave joins, history reads, and incoming live messages unchanged.

Do not read moderation storage on each message.

Activation paths

Planned maintenance / all-server shutdown

A socket disconnect alone is insufficient because a client can reconnect using its still-valid JWT and its stale session variables. Maintenance must use one of these complete paths:

  • globally invalidate existing sessions, then require clients to authenticate again before reconnecting realtime; or
  • require every client to perform an authoritative session refresh before its post-maintenance realtime reconnect.

Only after the new token contains server_chat_muted = true is the mute active on the replacement socket.

Manually triggered activation for one offender

The safe, minimally disruptive client-coordinated sequence is:

  1. Persist muted = true.
  2. Trigger an authoritative session refresh for that user.
  3. Connect a replacement realtime socket using the refreshed session.
  4. Restore house chat setup/subscriptions on the replacement socket.
  5. Disconnect the old socket.

Prefer this make-before-break sequence. With the current House lifecycle, disconnecting the offender's final tracked socket first can immediately remove a non-host member or close a host's House; the Unreal reconnect grace period does not prevent that backend cleanup.

If an operator deliberately force-disconnects first, treat it as a kick/disruptive enforcement path, not as seamless mute activation.

House and chat behavior

  • House/match chat continues to use the House ID as its channel identity.
  • Chat history behavior and the live OnChatMessage path are unchanged.
  • Muted users can join/rejoin House chat, load history, and receive broadcasts.
  • Only outbound message sends are rejected.
  • No Nakama authoritative gameplay match is introduced.

Unreal-facing state

Expose the mute rejection distinctly from transport failure. UI may disable or annotate the composer after the first authoritative rejection, but the server guard remains the source of truth.

The manual activation flow must refresh the HTTP session and replace/reconnect the realtime socket. Replacing only UNakamaSession while retaining the old healthy socket does not update the realtime runtime context.

Deferred work

The following are explicitly deferred:

  • immediate enforcement on every already-connected socket;
  • live override/hybrid cache;
  • multi-node cache invalidation;
  • timed mute expiry;
  • operator UI and automated disconnect orchestration;
  • changing House cleanup semantics for socket rotation.

Verification

Primary verification uses House/match chat:

  1. Start unmuted, join House chat, send and receive a message.
  2. Persist muted = true while the socket remains connected; confirm this implementation's expected propagation delay.
  3. Perform authoritative session refresh plus realtime socket replacement/reconnect.
  4. Confirm outbound House chat is rejected with the stable mute error.
  5. Confirm the muted user can still receive live messages and retrieve history.
  6. Persist muted = false, repeat refresh plus socket reconnect, and confirm sending is restored.
  7. Verify a maintenance restart does not claim success unless sessions were invalidated or refreshed before realtime reconnect.

Acceptance boundary

For this phase, persistence plus enforcement on newly authenticated/refreshed-and-reconnected sockets is sufficient. Immediate enforcement against an unchanged live socket is not required.