ServiceRealtime Hub
Service

Realtime Hub

RealtimeHub

MPO Version: 1.3.0

Defines a bidirectional realtime communication hub powered by Socket.IO. Each hub provides room-based messaging with built-in message types (text, image, video, audio, document, sticker, contact, location, system) and optional custom message types for app-specific structured data. Messages are always persisted to an auto-generated DataObject. The hub auto-bridges CRUD events from the room, membership, and message DataObjects via Kafka, and provides standard events (typing, read receipts, presence) as toggles. Hubs are mounted as Socket.IO namespaces and expose auto-generated REST endpoints.

interface RealtimeHub = {
  hubBasics : HubBasics;
  roomSettings : HubRoomSettings;
  hubRoles : HubRole[];
  messageSettings : HubMessageSettings;
  eventSettings : HubEventSettings;
  historySettings : HubHistorySettings;
  guardrails : HubGuardrails;
  hubLogic : HubLogic;
}
FieldDescription
hubBasicsundefined
roomSettingsundefined
hubRolesundefined
messageSettingsundefined
eventSettingsundefined
historySettingsundefined
guardrailsundefined
hubLogicundefined

HubBasics

MPO Version: 1.3.0

Core identity of the realtime hub. The name is used as the Socket.IO namespace path /hub/{name}, REST endpoint prefix /{name}, and code identifier.

interface HubBasics = {
  name : String;
  description : Text;
}
FieldDescription
nameUnique hub identifier within the service. Used as the Socket.IO namespace /hub/{name} and REST endpoint prefix. Must be a valid code name (camelCase, no spaces). Examples: chatHub, gameLobby, liveDashboard.
descriptionHuman-readable description of the hub's purpose. Used in documentation and AI assistant context.

HubRoomSettings

MPO Version: 1.3.0

Configures how rooms are managed and who can join them. Every hub requires a roomDataObject -- an existing DataObject that represents rooms. Authorization uses a layered flow: (1) roomEligibility checks if the room supports real-time features, (2) absoluteRoles bypass all checks, (3) checkRoles gates access to specific user roles, (4) checkScript applies a custom condition, (5) authSources and authScripts (OR-logic) determine the user's hub role. The first matching authSource/authScript wins (ordered highest-privilege first).

interface HubRoomSettings = {
  roomDataObject : LocalDataObjectName;
  roomEligibility : MScript;
  absoluteRoles : String[];
  checkRoles : String[];
  checkScript : MScript;
  authSources : HubAuthSource[];
  authScripts : HubAuthScript[];
}
FieldDescription
roomDataObjectName of the existing DataObject that represents rooms in this hub. The DataObject must be defined in the same service. Examples: Chat, ChessMatch, SupportTicket. Clients join rooms by referencing the ID of this DataObject.
roomEligibilityMScript condition evaluated against the room record (fetched from Elasticsearch) BEFORE any authorization. The context variable name is the roomDataObject name (for example, if roomDataObject is catalogEvent, use catalogEvent.chatEnabled). The generic alias room is also available. If false, the room does not support real-time features and NOBODY can join -- including absoluteRoles. Examples: catalogEvent.chatEnabled == true, catalogEvent.status == "published".
absoluteRolesList of role names (from session.roleId) that bypass ALL authorization checks. Users with any of these roles get the built-in system hub role with full permissions. Checked AFTER roomEligibility (even superAdmins cannot join ineligible rooms). Examples: ["superAdmin"], ["superAdmin", "systemAdmin"].
checkRolesList of role names that are REQUIRED to proceed to auth sources. If defined, users whose roleId is NOT in this list are immediately denied. Use to restrict hub access to specific user tiers. Examples: ["premiumUser", "subscriber"]. Leave empty to allow all roles.
checkScriptMScript condition evaluated after checkRoles and before authSources. Context variables: user (session data) and the roomDataObject name for the room record (for example, catalogEvent). The generic alias room is also available. Must return true to proceed. Examples: user.city == catalogEvent.city, user.subscriptionLevel >= catalogEvent.minLevel.
authSourcesOrdered array of DataObject-based authorization sources. Each source queries Elasticsearch for a record matching the user and room. Sources are checked top-to-bottom (highest-privilege first). The FIRST matching source determines the user's hub role -- remaining sources are skipped. Each source can have an optional condition MScript evaluated against the found record (context variable: record).
authScriptsOrdered array of MScript-based authorization sources. Checked after authSources if no authSource matched. Each script is evaluated with user (session) and room (room record) context. The FIRST script returning true determines the user's hub role. Use for complex authorization logic that can call library functions.

