Astro Chat

Official Astro Chat documentation for installing the plugin, styling TextChatService, configuring secure chat tags, sending system messages and maintaining the generated runtime.

On this page

Release and build versions

The storefront release is Astro Chat 1.0. The plugin package included with that release identifies its editor, schema and generated runtime as 1.6.1.

These numbers describe different layers:

Version Meaning
Product release 1.0 The public release shown on the GoldAstro product page and in release notes
Plugin/runtime 1.6.1 The internal build written into the supplied Studio plugin and generated runtime
Schema 7 The configuration structure used by the included build

When asking for support, include both the product release and the internal plugin/runtime build.

Before you install

Astro Chat is a Roblox Studio plugin built for the modern TextChatService system. It is not a replacement chat service and does not provide a separate hosted backend.

You need:

  • Roblox Studio and permission to install plugins.
  • An experience using TextChatService rather than the legacy chat system.
  • A safe test place or unpublished copy of the experience.
  • Test accounts for every important UserId or group-rank rule.

Back up or publish a Studio version before the first Apply. If another script already assigns TextChatService.OnIncomingMessage, OnChatWindowAdded or OnBubbleAdded, plan the integration carefully because only one callback can occupy each property at a time.

Apply, Save, Repair and Remove Runtime must be performed in Edit mode. The plugin intentionally refuses to rewrite generated scripts during Play or Play Here.

Quick start

  1. Install Astro Chat from the Roblox Creator Store when the listing becomes available.
  2. Open the experience in Roblox Studio.
  3. Select Astro Chat from the Plugins toolbar.
  4. Open Chat and choose the global chat and bubble appearance.
  5. Open Tags and configure the supplied Owner, Developer, Moderator and VIP examples or create your own.
  6. Set each tag’s access rule before enabling Auto equip or exposing a client-side tag picker.
  7. Open Messages and configure the system message types the experience will send.
  8. Stop Play or Play Here, return to Edit mode and select Apply.
  9. Start Play and test real player messages, bubble chat and server messages.
  10. Return to Edit mode and select Save after any final changes.

The @everyone tag and message type are protected fallbacks. They cannot be deleted, and the fallback tag keeps priority 1.

Generated runtime

Apply creates or updates this managed structure:

ReplicatedStorage
└── AstroChat
    ├── AstroChatAPI
    ├── Bridge
    ├── Tags
    └── MessageTypes

ServerScriptService
└── AstroChat
    └── AstroChatServer

StarterPlayer
└── StarterPlayerScripts
    └── AstroChat
        └── AstroChatClient

ReplicatedStorage.AstroChat also stores global configuration as attributes. Tag and message-type definitions are folders whose settings are attributes.

Do not edit the generated AstroChatAPI, AstroChatServer or AstroChatClient source directly. Use the plugin and select Save or Settings → Repair generated runtime after a plugin update. Manual edits can be replaced the next time Astro Chat writes the runtime.

Runtime responsibilities

  • AstroChatAPI exposes the supported server and client functions.
  • Bridge carries limited tag requests and styled system messages between server and clients.
  • AstroChatServer applies automatic tags and validates client tag requests.
  • AstroChatClient formats incoming player messages, system messages and speech bubbles locally through TextChatService.

The configuration and runtime are stored in the place. The plugin itself does not need to remain open when a published server runs.

Chat configuration

The Chat panel edits Roblox TextChatService presentation. The included build supports:

  • Chat font and weight.
  • Message text size and color.
  • Chat-window background color and transparency.
  • Input-bar background, text, placeholder color and text size.
  • Chat-window width and height scale.
  • Chat-window, input-bar and channel-tab visibility.
  • Bubble-chat visibility, font, text size, text color, background color and transparency.
  • Tag visibility, tag separator and front/back placement.
  • Single-tag or stacked-tag display.
  • Priority-based player-name and message colors.
  • Name suffix text and white, strongest-tag or custom suffix color.
  • Live synchronization during play testing.

Use the property search field to jump to a setting in a long panel.

Stacked-tag behavior

When Stack equipped tags is enabled, every explicitly equipped tag prefix can be shown. Tags are sorted by priority, then by name when priorities match. The highest-priority equipped tag controls the username color, optional message color, per-message font size and tag-specific bubble override.

When stacking is disabled, equipping a tag replaces the previously equipped non-fallback tag.

