Onboarding flow

The gateway does not know onboarding exists. onbod is a channel adapter that writes directly to the registered_groups table.

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.

Enable it

# /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.

The state machine

StateWhat happens
pendingNew user sent a message. onbod stores their JID and intro message. Notifies operator.
waitingAuto-reply sent to user: "your request is under review". Waiting for operator approval.
approvedOperator approved via dashboard. onbod copies prototype group, writes registered_groups row.
rejectedOperator rejected. onbod sends rejection message. No group created.
activeUser's next message routes normally through the gateway to their group.

How onbod intercepts messages

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.

The approval flow

When a new user messages the bot:

  1. Telegram adapter checks if the chat JID is in registered_groups — it is not
  2. Adapter calls onbod's intercept endpoint
  3. onbod stores the pending user, sends auto-reply, notifies operator
  4. Operator sees the request in dashd's onboarding view
  5. Operator clicks Approve
  6. onbod copies the prototype group folder to groups/main/users/<jid>/
  7. onbod inserts a row into registered_groups: maps the user's JID to the new folder
  8. User sends next message — Telegram adapter finds it in registered_groups, routes to gateway
  9. Gateway spawns container for the new group — normal flow

The prototype group

The 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/

How it fits the system

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.