HubAuthSource

MPO Version: 1.3.0

A DataObject-based authorization source for hub room access. Queries Elasticsearch for a record in the specified DataObject where userField matches the authenticated user and roomField matches the target room. If a record is found (and the optional condition passes), the user is granted the specified hubRole. Sources are evaluated top-to-bottom; the first match wins.

interface HubAuthSource = {
  name : String;
  sourceObject : DataObjectName;
  userField : PropRefer;
  roomField : PropRefer;
  condition : MScript;
  hubRole : String;
}
FieldDescription
nameUnique identifier for this authorization source. Used as a reference name in logs and documentation. Examples: ticketHolder, eventOwner, hostAssignment.
sourceObjectName of the DataObject to query in Elasticsearch. Can be the room DataObject itself (for ownership checks), a membership/join-table DataObject, or any other DataObject. The query searches for a record where userField = current userId AND roomField = roomId. Examples: issuedTicket, eventHost, catalogEvent.
userFieldProperty on the DataObject that holds the user identifier. Matched against the authenticated user's userId. Examples: userId, ownerUserId, createdBy, playerId.
roomFieldProperty on the DataObject that holds the room identifier. Matched against the room's ID. Use id when checking the room DataObject itself (ownership pattern). Examples: eventId, chatId, id.
conditionOptional MScript condition evaluated against the found record. The context variable name matches the sourceObject name (for example, if sourceObject is issuedTicket, use issuedTicket.status). The generic alias record is also available. If the condition returns false, this source does not match and the next source is tried. Examples: issuedTicket.status == "active", eventHost.role == "lead".
hubRoleThe hub role assigned to the user if this source matches. Must correspond to a role defined in the hubRoles section. If null, the user is authorized but gets no specific role (all members have equal permissions). Examples: owner, moderator, attendee.

HubAuthScript

MPO Version: 1.3.0

An MScript-based authorization source for hub room access. The script is evaluated with user (session data) and room (room record from Elasticsearch) as context variables. Scripts can call library functions for complex logic. If the script returns true, the user is granted the specified hubRole. Scripts are evaluated after authSources, top-to-bottom; the first match wins.

interface HubAuthScript = {
  name : String;
  script : MScript;
  hubRole : String;
}
FieldDescription
nameUnique identifier for this authorization script. Used as a reference name in logs and documentation. Examples: vipAccess, geoRestriction, publicRoomGuest.
scriptMScript expression that must evaluate to a boolean. Context variables: user (authenticated session with userId, roleId, etc.) and room (the room DataObject record). Can call library functions. Examples: lib.checkVIPAccess(user, room), user.tier == "gold" && room.isPublic.
hubRoleThe hub role assigned to the user if this script returns true. Must correspond to a role defined in the hubRoles section. If null, the user is authorized but gets no specific role (all members have equal permissions).

HubRole

MPO Version: 1.3.0

Defines a named permission set for hub users. Each role controls what actions a user can perform within a room. Roles are assigned by authSources, authScripts, or absoluteRoles. A built-in system role (all permissions true) is automatically used for absoluteRoles users. When globalModeration or the role's moderated flag is true, messages require moderator approval before broadcast.

interface HubRole = {
  name : String;
  canRead : Boolean;
  canSend : Boolean;
  allowedMessageTypes : String;
  moderated : Boolean;
  canModerate : Boolean;
  canDeleteAny : Boolean;
  canManageRoom : Boolean;
}
FieldDescription
nameUnique role name. Referenced by authSources and authScripts via their hubRole property. Convention: lowercase descriptive names. Examples: owner, moderator, attendee, viewer.
canReadWhen true, the user can receive messages and events in the room. When false, the user is in the room but receives nothing (rare use case). Default: true.
canSendWhen true, the user can send messages. When false, the user is read-only (viewer). Default: true.
allowedMessageTypesComma-separated list of message types this role can send. If empty, all hub message types are allowed. Use to restrict certain roles to text-only or exclude media types. Examples: text, text,image.
moderatedWhen true, messages from users with this role are saved as pending and require moderator approval before being broadcast to the room. Default: false.
canModerateWhen true, the user can approve/reject pending messages, and block/silence other users. Default: false.
canDeleteAnyWhen true, the user can delete any message in the room (not just their own). Default: false.
canManageRoomWhen true, the user can update room settings and kick users from the room. Default: false.