Name and message styling

Color player names uses the strongest tag’s username color. A blank per-tag message color inherits the global chat text color. A tag font size of -1 inherits the global chat size.

Player tags

The supplied examples are Owner, Developer, Moderator and VIP, plus the protected Everyone fallback. Examples are starting points only; configure their access before publishing.

Each non-fallback tag supports:

Setting Purpose
Tag ID Stable name used by the API
Display name Human-readable name in the editor
Prefix Text shown around the player’s name
Placement Shows the prefix in front of or behind the name
Tag, username and message colors Separates prefix, name and message presentation
Font size and emphasis Overrides prefix size, bold, italic or underline styling
Priority Determines ordering and the strongest style when tags stack
Enabled Makes the tag available to the runtime
Allow everyone Allows any player to equip the tag
Auto equip Equips the tag automatically for eligible players
Allowed UserIds Comma-separated account IDs allowed to use the tag
Group ID and minimum rank Allows members at or above a group rank

Use -1 for an unused UserId, group ID or minimum group rank field. Keep tag IDs at 40 characters or fewer and use letters, numbers, underscores or hyphens.

Access rules

A player is eligible when the tag is enabled and at least one configured rule allows access. The protected fallback is always available. Non-fallback tags may use an open-to-everyone rule, an explicit UserId list, or a valid group and minimum rank.

The server checks access for both direct server calls and client requests. Hiding a tag button in local UI is not a permission boundary.

Tag-specific speech bubbles

Enable Bubble custom style to let the strongest tag override bubble text color, background color, transparency, text size, font, weight and tail visibility. Blank colors and numeric values of -1 inherit global bubble settings.

Auto equip

Auto-equip runs when a player joins. If multiple eligible tags have Auto equip enabled, the highest-priority eligible tag wins. With tag stacking enabled, players can later add other permitted tags through the API.

Message types

Message types style server-created system messages. The supplied build includes System, Announcement, Warning and Error, plus the protected Everyone fallback.

A message type controls:

  • Stable type ID and editor display name.
  • Prefix and color.
  • Optional font-size override.
  • Bold, italic and underline prefix styling.
  • Whether a non-fallback type is enabled.

Use the preview Send button during Play or Play Here to test a type through Roblox’s real chat. A font size of -1 inherits the global chat size.

Message types do not bypass Roblox text filtering for player-authored content. Do not relay untrusted player text through server system messages as a way to avoid normal filtering.

Runtime API

The same AstroChatAPI ModuleScript can be required by server Scripts and LocalScripts. Individual methods enforce where they may run. Use the server for authoritative game decisions and LocalScripts only for a player’s permitted tag requests.

Require the API

From a server Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AstroChat = ReplicatedStorage:WaitForChild("AstroChat")
local ChatAPI = require(AstroChat:WaitForChild("AstroChatAPI"))

From a LocalScript, use the same path:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatAPI = require(
    ReplicatedStorage:WaitForChild("AstroChat"):WaitForChild("AstroChatAPI")
)

The API exposes ChatAPI.Version, which is 1.6.1 in the package supplied with product release 1.0.

API quick reference

Method Context Result
GetTag(tagId) Server or client One enabled tag Folder, or nil
GetMessageType(typeId) Server or client One enabled message-type Folder, or nil
GetTags() Server or client All tag Folders, sorted by priority
GetMessageTypes() Server or client All message-type Folders, sorted by ID
IsTagAllowed(player, tagId) Server only Boolean permission result
GetEquippedTags(player) Server or client Equipped tag Folders in priority order
GetEquippedTag(player) Server or client Strongest equipped tag Folder, or nil
EquipTag(player, tagId) Server only Success boolean and tag ID or error message
UnequipTag(player, tagId) Server only Success boolean and optional error message
ClearTags(player) Server only Success boolean and optional error message
RequestTag(tagId) Client only Whether a valid request was sent
RequestUnequipTag(tagId) Client only Whether a valid request was sent
RequestClearTags() Client only Whether a valid request was sent
SendMessage(text, typeId, target?) Server only Success boolean and optional error message

GetTag

local tag = ChatAPI:GetTag("Moderator")
if tag then
    print(tag:GetAttribute("DisplayName"))
    print(tag:GetAttribute("Priority"))
end

