arizuko › concepts › onboarding
onboarding
Onboarding is how a new user gets in and gets a first group. When a new user arrives, the onbod daemon decides whether the user gets in and which group the user lands in. It also binds the identity of the user to one canonical sub. Three paths lead in: invite tokens, the admission queue, and pairing a second chat to an account that already has a world.
A chat needs a pairing and a route before an agent answers
The figure follows a chat that no route claims, from its first message to its route. Two separate facts must become true before an agent answers. First, the identity of the chat is tied to a person, which is the pairing edge. Second, the chat points at a folder, which is the route row. Routing never reads the ACL, and the ACL never creates a route.
inbound message, no route claims the chat
│
▼
┌──────────────────┐ observe rule matched ──► stored as observed,
│ routd routeMiss ├──► no turn, no greeting
│ │ else: held under no folder, cursor advances;
│ │ the operator sees it in /v1/routing/unrouted
│ │ and may claim it with a chat_jid= rule
└────────┬─────────┘
│ ONBOARDING_ENABLED, platform allowed,
│ a Discord guild only on @mention
▼ POST onbod /v1/onboarding
┌──────────────────┐
│ onboarding row │ one per chat JID, carries inbound sender
│ awaiting_message │
└────────┬─────────┘
│ promptUnprompted: mint route_tokens kind=pair,
│ claim prompted_at, send through routd /v1/outbound
▼
chat: greeting + https://<host>/pair/<token>
│ the person opens it in a browser
▼
┌──────────────────┐ proxyd auth=user: no session ──► OAuth,
│ webd /pair/<t> ├──► back to the same URL
│ │ GET confirm page, writes nothing
│ │ POST CSRF, RedeemPairing, one batch:
│ │ acl_membership(child=sender, parent=sub,
│ │ added_by=pairing) + token delete
└────────┬─────────┘
│ success page links /onboard
▼
┌──────────────────┐ no gate approved
│ onbod admitJID ├──►gate matches queued, chat told
│ observePairings │ gates, no match refused, chat told
└────────┬─────────┘
▼ browser /onboard
┌──────────────────┐ queued queue position
│ world choice ├──►holds no folder create a world, if invited
│ │ or gate-approved
│ │ 1 admin world route it, via sole_world
│ │ N admin worlds picker, via picker
│ │ 0 admin worlds "no world" page
└────────┬─────────┘
▼
routes row: room=<id> → world, added_by=<sub>, added_via
the next message reaches the world; history does not move
The figure has a second path at the top. The operator can claim a held chat from the dashboard with no pairing, which gives the route half alone. Onboarding is the self-service path to both halves, and the two paths never check each other. A chat that got a greeting and never paired stays in the held list until someone claims it.
An invite token admits one person to one folder
An operator creates a token with the CLI:
arizuko invite -i <instance> create <target_glob> [--max-uses N] [--expires DURATION]
arizuko invite -i <instance> list [--issued-by SUB]
arizuko invite -i <instance> revoke <ref>
The token is a row in the invites table, which onbod owns. target_glob is a folder, and a trailing slash changes its meaning:
corp/eng- The person who redeems the token becomes an admin of that folder.
corp/eng/- The person can create a new folder under it and names it on the next screen.
/- The person can create a top-level world.
** is not a folder, so onbod refuses it. onbod keeps only the sha256 of the token. --max-uses defaults to 1. --expires takes a Go duration such as 72h, and without it the token never expires.
The recipient opens the link and picks an OAuth provider: GitHub, Google or Discord, as the instance configures. authd then signs the person in under the returned sub. auth explains what a sub is and how linking works.
The greeting is a pairing link
When ONBOARDING_ENABLED=true, a chat that messages the instance and matches no route gets a reply. The reply is one link, valid for ten minutes, that proves the person who opens it can read that chat. An agent creates the same link with issue_pairing_link, so onboarding has no login of its own. The greeting is fixed text plus the link, never an agent turn. A chat that belongs to no folder has no spend cap to limit a turn. The link signs the person in, shows exactly which chat account it attaches to their name, and asks for confirmation. Nothing is written until the person confirms.
The confirmation binds an identity and nothing more. It does not decide where the messages of the chat go. The next screen decides that, as a choice the person makes. See routing.
A new message in the same chat brings a greeting with a new link when two conditions hold: the chat speaks again, and the previous link has expired. A chat that has a route since then, or that stopped speaking, never gets a second greeting.
The admission queue decides whether the instance takes you
Pairing says who you are, and the queue says whether this instance takes you. The binding is immediate. The verdict arrives at the next poll of onbod, every 10 seconds by default.
Rows in the onboarding_gates table of onbod control the queue. You manage them with arizuko gate or /v1/gates. A gate key names an identity provider:
*- Every account.
google:domain=example.com- Google accounts in one domain.
email:domain=example.com- Email accounts in one domain.
github:org=kronael- Every GitHub account. The gate does not check the organisation. To limit GitHub sign-in to one organisation, set
GITHUB_ALLOWED_ORGforauthd.
Each gate can carry a daily limit. The admitFromQueue loop runs about every 60 seconds. It counts the admissions of the current day and moves queued users to approved until the gate reaches its limit.
A queued user sees the position in the queue on /onboard, which refreshes every 30 seconds. An operator approves users manually in dashd or changes the gate configuration.
A refusal goes to the chat. If gates exist and none matches the account, the chat gets a message that says so. The person does not get a browser error on a page that they already closed. With no gates at all, everyone who pairs is approved.
An approval lets you create a workspace only when a gate exists
An approval lets you create your own workspace. You pick a username, and that name becomes a top-level world that you administer. You need no invite and no admin. This is the purpose of the queue: an approval gives a real right.
The right exists only on an instance with at least one gate. If an approval alone allowed a workspace, anyone who messaged the bot on a new instance could take a tenant.
- No gates
- The default. Workspaces come only from invites. People can still pair chats and get approved, but an approval gives no workspace.
- One or more gates
- Self-service signup, limited by the gate. The gate decides who can queue, and its daily limit decides how fast they get in.
A disabled gate does not count. admitJID and mayCreateFirstWorld both read the enabled gates through loadGates, and handleCreateWorld calls mayCreateFirstWorld again when it writes the workspace.
SetupGroup writes every new group
When a user has the right to a workspace, through an invite or through an approval under a gate, onbod calls container.SetupGroup(cfg, folder, seedDir). arizuko create, arizuko group add and dashd call the same function. register_group does not.
The function creates the group directory and logs/. If a seedDir is given, it copies that directory in. It creates .claude/.claude.json with a stable user id derived from the folder and attempts to give the files to uid 1000.
At turn start, runed mounts the selected packages' skills and combined manuals into the guest. It also writes the turn's settings.json.
A second chat pairs to the same account
A person who already has a world and arrives on another platform does not onboard again. The person pairs the new chat to the existing account, and the chat gets everything that account holds. No queue and no new group are involved.
You can undo every binding with one verb, unpair. It reaches every binding, both those an agent created and those a greeting created, because one piece of code writes them all. Access ends on the next action of that account, not after a restart.
Invites and gates have limits
An invite token enforces max_uses at redemption: the UsedCount column increments, and RevokeInvite marks the token dead. onbod refuses an expired token, where expires_at < now, with an error. The daily limit of a gate resets at the UTC day boundary: the queue counts admissions whose queued_at is in the current day.
Related pages cover identity and security
auth explains how an identity becomes a signed header that other daemons trust. skills lists the skills a new group gets. [onbod/README.md] covers the token model, the /v1/invites and /v1/gates endpoints, and who can write which route. security explains why the confirm page is the defence.