HubMessageSettings

MPO Version: 1.3.0

Controls message structure and persistence. Every hub auto-generates a Message DataObject with system fields (id, roomId, senderId, timestamp), a messageType enum column, and a content JSONB column. Designers select which built-in message types to support (text, image, video, etc.) and can define custom message types with app-specific field schemas. Cross-cutting features (reply threading, reactions, forwarded flag) are toggleable across all message types.

interface HubMessageSettings = {
  dataObjectName : String;
  messageTypes : HubMessageType[];
  enableReplyTo : Boolean;
  enableReaction : Boolean;
  enableForwarded : Boolean;
  customMessageTypes : HubCustomMessageType[];
}
FieldDescription
dataObjectNameName for the auto-generated Message DataObject. Must be a valid code name. Convention: {HubName}Message. Example: ChatHubMessage, GameLobbyMessage. If omitted, defaults to {hubName}Message.
messageTypesBuilt-in message types to support in this hub. Each type has a known content schema managed by the framework. Select the types relevant to your use case. The messageType column in the DataObject becomes an enum of all selected built-in types plus any custom message type names.
enableReplyToWhen true, adds a replyTo JSON field to all messages for threading/reply support. Contains an object like { id, preview }. Works across all message types.
enableReactionWhen true, adds a reaction JSON array field to all messages for emoji reactions. Each entry has emoji, userId, timestamp. Works across all message types.
enableForwardedWhen true, adds a forwarded Boolean field to all messages indicating if the message was forwarded from another room. Works across all message types.
customMessageTypesApp-specific message types beyond the built-in set. Each custom type defines a name (added to the messageType enum) and a field schema validated at runtime. Use for structured payloads like game moves, bids, polls, or domain-specific actions. The fields are stored in the message's content JSONB column.

HubMessageType

Built-in message content types for hub messages. Each type defines a known content schema stored in the message's content JSONB column.

const HubMessageType = {
  text: "text",
  image: "image",
  video: "video",
  audio: "audio",
  document: "document",
  sticker: "sticker",
  contact: "contact",
  location: "location",
  system: "system",
};
EnumDescription
textPlain text message. Content: { body }.
imageImage message. Content: { mediaUrl, thumbnail, caption, width, height }.
videoVideo message. Content: { mediaUrl, thumbnail, caption, duration }.
audioAudio/voice message. Content: { mediaUrl, duration, waveform }.
documentFile/document message. Content: { mediaUrl, fileName, fileSize, mimeType }.
stickerSticker image. Content: { stickerUrl, stickerPackId }.
contactShared contact card. Content: { contactName, contactPhone, contactUserId }.
locationLocation pin. Content: { lat, lng, address, label }.
systemAuto-generated system message (member joined, room renamed, etc.). Content: { systemAction, systemData }.

HubCustomMessageType

MPO Version: 1.3.0

Defines a custom (app-specific) message type for the hub. Custom message types extend the built-in types with designer-defined names and field schemas. Each custom type adds its name to the messageType enum in the generated DataObject. The content JSONB column stores the fields defined here, validated at runtime. Examples: chessMove with from/to/piece fields, nudge with animation field, bid with amount/currency fields.

interface HubCustomMessageType = {
  name : String;
  description : String;
  fields : HubCustomField[];
}
FieldDescription
nameUnique name for this custom message type. Becomes an entry in the messageType enum alongside built-in types. Must be a valid code name (camelCase). Examples: chessMove, nudge, bid, pollVote.
descriptionHuman-readable description of what this message type represents and when it is used.
fieldsTyped fields that make up this custom message type's content schema. Each field has a name, type, and optional constraints. Fields are validated before broadcast and persistence. The entire set is stored in the message's content JSONB column.

HubCustomField

MPO Version: 1.3.0

Defines a single field in a custom message type's content schema. Fields are stored inside the message's content JSONB column and validated before persistence and broadcast. For Enum types, provide comma-separated allowed values in enumValues.