GetTag(tagId) returns the enabled managed tag Folder with that exact ID. It returns nil for an unknown or disabled tag. Use the Folder’s attributes to read its configured presentation; do not change those attributes at runtime as a substitute for saving through the plugin.

GetMessageType

local messageType = ChatAPI:GetMessageType("Announcement")
if messageType then
    print(messageType:GetAttribute("Prefix"))
    print(messageType:GetAttribute("Color"))
end

GetMessageType(typeId) returns the enabled message-type Folder or nil. Check it before sending a message when the type name comes from configuration.

GetTags

local tags = ChatAPI:GetTags()
for _, tag in ipairs(tags) do
    print(tag.Name, tag:GetAttribute("Priority"))
end

GetTags() returns all managed tag Folders in display priority order. It is useful for building an in-game tag selector, but the server still decides which tags a player may equip.

GetMessageTypes

for _, messageType in ipairs(ChatAPI:GetMessageTypes()) do
    print(messageType.Name, messageType:GetAttribute("DisplayName"))
end

GetMessageTypes() returns message-type Folders alphabetically by ID. This is useful for developer tools or validating a configurable message type.

IsTagAllowed

local allowed = ChatAPI:IsTagAllowed(player, "VIP")
if allowed then
    print(player.Name, "may use VIP")
end

IsTagAllowed(player, tagId) is server-only. It checks that the Player is valid, the tag exists and is enabled, and one of its configured access rules allows the player. On a client it always returns false.

GetEquippedTags

local equipped = ChatAPI:GetEquippedTags(player)
for index, tag in ipairs(equipped) do
    print(index, tag.Name)
end

GetEquippedTags(player) returns the player’s explicit non-fallback tags in priority order. An empty table means the player is using only the fallback presentation.

GetEquippedTag

local strongest = ChatAPI:GetEquippedTag(player)
if strongest then
    print("Strongest tag:", strongest.Name)
else
    print("Using the fallback tag")
end

GetEquippedTag(player) is a convenience method that returns the first, strongest explicit tag or nil.

EquipTag

EquipTag(player, tagId) is server-only and always checks the configured tag permission. When stacking is enabled, it adds the new tag without duplicating it. When stacking is disabled, it replaces the current explicit tag.

local ok, tagIdOrError = ChatAPI:EquipTag(player, "VIP")
if ok then
    print("Equipped:", tagIdOrError)
else
    warn("Could not equip tag:", tagIdOrError)
end

Equipping the protected fallback clears explicit tags and returns the fallback tag ID.

UnequipTag

local ok, errorMessage = ChatAPI:UnequipTag(player, "VIP")
if not ok then
    warn(errorMessage)
end

UnequipTag(player, tagId) is server-only. It removes the matching explicit tag and preserves any other equipped tags.

ClearTags

local ok, errorMessage = ChatAPI:ClearTags(player)
if not ok then
    warn(errorMessage)
end

ClearTags(player) is server-only and removes every explicit tag so the fallback presentation is used. ClearTag is a compatibility alias that performs the same clear-all operation; despite the singular name, it does not accept a tag ID.

RequestTag

Use this from a LocalScript when the player selects a tag in your own UI:

local sent, errorMessage = ChatAPI:RequestTag("VIP")
if not sent then
    warn(errorMessage)
end

RequestTag(tagId) is client-only. A true result means the request was well formed and sent; it does not mean the tag was granted. The generated server validates permission before equipping it.

RequestUnequipTag

local sent, errorMessage = ChatAPI:RequestUnequipTag("VIP")
if not sent then
    warn(errorMessage)
end

RequestUnequipTag(tagId) is client-only and asks the server to remove one explicit tag.

RequestClearTags

local sent, errorMessage = ChatAPI:RequestClearTags()
if not sent then
    warn(errorMessage)
end

RequestClearTags() is client-only and asks the server to return the player to fallback presentation.

SendMessage

SendMessage(text, messageType, target) is server-only. Omit target to broadcast, pass one Player for a private message, or pass an array of Players for a selected audience.

-- Everyone
ChatAPI:SendMessage("The round begins in 30 seconds.", "Announcement")

-- One player
ChatAPI:SendMessage("Your loadout was saved.", "System", player)

-- Selected players
ChatAPI:SendMessage(
    "Moderator briefing ready.",
    "Warning",
    {leadModerator, respondingModerator}
)

