onbod is an optional daemon that handles new user registration. When ONBOARDING_ENABLED=true, it is included in the generated docker-compose and registers itself as a channel adapter. The gateway never imports onbod code. The only integration points are the channel protocol and two SQLite tables.
# /srv/data/arizuko_myinstance/.env
ONBOARDING_ENABLED=true
ONBOARDING_PROTOTYPE=main/prototype # group folder to copy for new users
ONBOARDING_NOTIFY_JID=telegram:@operator_username
Re-run arizuko generate myinstance to include onbod in the compose file. No other configuration is required.
| State | What happens |
|---|---|
pending | New user sent a message. onbod stores their JID and intro message. Notifies operator. |
waiting | Auto-reply sent to user: "your request is under review". Waiting for operator approval. |
approved | Operator approved via dashboard. onbod copies prototype group, writes registered_groups row. |
rejected | Operator rejected. onbod sends rejection message. No group created. |
active | User's next message routes normally through the gateway to their group. |
onbod registers as a channel with the receive_only capability. This means the channel registry knows about it, but the gateway's outbound routing never picks it for delivery. Inbound messages arrive because the adapter's registration includes user JID prefixes it has seen:
// onbod registration
{
"name": "onboarding",
"url": "http://onbod:9010",
"prefixes": [], // no prefix claims — gateway routes normally after approval
"capabilities": ["receive_only"]
}
The channel adapter receiving the initial message (e.g., Telegram) forwards all unrecognized JIDs to onbod's HTTP endpoint directly — before posting to the gateway. If onbod returns a 200 with {"handled": true}, the adapter does not forward to the gateway. If onbod returns {"handled": false}, the message proceeds normally.
When a new user messages the bot:
registered_groups — it is notonbod's intercept endpointonbod stores the pending user, sends auto-reply, notifies operatordashd's onboarding viewonbod copies the prototype group folder to groups/main/users/<jid>/onbod inserts a row into registered_groups: maps the user's JID to the new folderregistered_groups, routes to gatewayThe prototype is a regular group folder. It contains the initial CLAUDE.md, skill files, and any pre-seeded facts. Every approved user gets a copy. Changes to the prototype do not affect existing users — their groups are independent copies from the point of onboarding.
groups/
main/
prototype/ # template group
CLAUDE.md # initial instructions
facts.md # pre-seeded facts
skills/ # symlinks or copies of skill files
users/
telegram:12345/ # copy created on approval
telegram:67890/
onbod never calls the gateway API. It owns the registered_groups insert. The gateway discovers the new group on the next registered_groups refresh (default: every 30 seconds) — or immediately if the refresh is triggered by a table change notification.
This is the outside-in design: onbod speaks the channel protocol (register, intercept endpoint) and the schema contract (write to registered_groups). It does not need internal gateway access. It does not import gateway packages. It is a daemon that writes to a shared database and implements two HTTP endpoints. The gateway and onbod are coordinated by data, not by code.