Skip to content

Session extension (long-term session)

A long-term session ties your extension logic to the current broadcast for a bounded time. While it is active, the platform exposes a stable sessionId, remaining ttl, and session-scoped APIs (v1.session.*) and events (v1.session.updated, v1.session.whispered.model). Only one session can be active per model at a time.

This guide describes a typical session-based extension flow: the model starts a session, paying viewers join, clients exchange custom payloads through session whispers, and the model (or expiry) ends the session.

Quick choice: activity vs session

  • Use v1.ext.activity.request for short visual actions (reactions, short animations, brief status indicators) that do not need persistence.
  • Use v1.session.* when the flow must survive reboots, remain consistent for newly joined users, or keep long-running shared state.

Shared slots and moderation

An active long-term session locks the viewer-facing overlay slot your extension uses for that flow, for example EXTENSION_SLOT_RIGHT_OVERLAY, or another overlay slot your manifest declares for that mechanic, depending on category and setup. While the session is running, no other extension can use that same slot on the broadcast; competing extensions that declare the same slot are blocked from showing their UI there until the session ends.

Because of that shared-resource impact, only adopt a session when your design truly needs stream-scoped coordination and APIs like v1.session.*. Prefer short, purposeful TTLs, end sessions promptly with v1.session.finish.model when the experience is over, and avoid holding a session open without a clear viewer benefit.

Moderation actively monitors appropriate use of sessions and shared overlay slots; misuse or unnecessary locking of shared UI may be subject to review under platform rules.

Session snapshot

v1.session.get always returns a session object (TV1Session):

When session is active and belongs to this extension:

  • owner: 'self'
  • meta.sessionId — Platform session id
  • meta.ttlSec — Remaining lifetime in seconds

When session exists but belongs to a different extension:

  • owner: 'other'

When owner: 'other', your extension must treat the session as unavailable for its own flow (for example disable or hide the Join/Start controls).

When no session is active:

  • session is null

Who can call what

MethodRolePurpose
v1.session.join.modelModel onlyCreate/start the session when none is active. If a session is already active, this call does not recreate or replace it. Pass ttl (seconds, minimum 1).
v1.session.join.userLogged-in viewerJoin after a successful token payment; pass payment (TV1PaymentData) from the payment success path and ttl. If no session is active, this call can create one.
v1.session.finish.modelModel onlyClear the session for this stream.
v1.session.whisper.userUser onlyPropose a session action; the model receives v1.session.whispered.model, validates, updates state, then typically broadcasts the result (e.g. v1.ext.whisper).
v1.session.getEitherRead the current snapshot.

Session events

Subscribe with subscribe (and unsubscribe when disposing). Session-related events:

EventWho receives itPurpose
v1.session.updatedModel and viewers (every client with this extension on the stream)Lifecycle updates: data.action is join or finish; data.session is the current TV1Session snapshot (or null when inactive); data.userId ties the update to a user when relevant (TV1SessionUpdate).
v1.session.whispered.modelModel (broadcaster) onlyA viewer called v1.session.whisper.user; payload is TV1SessionWhisperData. Validate the proposal, update authoritative state, then publish to the room (e.g. v1.ext.whisper). Viewers do not get this event.

Model vs viewer: Viewers join participation through v1.session.join.user (after payment). If no session exists, either model join or user join can create it. Once active, the session is unique per model and cannot be recreated/replaced by another extension until it finishes. The active TTL is set by whoever created the session first (model or user).

Handling session state changes

Subscribe to v1.session.updated in the background slot both as a model and as a user. The background slot should normalize the session state by checking the TV1SessionUpdate object, and then notify other slots by using v1.ext.whisper.local.

Different session states

Carefully handle possible sesstion updates, especially by handling combinations of session being null, different session.owner and data.action values.

Model: start and stop

Model starting the session

This example flow shows how to start a session from the model UI. You don't need to use v1.payment.tokens.spend for the model to join, since they always have access to the session on their stream. Note, that you should handle v1.session.updated for both model and user clients to properly sync the UI on the stream.