Always check the return value when delivery is important:

local ok, errorMessage = ChatAPI:SendMessage(
    "Your daily reward is ready.",
    "System",
    player
)

if not ok then
    warn("Astro Chat message failed:", errorMessage)
end

The message type must exist and be enabled. The function returns false with an explanation for an empty message, unknown type or invalid target. If messageType is not a string, it defaults to System.

Keep server system messages short and trustworthy. Do not pass raw user-submitted text into SendMessage without applying the filtering and policy required for that text.

Use-case recipes

These recipes assume Astro Chat has already been applied and the referenced tag or message-type IDs exist in the plugin configuration.

Automatically equip a configured tag when players join

For a UserId or group-rank tag, the simplest method is built in:

  1. Open Tags in the plugin.
  2. Select the tag.
  3. Configure Allowed UserIds, or Group id and Minimum group rank.
  4. Turn Auto equip on.
  5. Save the runtime in Edit mode.

The generated server checks new players and equips the highest-priority eligible automatic tag. No extra Script is required.

For game-specific logic, place this Script in ServerScriptService:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatAPI = require(
    ReplicatedStorage:WaitForChild("AstroChat"):WaitForChild("AstroChatAPI")
)

Players.PlayerAdded:Connect(function(player)
    if ChatAPI:IsTagAllowed(player, "Member") then
        local ok, errorMessage = ChatAPI:EquipTag(player, "Member")
        if not ok then
            warn("Member tag failed for", player.Name, errorMessage)
        end
    end
end)

The Member tag still needs an Astro Chat access rule that permits the player. Server code cannot use EquipTag to bypass the plugin’s permission configuration.

Send a welcome message to the joining player

Place this Script in ServerScriptService:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatAPI = require(
    ReplicatedStorage:WaitForChild("AstroChat"):WaitForChild("AstroChatAPI")
)

Players.PlayerAdded:Connect(function(player)
    task.delay(2, function()
        if not player.Parent then
            return
        end

        ChatAPI:SendMessage(
            "Welcome! Use /help to view the available commands.",
            "System",
            player
        )
    end)
end)

The short delay lets the player’s chat client finish loading before the targeted message is sent.

Announce a staff member joining

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatAPI = require(
    ReplicatedStorage:WaitForChild("AstroChat"):WaitForChild("AstroChatAPI")
)

Players.PlayerAdded:Connect(function(player)
    if ChatAPI:IsTagAllowed(player, "Moderator") then
        ChatAPI:EquipTag(player, "Moderator")
        ChatAPI:SendMessage(
            "A moderator has joined the server.",
            "Announcement"
        )
    end
end)

Avoid broadcasting private staff status if your moderation policy requires staff to remain undisclosed.

Send timed round announcements

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatAPI = require(
    ReplicatedStorage:WaitForChild("AstroChat"):WaitForChild("AstroChatAPI")
)

local function announceCountdown(seconds)
    for remaining = seconds, 1, -1 do
        ChatAPI:SendMessage(
            string.format("Round starts in %d...", remaining),
            remaining <= 3 and "Warning" or "Announcement"
        )
        task.wait(1)
    end

    ChatAPI:SendMessage("The round has started!", "Announcement")
end

announceCountdown(5)

For a real round system, call announceCountdown() from the authoritative server round controller instead of creating an endless standalone loop.

Notify only one player about a saved action

local function confirmSave(player)
    local ok, errorMessage = ChatAPI:SendMessage(
        "Your settings were saved successfully.",
        "System",
        player
    )

    if not ok then
        warn(errorMessage)
    end
end

Targeted system messages are useful for purchases, inventory actions, quests and validation errors that should not fill the public chat.

Message a selected team or staff list

local Players = game:GetService("Players")

local function getPlayersOnTeam(teamName)
    local selected = {}

    for _, player in ipairs(Players:GetPlayers()) do
        if player.Team and player.Team.Name == teamName then
            table.insert(selected, player)
        end
    end

    return selected
end

local defenders = getPlayersOnTeam("Defenders")
if #defenders > 0 then
    ChatAPI:SendMessage(
        "Return to the base and prepare for the next wave.",
        "Warning",
        defenders
    )
end

The target array should contain Player instances. Empty arrays are accepted but naturally reach nobody.