interface HubCustomField = {
  name : String;
  fieldType : HubCustomFieldType;
  required : Boolean;
  description : String;
  enumValues : String;
  defaultValue : String;
}
FieldDescription
nameField name in the custom message content JSON. Must be a valid JavaScript identifier. Example: from, piece, bidAmount, animation.
fieldTypeData type of this field. Determines validation and serialization. Use Enum with enumValues for fixed-choice fields.
requiredWhen true, messages of this custom type must include this field. Validated before persistence and broadcast.
descriptionHuman-readable description of the field's purpose. Used in documentation and AI context.
enumValuesComma-separated list of allowed values when fieldType is Enum. Example: pawn,rook,knight,bishop,queen,king. Ignored for other field types.
defaultValueDefault value for this field if not provided in the message payload. Stored as a string and coerced to the field type at runtime.
HubCustomFieldType

Data types for fields in custom message type definitions.

const HubCustomFieldType = {
  String: "String",
  Text: "Text",
  Integer: "Integer",
  Decimal: "Decimal",
  Boolean: "Boolean",
  ID: "ID",
  DateTime: "DateTime",
  Enum: "Enum",
  JSON: "JSON",
};
EnumDescription
StringShort text, up to 255 characters.
TextLong-form text content.
IntegerWhole number.
DecimalFloating-point number.
BooleanTrue or false.
IDUUID or ObjectId reference.
DateTimeISO 8601 date-time string.
EnumOne of a fixed set of string values (defined in enumValues).
JSONArbitrary JSON object or array.

HubEventSettings

MPO Version: 1.3.0

Configures standard events, auto-bridged DataObject events, custom events, and external Kafka events. Standard events (typing, recording, read receipts, delivery receipts, presence) are toggleable -- the framework generates the socket handlers and payload validation. Auto-bridge automatically listens to CRUD events from the room, membership, and message DataObjects and broadcasts them to hub rooms. Custom events and Kafka events remain for app-specific signals and external sources.