After starting a session with v1.session.join.model, rely on v1.session.updated with action: 'join' for fresh session.meta.ttl and session.meta.sessionId (when session.owner === 'self'). See the following flow on how to restore the session state correctly on the user side, and how to sync it across different users.

Model finishing the session

The finish flow is simpler: you just need to call v1.session.finish.model from the model client, and handle v1.session.updated with action: 'finish' on all clients to update the UI and clear the state.

User: paid join

This flow shows how to join a session as a user. Note, that you should save the playing user in the storage as part of the joining proccess, in order to be able to restore the session state later and sync it across different users.

ts
extHelper.subscribe('v1.payment.tokens.spend.succeeded', async ({ paymentData, tokensSpendData }) => {
  if (tokensSpendData.action === 'join-session-user') {
    await extHelper.makeRequest('v1.session.join.user', {
      ttl: '200',
      payment: paymentData,
    });
  }
});

Use any stable tokensSpendData.action value you define, as long as the spend handler and your UI agree on it.

Custom actions in-session

Viewers call v1.session.whisper.user with extension-defined data (e.g. a proposed move or vote). Only the model (broadcaster) client receives v1.session.whispered.model with that payload—use it to validate the action, apply rules, and update authoritative state. After that, publish the updated state to everyone (typically v1.ext.whisper to the room, plus v1.ext.whisper.local for model slots).

For coordination only between the model's slots (no viewer proposal), use v1.ext.whisper.local so you never rely on the viewer-only session whisper path.

The platform does not interpret your payloads—you define the schema (commands, state sync messages, etc.).

Reference architecture

A common pattern uses a background slot as the single authority for shared extension state:

  • On load it reads v1.ext.context.get and v1.session.get, decides whether the current client is the model, and hydrates state from v1.storage.string.get using a key derived from session.meta.sessionId (when session.owner === 'self'), so a new session starts with a clean key.
  • It subscribes to v1.session.updated: on join, it refreshes TTL and records the joining user id; on finish, it resets local state.
  • It subscribes to v1.payment.tokens.spend.succeeded in the background slot and performs v1.session.join.user there so the join completes in one place.
  • Viewer proposals arrive on the model via v1.session.whispered.model after v1.session.whisper.user; model-only coordination uses v1.ext.whispered.local. The reducer updates state, persists to storage (model only), then broadcasts authoritative state with v1.ext.whisper to the room and syncs model slots with v1.ext.whisper.local.
  • Overlay and menu slots stay thin: they render state from v1.ext.whisper / v1.ext.whispered.local, while controls call v1.session.join.model only to open a new session and v1.session.finish.model to end it.

When the TTL counts down to zero, treat the session as ended locally and have the model client call v1.session.finish.model to clear the platform session (avoid duplicate finishes with a small guard).

Game session restoration

Use this flow to correctly initialize or restore extension state. The background slot should fetch the context, storage and session data, and verify that the current session belongs to this extension and that the current user participates in the game, then notify other slots using v1.ext.whisper.local. If the user closes and opens Main Games & Fun tab again, the UI should request the latest state again and update accordingly.

Checklist

  1. Use v1.session.get before Start/Join UI decisions. If session.owner === 'other', block controls for this extension.
  2. If no session is active, either v1.session.join.model (model) or v1.session.join.user (viewer after payment) can create one.
  3. Only one session can be active per model. Once active, it cannot be recreated/replaced by another extension until it finishes.
  4. The active TTL is defined by the first creator (model or user) and applies while that session is active.
  5. Viewers pay, then join with v1.session.join.user + payment from the success event.
  6. Keep authoritative state in one place (often background), key persisted state by session.meta.sessionId (when owner === 'self') if sessions must not bleed into each other.
  7. Viewers propose actions with v1.session.whisper.user; the model handles v1.session.whispered.model, then pushes authoritative updates with v1.ext.whisper / v1.ext.whisper.local. Use v1.ext.whisper.local alone when only model slots need to coordinate.
  8. End explicitly with v1.session.finish.model (model) or let TTL drive your UX and still call it when the session should release the slot.

See also