Equip group staff automatically on join

Configure a staff tag with the correct Group id, Minimum group rank and Auto equip. The built-in auto-equip path is enough for normal use. If another server system needs to run alongside it, you can check the same rule explicitly:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    if not ChatAPI:IsTagAllowed(player, "Moderator") then
        return
    end

    local ok, errorMessage = ChatAPI:EquipTag(player, "Moderator")
    if not ok then
        warn(errorMessage)
        return
    end

    ChatAPI:SendMessage(
        "Moderator tools are ready.",
        "System",
        player
    )
end)

The group lookup is protected with an internal service-call check. If Roblox cannot return a rank, permission is not granted by that rule.

Equip an explicit owner or developer account

Add the account’s numeric ID to the tag’s Allowed UserIds field, then enable Auto equip. For a deliberate manual workflow:

local OWNER_USER_ID = 123456789

local function equipOwnerTag(player)
    if player.UserId ~= OWNER_USER_ID then
        return
    end

    if not ChatAPI:IsTagAllowed(player, "Owner") then
        warn("Owner UserId is missing from the Astro Chat tag configuration")
        return
    end

    ChatAPI:EquipTag(player, "Owner")
end

Replace the example ID and keep the matching Allowed UserIds configuration in Astro Chat. The explicit check documents your game’s intent; Astro Chat’s server permission remains the final gate.

Let players choose from their allowed tags

Build your own tag menu in a LocalScript. Populate it from GetTags(), then send only the selected ID through RequestTag():

local availableTags = ChatAPI:GetTags()

for _, tag in ipairs(availableTags) do
    print("Tag option:", tag.Name)
end

TagButton.Activated:Connect(function()
    local sent, errorMessage = ChatAPI:RequestTag("VIP")
    if not sent then
        warn(errorMessage)
    end
end)

ClearTagButton.Activated:Connect(function()
    ChatAPI:RequestClearTags()
end)

The client can display possibilities, but it cannot authoritatively determine eligibility. The server rejects a tag whose access rule does not permit the requesting player.

Restore a saved tag preference

Save only the selected tag ID in your own data system. After loading it on the server, validate it through Astro Chat before equipping:

local function restoreSavedTag(player, savedTagId)
    if type(savedTagId) ~= "string" then
        return
    end

    if not ChatAPI:GetTag(savedTagId) then
        return
    end

    if ChatAPI:IsTagAllowed(player, savedTagId) then
        local ok, errorMessage = ChatAPI:EquipTag(player, savedTagId)
        if not ok then
            warn(errorMessage)
        end
    end
end

Do not save Folder instances. Store the stable tag ID and re-check permission every time the player joins because roles and entitlements can change.

Clear temporary tags between rounds

local Players = game:GetService("Players")

local function clearRoundTags()
    for _, player in ipairs(Players:GetPlayers()) do
        ChatAPI:UnequipTag(player, "RoundWinner")
        ChatAPI:UnequipTag(player, "MVP")
    end
end

Use UnequipTag when permanent identity tags should remain. Use ClearTags only when every explicit tag should be removed.

Validate message types before using configurable IDs

local function sendConfiguredMessage(text, configuredType)
    local messageType = ChatAPI:GetMessageType(configuredType)
    if not messageType then
        warn("Unknown Astro Chat message type:", configuredType)
        return false
    end

    return ChatAPI:SendMessage(text, messageType.Name)
end

This pattern is useful when another server module chooses a message type by configuration rather than a hard-coded constant.

Studio preview and saving

Preview chat accurately

Run Play or Play Here, open Astro Chat and use Roblox’s actual chat window. The selection previews in the editor help compare styles, but the real TextChatService window is the final rendering environment.

The Equip and Unequip controls in the Tags preview are Studio-only visual overrides. They do not grant published players access and do not replace server permission checks.

Apply, Save and Load

  • Apply installs the generated runtime for the first time.
  • Save writes configuration changes into an existing managed runtime.
  • Load imports the installed runtime configuration into the plugin editor.

The plugin tracks changes and labels an installed runtime as up to date or unsaved. While the editor remains open, it also periodically saves changed configuration. Always perform an explicit Save before closing Studio or publishing.

With Live sync enabled, supported presentation changes are applied to the temporary play-test DataModel. Play-test changes are restored to Edit mode when the test stops, then must be saved to become part of the place.