interface HubEventSettings = {
  enableTypingIndicator : Boolean;
  enableRecordingIndicator : Boolean;
  enableReadReceipts : Boolean;
  enableDeliveryReceipts : Boolean;
  enablePresence : Boolean;
  autoBridgeDataEvents : Boolean;
  customEvents : HubCustomEvent[];
  kafkaEvents : HubKafkaEvent[];
}
FieldDescription
enableTypingIndicatorWhen true, enables built-in typing/stopTyping events. Clients emit hub:event with event typing and the server broadcasts hub:typing / hub:stopTyping to room members. Ephemeral (not persisted).
enableRecordingIndicatorWhen true, enables built-in recording/stopRecording events for voice message recording state. Ephemeral (not persisted).
enableReadReceiptsWhen true, enables built-in read receipt events. Clients emit messageRead with { roomId, lastReadTimestamp } and the server broadcasts hub:messageRead to room members. Persisted.
enableDeliveryReceiptsWhen true, enables built-in delivery receipt events. Clients emit messageDelivered when a message is received, server broadcasts hub:messageDelivered. Persisted.
enablePresenceWhen true, enables built-in online/offline/away presence events. The server tracks connection state and broadcasts hub:online, hub:offline, hub:away to room members. Ephemeral.
autoBridgeDataEventsWhen true (default), the hub automatically subscribes to Kafka CRUD events from the room DataObject, membership DataObject, and message DataObject. Events like memberJoined, memberLeft, messageEdited, messageDeleted, roomUpdated, roomClosed are broadcast to the appropriate rooms without manual configuration. If the system message type is enabled, these also generate system messages.
customEventsApp-specific events beyond the standard set. Define named signals with a direction (clientToRoom, serverToClient, serverToRoom) and an ephemeral flag. Use for domain-specific interactions like game clock updates, collaborative cursor tracking, or custom notifications that do not fit built-in standard events.
kafkaEventsExternal Kafka topic events to bridge into hub rooms. Use for events from other services or topics not covered by auto-bridge (which handles the hub's own room, membership, and message DataObjects). Each entry specifies a topic, a filter expression, and a target room expression.

HubCustomEvent

MPO Version: 1.3.0

Defines a custom event that clients or the server can emit within the hub. Custom events go beyond standard messages -- they carry typed payloads for signals like typing indicators, read receipts, game actions, or system notifications. Ephemeral events are broadcast but not persisted.

interface HubCustomEvent = {
  name : String;
  description : String;
  ephemeral : Boolean;
  direction : HubEventDirection;
}
FieldDescription
nameEvent name. Used as the Socket.IO event identifier. Examples: typing, messageRead, gameOver, presence. Convention: camelCase.
descriptionHuman-readable description of when and why this event is emitted.
ephemeralWhen true, the event is broadcast to room members but not persisted to the database. Use for high-frequency signals like typing indicators, cursor positions, or presence pings.
directionWho can emit this event and who receives it. clientToRoom = client emits, server broadcasts to room. serverToClient = server emits to a specific client. serverToRoom = server emits to all room members.

HubEventDirection

Defines the direction of a custom hub event.

const HubEventDirection = {
  clientToRoom: "clientToRoom",
  serverToClient: "serverToClient",
  serverToRoom: "serverToRoom",
};
EnumDescription
clientToRoomClient emits to the server, which broadcasts to all room members.
serverToClientServer emits directly to a specific client (for example, presence updates).
serverToRoomServer emits to all members of a room (for example, system messages, deletions).

HubKafkaEvent

MPO Version: 1.3.0

Bridges a Kafka topic message into the hub as a room-scoped event. When a matching message arrives on the configured Kafka topic, the filterExpression is evaluated. If it passes, the targetRoomExpression extracts the room ID, and the event is broadcast to all clients in that room. Use for backend-driven updates like participant changes, status transitions, or external system notifications.

interface HubKafkaEvent = {
  name : String;
  description : String;
  topic : String;
  filterExpression : MScript;
  targetRoomExpression : MScript;
}
FieldDescription
nameEvent name emitted to room clients when a matching Kafka message arrives. Example: participantAdded, statusChanged.
descriptionHuman-readable description of what this Kafka-bridged event represents.
topicKafka topic to subscribe to. The hub creates a consumer for this topic and evaluates incoming messages against the filterExpression.
filterExpressionMScript expression evaluated against each Kafka message to decide if it should be bridged. Has access to data (the parsed message payload). Example: data.eventType === "participant.added". If omitted, all messages on the topic are bridged.
targetRoomExpressionMScript expression to extract the target room ID from the Kafka message. Has access to data. Example: data.chatId. The event is broadcast only to clients in this room.

HubHistorySettings

MPO Version: 1.3.0

Controls whether message history is sent to clients when they join a room. When enabled, the server queries the message DataObject for the most recent messages and emits them as a hub:history event immediately after join confirmation.

interface HubHistorySettings = {
  historyEnabled : Boolean;
  historyLimit : Integer;
}
FieldDescription
historyEnabledWhen true, sends the most recent messages to a client when they join a room. Requires autoDataObject to be true in messageSettings.
historyLimitNumber of most recent messages to send when a client joins a room. Default: 50.

HubGuardrails

MPO Version: 1.3.0

Safety limits, rate controls, moderation settings, and caching for the hub. Prevents resource exhaustion, abuse, and oversized payloads. All values have sensible defaults but should be tuned for production use cases.

interface HubGuardrails = {
  maxUsersPerRoom : Integer;
  maxRoomsPerUser : Integer;
  messageRateLimit : Integer;
  maxMessageSize : Integer;
  connectionTimeout : Integer;
  authCacheTTL : Integer;
  globalModeration : Boolean;
  defaultSilenced : Boolean;
}
FieldDescription
maxUsersPerRoomMaximum number of concurrent users allowed in a single room. When exceeded, new join requests are rejected. Default: 1000.
maxRoomsPerUserMaximum number of rooms a single user can be joined to simultaneously. Prevents memory exhaustion from users joining too many rooms. Default: 50.
messageRateLimitMaximum number of messages a single user can send per minute across all rooms. Exceeding this triggers a temporary cooldown. Default: 60.
maxMessageSizeMaximum message payload size in bytes. Messages exceeding this limit are rejected. Default: 65536 (64 KB).
connectionTimeoutIdle connection timeout in milliseconds. Connections with no activity (no messages, no heartbeats) for this duration are disconnected. Default: 300000 (5 minutes).
authCacheTTLTime-to-live in seconds for the Redis auth cache. After a user's authorization is resolved, the result (hubRole, permissions) is cached in Redis. Subsequent joins within the TTL skip auth source checks. Set to 0 to disable caching. Default: 300 (5 minutes).
globalModerationWhen true, ALL messages in the hub require moderator approval before being broadcast, regardless of the sender's hubRole. When false, only messages from roles with moderated: true go through moderation. Default: false.
defaultSilencedWhen true, all users join rooms in a silenced state (canSend overridden to false) regardless of their hubRole. Users with canModerate permission are exempt. Other users must request speak permission, which a moderator can grant. Useful for webinar/classroom/town-hall scenarios. Default: false.

HubLogic

MPO Version: 1.3.0

Server-side logic hooks for the hub. Allows intercepting custom message types with MScript before broadcast, fetching room state on join or on demand, and running scheduled actions on an interval. MScript hooks can call service library functions, Redis, DB, Business APIs, or external services -- the hub just provides the hook points and acts on the return values. Intercepted messages are queued per-room to prevent race conditions.

interface HubLogic = {
  stateScript : MScript;
  messageHandlers : HubMessageHandler[];
  scheduledActions : HubScheduledAction[];
}
FieldDescription
stateScriptMScript called when a user joins a room or emits hub:requestState. Must return the current room state (any shape). The return value is sent to the requesting client only via hub:state event. Context: session (user session), roomId, room (room DataObject record), lib (service library). Example: lib.chess.getState(roomId). Leave empty if the hub does not need state management.
messageHandlersArray of message type interceptors. When a client sends a custom message type that matches a handler, the message is NOT broadcast immediately. Instead, the handler's script is called and the return value determines accept/reject, broadcast behavior, and server-generated messages. Unmatched message types follow the standard flow (immediate broadcast + persist). Handlers are processed sequentially per room (no race conditions).
scheduledActionsArray of interval-based actions that run while a room has connected users (or always if keepAlive is true). Each action calls its script on the configured interval. The script can return serverMessages to broadcast to the room. Use for game ticks, world updates, auction countdowns, or periodic state broadcasts.

HubMessageHandler

MPO Version: 1.3.0

Intercepts a specific custom message type and routes it through an MScript before broadcast. The script receives the message, sender session, room record, and service library, and returns an object controlling accept/reject, broadcast targets, message modifications, persistence, and server-generated messages. Use for game moves, bids, collaborative edits, or any message that needs server-side validation or state management.

interface HubMessageHandler = {
  messageType : String;
  script : MScript;
}
FieldDescription
messageTypeThe custom message type name to intercept. Must match a name from customMessageTypes in HubMessageSettings. When a client sends a message of this type, the handler script is called instead of the standard broadcast flow.
scriptMScript called when a message of this type is received. Context: session (sender session), message (an object like { type, content }), roomId, room (room DataObject record), lib (service library). The script must return an object that controls behavior. Example call: lib.chess.processMove(roomId, session, message).

The returned object from the handler script has a shape like:

{
  accept: boolean;
  rejectReason?: string;
  broadcast?: boolean;
  broadcastTo?: "all" | "others" | "sender" | string[];
  modifiedContent?: Record<string, unknown>;
  persist?: boolean;
  serverMessages?: Array<{
    type: string;
    content: Record<string, unknown>;
    to: "all" | "others" | "sender" | string[];
    persist?: boolean;
  }>;
}

HubScheduledAction

MPO Version: 1.3.0

A recurring action that runs on an interval for each active room. The script is called every intervalMs milliseconds and can return serverMessages to broadcast. The interval starts when the first user joins the room and stops when the last user disconnects, unless keepAlive is true. Use for game world ticks, NPC movement, countdown timers, or periodic state sync.

interface HubScheduledAction = {
  name : String;
  intervalMs : Integer;
  keepAlive : Boolean;
  script : MScript;
}
FieldDescription
nameUnique identifier for this scheduled action. Used for logging and lifecycle management.
intervalMsInterval in milliseconds between script invocations. Example: 1000 for a 1-second game tick, 60000 for a 1-minute cycle. Minimum recommended: 100ms.
keepAliveWhen true, the scheduled action keeps running even when no users are connected to the room. Use for game worlds that need to evolve independently of player presence. When false (default), the action starts on the first join and stops on the last disconnect.
scriptMScript called on each interval tick. Context: roomId, room (room DataObject record), lib (service library). The script must return an object that can include serverMessages to broadcast. Example: lib.world.tick(roomId).

The script return value has a shape like:

{
  serverMessages?: Array<{
    type: string;
    content: Record<string, unknown>;
    to: "all" | "others" | "sender" | string[];
    persist?: boolean;
  }>;
}