Theme and customization
Every chat space has one published theme: a closed JSON document that decides how the chat page looks, which features it offers, how large attachments may be, and what the interface says. A theme is data only. There is no CSS, HTML or script in it, and the validator rejects any key it does not know.
The lifecycle
| Step | Workflow | Inputs | What happens |
|---|---|---|---|
| Check | buzz.theme.validate | document | Read-only. Returns the normalized document, field errors, and contrast warnings |
| Save a draft | buzz.theme.save-draft | space_uuid, document | Validates and stores the draft. One draft per space; a new save replaces it |
| Publish | buzz.theme.publish | space_uuid | The draft becomes the published theme. Open chats pick it up on their next theme read |
| Roll back | buzz.theme.rollback | space_uuid, version | Republishes an earlier version as a new version. History is never rewritten |
| Read | data.buzz.theme.get | space_uuid or identity_app_uuid, which, version | The published theme by default. Org members may also read the draft or a version. End users read the published theme only |
In the console, the identity app's Chat tab has a live editor: every change is validated as you type, the preview beside it renders the real chat page with demo channels and a demo actor (it never touches your relay), and a pause saves the draft. Publish and history are buttons on the same screen.
buzz.space.enable publishes the default Orkestia theme, so a new space always has one.
A theme document
A document may be partial. Anything you leave out keeps the default.
{
"brand": {"display_name": "Acme Support", "logo_url": "https://example.com/logo.png"},
"appearance": {"color_scheme": "system"},
"tokens": {
"color": {"primary": "#0f766e", "bubble_self": "#0f766e"},
"radius": {"bubble": 12},
"density": "comfortable"
},
"layout": {"mode": "full"},
"features": {"member_channel_create": true, "read_receipts": true, "missed_message_emails": true},
"media": {
"max_bytes": 10485760,
"max_per_message": 4,
"allowed_types": ["image/png", "image/jpeg", "application/pdf"],
"allow_in_private_channels": false
},
"copy": {"locale": "en", "strings": {"composer.placeholder": "Ask the team or mention an assistant"}}
}
start_workflow("buzz.theme.save-draft", {"space_uuid": "<space uuid>", "document": { ... }})
start_workflow("buzz.theme.publish", {"space_uuid": "<space uuid>"})
Sections
| Section | Keys and rules |
|---|---|
brand | display_name (1 to 64 characters), logo_url, favicon_url (https only) |
appearance | color_scheme: light, dark or system |
tokens.color, tokens.color_dark | Hex colors only: primary, primary_contrast, background, surface, surface_alt, text, text_muted, border, danger, success, bubble_self, bubble_self_text, bubble_other, bubble_other_text, actor_accent |
tokens.font | family_body, family_mono (letters, digits, spaces, commas, quotes and hyphens), size_base_px 12 to 20 |
tokens.radius | sm, md, lg, bubble, each 0 to 32 |
tokens.density, tokens.shadow | compact, comfortable, spacious; none, subtle, strong |
layout | mode (full, drawer, widget), channel_list, header, position (left, right), width_px 320 to 720 |
features | Boolean flags, below |
media | Attachment limits, below |
copy | locale (pt-BR, en, es; default pt-BR) and strings, overrides for allowlisted interface keys such as composer.placeholder, login.title or search.placeholder, each up to 200 characters |
Contrast below WCAG AA (4.5 to 1) on the text and bubble pairs is a warning in the editor, not a rejection.
Feature flags
| Flag | Default | Turns on |
|---|---|---|
threads | on | Reply counts and the thread panel |
reactions | on | Reaction chips and the picker |
message_actions | on | Edit and delete of your own messages |
pins | on | Pinned messages: pin and unpin, the pinned bar and list |
mentions | on | The @ picker and plain-text @Name mentions |
direct_messages | on | DMs |
attachments | on | Upload, drag and drop, paste, and rendering files on messages |
search | on | Message search and the member search box |
read_receipts | off | Read markers and unread counts. Markers are private to each person |
typing_indicator | on | "Ana is typing" and publishing your own typing |
markdown | on | Markdown in messages (raw HTML is never rendered) |
actor_badges | on | The badge that marks an actor |
actor_rail | on | Chips above the composer for the actors that answer in this channel |
member_panel | on | The member panel with people and actors |
message_filters | on | Filters such as "mentions of me" and "actors" |
member_channel_create | off | Members may create, rename and invite to channels |
member_channel_join | on | Members may join and leave open channels |
missed_message_emails | on | The missed-message email for this space |
structured_ui | on | Cards from actors with a DGI responder: forms, confirm cards and views. Off, a card shows as its text |
The chat page also shows an actor's progress line, its tool trace and its quick replies. Those are on by default and are not theme flags today.
Three flags are enforced by the platform
member_channel_create, member_channel_join and missed_message_emails are read on the server from the published theme. With the flag off, the matching workflows refuse with buzz_feature_disabled: <flag>, so the page can hide the control and the console shows why.
Attachment limits (media)
| Key | Default | Range |
|---|---|---|
max_bytes | 25 MiB | 16 KiB to 25 MiB |
max_per_message | 4 | 1 to 10 |
allowed_types | application/pdf, image/gif, image/jpeg, image/png, image/webp, text/csv, text/plain | A non-empty subset of that list |
allow_in_private_channels | true | false hides the attach control in private channels and DMs |
A space may only make a limit smaller. The type list is deliberately short and has no wildcards: SVG, archives and executables are not allowed (see Limits).
These limits are what the chat page enforces before it uploads, and what buzz.session.issue reports back to the client. The relay itself only applies its own, much larger, global ceilings.
Beyond the theme
| You want | Do |
|---|---|
| Your own components inside the Orkestia chat | The chat component behind the hosted page exposes slots (header, channel list item, message bubble, empty state, actor badge, actor rail, member panel) that you override in your own bundle. Access to the component package is by request today |
| A completely different chat page | Deploy your own build to the site with the App Host release workflows. The chat entry points on your identity app stay the same |
Ask your AI assistant
Read the published theme of my chat space with data.buzz.theme.get and summarize the brand, colors and which feature flags are off.
Validate this theme with buzz.theme.validate and list every error and contrast warning: <paste JSON>.
Change my chat's primary color to #0f766e and turn on member_channel_create. Save it as a draft, show me the diff, and publish only after I confirm.
Roll my chat theme back to version 3 with buzz.theme.rollback.
For AI agents
| Rule | Detail |
|---|---|
| Validate before saving | buzz.theme.validate is read-only and safe. Unknown keys are errors, not ignored |
| Draft then publish | buzz.theme.save-draft does not change what people see. buzz.theme.publish does. Confirm before publishing |
| Human callers | Save, publish and rollback refuse agents |
| Partial documents | Send only the keys you change. The validator merges them over the defaults |
| Policy flags | Only member_channel_create, member_channel_join and missed_message_emails gate workflows. Do not describe other flags as security controls |
Enable and publish
Turn on a chat space for an identity app with buzz.space.enable, publish the hosted chat page with buzz.space.publish-chat, and let your end users sign in with their app identity
Members and moderation
Invite people into a chat space, set roles and display names, and moderate with suspend, timeout, ban, remove, key rotation and reconcile
