arizuko › concepts › onboarding
onboarding
Routing, engagement, and topics all assume a group already exists. Onboarding is how one comes to be. When a new user arrives, the onbod daemon decides whether they get in, which group they land in, and how their identity binds to one canonical sub. The principle to hold: a group is never created by hand — it is always the output of an admission. Three entry paths feed it: invite tokens, the admission queue, and pairing a second chat to an account that already has a world.
invite tokens
An operator mints a token with the CLI:
arizuko invite <instance> create <target_glob> [--max-uses N] [--expires DURATION]
arizuko invite <instance> list [--issued-by SUB]
arizuko invite <instance> revoke <token>
The token is a row in the invites table (owned by onbod). target_glob picks which group the redeemer lands in — for example corp/eng/* or a specific folder. --max-uses defaults to 1; --expires is a Go duration like 72h. Omit it and the token never expires.
The recipient opens the link, picks an OAuth provider (GitHub, Google, Discord, Telegram widget — whichever the instance has configured), and onbod writes an auth_users row keyed by the returned sub. See auth for what a sub is and how linking works.
the greeting is a pairing link
When ONBOARDING_ENABLED=1, a chat that messages the instance and matches no route gets a reply: one link, good for ten minutes, that proves whoever clicks it can read that chat. It is the same link an agent mints with issue_pairing_link — onboarding does not have a login of its own. Clicking it signs the person in, shows them exactly which chat account they are about to attach to their name, and asks. Nothing is written until they say yes.
What that click does is bind an identity, and only that. It does not decide where the chat’s messages go — that is the next screen, and it is a choice, not a guess. See routing.
A dead link is not a dead end. Ten minutes is short on purpose: the protection is the confirm page, not the clock. Send another message in the same chat and the next greeting brings a fresh link. Two things stop that becoming noise — the chat has to actually speak again, and the previous link has to have expired first. A chat that has since been routed, or has gone quiet, is never greeted twice.
admission queue
Pairing says who you are. The queue says whether this instance takes you. They are separate on purpose, and they happen at different moments: the binding is instant, the verdict lands within about ten seconds.
The queue is gated by ONBOARDING_GATES — a list of rules like github-org=kronael, google-domain=example.com, or a catch-all. Each gate carries an optional daily limit. The admitFromQueue loop runs roughly every 60 seconds, counts admissions in the current day, and promotes queued users to approved until the gate’s limit is hit.
Queued users see their position on /onboard, which auto-refreshes every 30 seconds. Operators approve manually through dashd or by adjusting gate config.
A refusal is spoken, not swallowed. If gates are configured and none of them matches the account, the chat gets told so, in the chat — not a browser error on a page the person has already closed. Configure no gates at all and everyone who pairs is approved.
what an approval buys
Being approved lets you make your own workspace. You pick a username, and that name becomes a top-level world you administer — no invite, no admin to ask. That is the point of the queue: without it, an approval was a status that entitled you to nothing and the next screen still told you to go find an invite.
It only works on an instance that has at least one gate configured. That reads like a technicality and it is the opposite — it is the switch between two very different instances. Remember the line above: configure no gates and everyone who pairs is approved. So if an approval alone opened workspace creation, then on a fresh instance anyone who messaged the bot could help themselves to a tenant. Requiring a gate means nothing changes until you say it changes.
So the two setups are:
- No gates — the default, and what every existing instance already does. Workspaces come from invites only. People can still pair chats and be approved; approval just does not buy a workspace.
- One or more gates — self-serve signup, fenced by the gate. The gate decides who may queue (your GitHub org, your Google domain, or a catch-all) and its daily limit decides how fast they get in.
A gate you have switched off does not count. It has to be enabled — the same rule that decides who gets queued decides whether approvals are worth anything, so the two can never disagree.
The check that matters happens when the workspace is written, not when the page is drawn. Hiding a button is not a lock, so guessing the address gets you the same refusal as never seeing the button.
SetupGroup writes a group
Once a user is entitled to a workspace (via invite, or via an approval under a gate), onbod calls container.SetupGroup(cfg, folder, seedDir). That function is the only correct way to create a group — never mkdir a folder under groups/ by hand.
It creates the group directory plus logs/, copies seedDir in when one is given (the CLI passes a product template; onboarding passes none), then runs seedGroupDir:
.claude/skills/— every directory underant/skills/with a valid name is copied in. See skills..claude/CLAUDE.md— copied fromant/CLAUDE.md..claude/.claude.json— seeded with a per-folder user id (sha256 ofarizuko:<folder>).- Ownership chowned to uid 1000 so the in-container
nodeuser can write.
Skipping SetupGroup produces a folder the agent can’t use: no skills, no settings, wrong ownership. Everything onbod does for a brand-new user funnels through this one call.
a second chat, same person
Someone who already has a world and turns up on another platform — same human, new chat — does not re-onboard. They pair the new chat to the account they already have, and it inherits everything that account holds. No queue, no new group.
Every one of these bindings is undoable. One verb removes it, unpair, and it reaches all of them — the ones an agent minted and the ones a greeting did — because a single piece of code writes them all. Access disappears on the next thing that account tries to do, not on some later restart.
limits and lifetime
Invite tokens enforce max_uses at redemption: the UsedCount column increments, and RevokeInvite marks them dead. Expired tokens (expires_at < now) are refused with an error. Gate daily limits reset on UTC day boundaries — the queue counts admissions whose queued_at falls in the current day.
go deeper
How identity becomes a signed header that downstream daemons trust: auth. What skills land in a freshly-seeded group: skills. Full token model, planned /v1/invites REST surface, and the cross-daemon flow: onbod/README.md. Specs: specs/5/18-onboarding-model.md (where a chat lands), specs/5/31-identity-pairing.md (who a chat is), specs/5/1-auth-standalone.md.