Upgrading and repairing

Use this upgrade sequence:

  1. Back up the place and publish a recoverable Studio version.
  2. Stop every running play test.
  3. Update the Astro Chat plugin.
  4. Open the plugin and select Load if an existing runtime is present.
  5. Review new or migrated settings.
  6. Open Settings and select Repair generated runtime.
  7. Save, run a complete test and publish only after the test passes.

The included build can migrate the earlier ChatPro root and generated object names when it recognizes managed configuration. Version 1.6.1 also removes the earlier experimental global/cross-server message path and clears its obsolete attributes.

Removing the runtime

Remove runtime, keep configuration removes the managed API, bridge, server and client scripts while retaining the ReplicatedStorage.AstroChat configuration. Use it when temporarily disabling the installed runtime or preparing a clean regeneration.

Do not manually delete only one generated script. A partial runtime can leave configuration present while chat formatting no longer runs consistently.

Security

Use these rules in every production experience:

  • Grant tags through explicit UserIds or the smallest suitable group rank.
  • Keep sensitive or staff-only tags disabled until their rules are verified with test accounts.
  • Treat every LocalScript request as untrusted input.
  • Use EquipTag on the server for game-owned entitlement decisions.
  • Never use client visibility or a local tag picker as proof of permission.
  • Do not expose private configuration or credentials in tag text or system messages.
  • Filter untrusted text according to Roblox requirements before displaying it.
  • Review every script that assigns TextChatService formatting callbacks.
  • Test with an unauthorized account, not only an owner account.

Astro Chat’s runtime validates client tag requests on the server. It does not determine whether a purchase, game pass, subscription or external entitlement is legitimate; your server code must verify those systems before calling EquipTag.

Troubleshooting

The plugin opens, but the game has no Astro Chat runtime

Stop Play or Play Here, return to Edit mode and select Apply. Confirm the generated structure exists in ReplicatedStorage, ServerScriptService and StarterPlayerScripts.

Apply or Save is blocked

Astro Chat does not write generated source during a play test. Stop the test completely and try again in Edit mode.

Chat styles do not appear

Confirm the experience uses TextChatService. Look for another LocalScript assigning TextChatService.OnIncomingMessage, OnChatWindowAdded or OnBubbleAdded; a later assignment can replace Astro Chat’s formatter.

A tag does not auto-equip

Check that the tag is enabled, Auto equip is on, and at least one access rule permits the test player. For group rules, verify the numeric group ID and that minimum rank is between 0 and 255. Test in a server session with a real account.

A client tag request returns true but nothing changes

The client return value only confirms that the request was sent. Check the tag ID, enabled state and server-side UserId or group permission. Review server output for the rejection reason while testing.

Multiple tags appear in the wrong order

Higher priorities appear first. If two tags share a priority, their IDs are ordered alphabetically. Give important staff tags distinct priorities for predictable results.

Bubble overrides do not appear

Enable Bubble chat globally and enable Bubble custom style on the tag. When tags stack, only the strongest tag controls the bubble override. Blank colors and -1 values inherit global settings.

System messages do not appear

Call SendMessage from a server Script, use an enabled message-type ID, and confirm the generated client and Bridge are present. If targeting selected players, pass a Player or an array of Players.

Runtime is marked dirty after a plugin update

This is expected when the generated runtime version no longer matches the plugin. In Edit mode, use Repair generated runtime, review the configuration and Save.

Production checklist

Before publishing:

  1. Back up the place and current Astro Chat configuration.
  2. Confirm the product release is 1.0 and the included plugin/runtime build is 1.6.1.
  3. Confirm the experience uses TextChatService.
  4. Apply or repair the complete generated runtime in Edit mode.
  5. Configure every non-fallback tag’s access rule.
  6. Test authorized and unauthorized accounts.
  7. Test single-tag or stacked-tag ordering as configured.
  8. Test the chat window, input bar, channel tabs and bubble chat on desktop and mobile layouts.
  9. Test each message type through a server Script.
  10. Check for competing TextChatService callbacks.
  11. Save explicitly and verify the runtime is reported as up to date.
  12. Publish to a test universe before production.

After launch, review tag access whenever staff roles, group ranks or entitlements change. Re-run the checklist after plugin updates or changes to other chat-related scripts.