Socket.IO API
All application events are on the /chat namespace. The client connects, emits authenticate with a JWT, then interacts via events.
Connection lifecycle
| Event |
Direction |
Description |
connect |
client → server |
Establishes WebSocket connection |
authenticate |
client → server |
JWT handshake. Server responds with authenticated (user) or bot_authenticated (bot), then sends group_list, bot_list, my_permissions, and auto-joins general. |
disconnect |
client → server |
Server cleans up session. If bot, removes from registry and broadcasts bot_unregistered. |
Chat events
| Event |
Direction |
Auth |
Description |
message |
client → server |
yes |
Send a message. Server broadcasts to group, persists to JSONL, routes @bot mentions, evaluates patterns, evaluates label subscriptions. |
typing |
client → server |
yes |
Typing indicator (rate-limited to 1/sec). |
remove_message |
client → server |
yes |
Mark a message as removed (strikethrough). |
fetch_messages |
client → server |
yes |
Fetch specific messages by ID. Server responds with fetch_messages_result. |
bot_response |
client → server |
bot only |
(deprecated — use message with bot auth) Bot sends a response. |
link_click |
client → server |
yes |
Track that a user clicked a link in a message. |
RPC (via message event)
RPC uses the existing message event with type set to rpc_request or rpc_response. There are no separate RPC Socket.IO events. The server routes RPC messages exclusively via label subscriptions — no room broadcast, no pattern evaluation, no @bot routing.
Sending an RPC request
{
"content": "add 2 3",
"type": "rpc_request",
"group_id": "general",
"labels": [["bot-math-svc", "service:math", "1.0.0", {"request_id": "a1b2c3"}]]
}
Sending an RPC response
{
"content": "5",
"type": "rpc_response",
"group_id": "general",
"labels": [["bot-math-svc", "service:math-response", "1.0.0", {"request_id": "a1b2c3"}]]
}
Behaviour
- No room broadcast. RPC messages skip
emit_frame(EventName.MESSAGE, room=...). They reach subscribers exclusively via label routing.
- No auto-room-label. The
meadows:room:<group_id> label is not applied to RPC messages.
- Persisted. Both request and response are stored in the group's JSONL file.
- Correlation. The
request_id in label metadata ties response to request. The caller's call_rpc() future resolves when a matching RPC_RESPONSE arrives.
- Deliver modes. Service bots subscribe with
deliver="message_only" to receive the full message content. The server loads the message from persistence and emits it as a MESSAGE event to the subscriber.
Message types
Every message event carries a type field that determines how the server and clients handle it:
| Type |
Value |
Description |
user |
user |
Human-authored message |
bot |
bot |
Bot-authored message |
reaction |
reaction |
Reaction to another message (carries emoji + target_message_id) |
form_submission |
form_submission |
Interactive form response (reserved, not yet implemented) |
webhook |
webhook |
Message received via HTTP webhook |
system |
system |
System-generated message |
rpc_request |
rpc_request |
RPC request to a service bot (routes via labels only) |
rpc_response |
rpc_response |
RPC response from a service bot (routes via labels only) |
GUI/TUI clients typically filter on user and bot types. RPC and system messages are invisible to chat rendering unless the client explicitly subscribes to them.
Group events
| Event |
Direction |
Auth |
Description |
create_group |
client → server |
yes |
Create a group (^[a-z0-9_-]{1,32}$). Bots auto-join. |
list_groups |
client → server |
yes |
Returns group_list. |
join_group |
client → server |
yes |
Join a group. Server sends joined_group with history. |
leave_group |
client → server |
yes |
Leave a group. |
delete_group |
client → server |
yes |
Delete a group (not general). Archives JSONL. |
Reaction events
| Event |
Direction |
Auth |
Description |
add_reaction |
client → server |
yes |
Toggle a reaction (emoji) on a message. If the same emoji exists from the same user, it is removed (toggle). |
remove_reaction |
client → server |
yes |
Explicitly remove a reaction. |
Bot events
| Event |
Direction |
Auth |
Description |
register_bot |
client → server |
bot only |
Register bot metadata. Server broadcasts bot_list. |
bot_list_bots |
client → server |
yes |
Returns bot_list. |
register_pattern |
client → server |
bot only |
Register a regex pattern (max 50/scope, 512 chars). |
unregister_pattern |
client → server |
bot only |
Remove a pattern by name. |
register_label_subscription |
client → server |
bot only |
Register a JSON Logic predicate against label data. |
unregister_label_subscription |
client → server |
bot only |
Remove a label subscription by name. |
label_assigned |
client → server |
bot only |
Bot emits a label on a message (dedup + cascade). |
Ntfy events
| Event |
Direction |
Auth |
Description |
get_ntfy_prefs |
client → server |
yes |
Request the user's ntfy notification preferences. |
save_ntfy_prefs |
client → server |
yes |
Save ntfy notification preferences. |
JWT invite events
| Event |
Direction |
Auth |
Description |
request_user_jwt |
client → server |
user-invite |
Mint a JWT for a new user. |
request_bot_jwt |
client → server |
bot-invite |
Mint a JWT for a new bot. |
Server-to-client events
| Event |
Trigger |
Payload |
authenticated |
User auth success |
— |
bot_authenticated |
Bot auth success |
— |
auth_error |
Auth failure |
{error} |
my_permissions |
Sent after auth |
{permissions, available_permissions} |
message |
Message broadcast |
Full message envelope |
message_removed |
Message marked as removed |
{message_id, group_id} |
user_typing |
Typing indicator |
{user_id, group_id} |
joined_group |
Group joined (includes history) |
{group_id, messages, members} |
left_group |
Group left |
{group_id} |
user_joined |
Someone joined a group (room broadcast, skip_sid) |
{user_id, group_id} |
user_left |
Someone left a group (room broadcast, skip_sid) |
{user_id, group_id} |
group_list |
Full list of groups |
{groups} |
group_created |
New group created |
Group state |
group_deleted |
Group deleted |
{group_id} |
members_updated |
Group membership changed |
{group_id, members} |
bot_list |
List of registered bots |
{bots} |
bot_registered |
Bot successfully registered |
{bot_name} |
bot_unregistered |
Bot disconnected |
{bot_name} |
bot_command |
@bot mention routed to bot |
Full message envelope |
bot_not_found |
@mention targets nothing |
{name} |
rate_limited |
Bot exceeded 30 msg/min |
{bot_name} |
pattern_registered |
Pattern registration ack |
{name} |
pattern_unregistered |
Pattern removal ack |
{name} |
pattern_matched |
Regex pattern matched a message |
{name, message_id, group_id} |
label_subscription_registered |
Label subscription registered |
{name} |
label_subscription_unregistered |
Label subscription removed |
{name} |
label_assigned |
Label matched a subscription |
{labels, target_msg_id, applied_by, subscription_name} |
reaction_added |
Reaction added to a message |
{emoji, target_message_id, user_id, group_id} |
reaction_toggled |
Same emoji toggled off |
{emoji, target_message_id, user_id, group_id} |
reaction_removed |
Reaction explicitly removed |
{emoji, target_message_id, user_id, group_id} |
fetch_messages_result |
Response to fetch_messages |
{request_id, messages} |
ntfy_prefs |
Ntfy notification preferences |
Preferences data |
ntfy_prefs_saved |
Ntfy preferences save ack |
{success} |
user_jwt_generated |
JWT minted for a user |
{token, username} |
bot_jwt_generated |
JWT minted for a bot |
{token, bot_name} |
error |
Generic error |
{error} |
Rate limiting
| Limit |
Value |
Scope |
| Max messages per window |
30 |
per bot (sliding 60s window) |
| Cooldown on violation |
60 seconds |
per bot |
| Max patterns per scope |
50 |
per scope-key |
| Max pattern length |
512 chars |
— |