Onboarding
The Onboarding model — fields, relations, and API.
Transport: WebSocket — Socket.IO /ai-v2/onboarding (no REST) · Flag: AI_V2_ONBOARDING_ENABLED
PRD
Business problem
New users land in an empty workspace and don't know what to do (blank-page friction). This agent captures context — industry, role, team size, tools — once, verifies the owner's WhatsApp number, and drives fast activation: signup to a usable, personalized CRM in a couple of minutes.
User stories
- As a new workspace owner, I want to be guided through a few quick questions and pick how my workspace is built, so that I go from an empty screen to a ready-to-use, personalized CRM without manual setup.
Success criteria
- Connecting the socket immediately drives the flow: greeting + first question, or the terminal outcome if already done, or a rejection if the flag is off / the caller isn't the owner (there is no pre-socket status call).
- WhatsApp verification is enforced (
phone+phone_otprequired); the global skip clamps at the phone step until the number is verified or a failed send releases it. - Answering
setup_choiceis terminal and idempotent — oneonboarding_setuprow per workspace, no duplicate handoff;is_new_useris cleared so onboarding never re-shows.
How it works
High-level overview — enough to understand the feature without reading the code.
Summary
It is a server-driven form: the backend owns every question and its render type;
the frontend is a thin renderer that draws a widget per step.type and never hard-codes
the list or order. The engine walks an ordered step registry by a cursor held in a
transient Redis draft (answers accumulate; nothing is written to the DB until the
terminal step). AI is used narrowly — normalizing free-text / "Other" answers and
writing the confirmation summary — and the terminal setup_choice records the outcome
and hands off to the Workspace Builder.
Flow
Steps
- Client connects the Socket.IO socket with the owner's admin Bearer token in the
handshake (
/ai-v2/onboarding). - Server runs connect gates: feature flag, valid token, super-admin owner only;
failure →
onboarding:error+ disconnect. - Server emits
onboarding:ready { agent: 'onboarding' }, then the current view — anonboarding:question(greeting on Q1) oronboarding:outcomeif finished. - Client renders by
step.type; the user answers →onboarding:answer { step_key, value }; server validates, records, advances the cursor, returns the next question.onboarding:backrevises the previous answer. - Profile questions in order:
name → industry → role → team_size → tool_integrations. - Phone verify (required): submitting
phonesends a WhatsApp OTP and returns thephone_otpstep. Skip rule:phone/phone_otpare not freely skippable — only a genuine send failure setsphoneSkipUnlocked; an invalid-number 400 is not skippable. phone_otp: submit the code; it must verify to advance.onboarding:resendresends. The phone/OTP are never stored in the draft — the verified number lives inadmin_whatsapp.invite(optional): capture teammate emails; they ride along in the handoff envelope — onboarding provisions none itself.setup_choice(terminal): records the choice, writes theonboarding_setuprow, runs the builder handoff, clears the draft, flipsis_new_useroff, emitsonboarding:outcome.
Key components
- Gateway —
realtime/onboarding.gateway.ts(namespace/ai-v2/onboarding, websocket only) +realtime/onboarding-ws-auth.service.ts(super-admin owner gate). - Form engine —
services/onboarding-form.service.ts(cursor walk / skip / back / jump); orchestration inservices/onboarding-answer.service.ts(phone + OTP side-effects, terminal complete + handoff, idempotency). - AI gates —
services/onboarding-gates.service.ts; profile inservices/onboarding-profile.builder.ts. - Persistence —
services/onboarding-draft.store.ts(Redis draft) andentities/onboarding-setup.entity.ts+repositories/onboarding-setup.repository.ts(RLS-pinned, idempotent). - Handoff —
services/builder-handoff.service.ts(to the Workspace Builder).
FRD
Per-step functional requirements — the ordered step registry (the agent analogue of a
model's fields), from steps/onboarding-steps.registry.ts:
| # | Step key | Type | Required | Skippable | Notes |
|---|---|---|---|---|---|
| 1 | name | text | yes | no | "What should I call you?" |
| 2 | industry | radio | no | yes | allows "Other" |
| 3 | role | radio | no | yes | allows "Other" |
| 4 | team_size | radio | no | yes | size buckets |
| 5 | tool_integrations | checkbox | no | yes | tools used today; allows "Other" |
| 6 | phone | phone | yes | no (verify:true) | sends WhatsApp OTP; failed send releases skip |
| 7 | phone_otp | text | yes | no | enter the code; forward-only (back → phone) |
| 8 | invite | invite | no | yes | invite teammates by email (max 10) |
| 9 | setup_choice | radio | yes | no | terminal: template / ai_build / zero |
HLD
Frontend
A thin, server-driven renderer: it draws one widget per step.type and reacts to socket
events — it never hard-codes the question list or order.
API
WebSocket only (Socket.IO /ai-v2/onboarding, transports: ['websocket']); auth via
handshake Bearer token. There is no REST controller (the controllers//guards/
folders are empty scaffolding). The surface is summarized under API; the full
wire contract is the co-located API.md, published under Guides.
Database changes
Nothing is written until the terminal step. In progress → a transient Redis draft.
Terminal → one onboarding_setup row per workspace, plus flipping is_new_user off; the
verified WhatsApp number is written to admin_whatsapp (never to the draft).
LLD
Tables
Redis draft (OnboardingDraft):
| Field | Type | Notes |
|---|---|---|
cursor | number | index of the next unanswered step |
answers | Record<stepKey, StoredAnswer> | discriminated by kind (text/radio/checkbox/invite); no phone variant |
phoneSkipUnlocked? | boolean | set only on a genuine WhatsApp send failure |
Key onboarding:draft:{tenantDatabase}:{workspaceId}:{adminId} (tenant scope baked in
since RLS doesn't cover Redis), TTL 24h, cleared at the terminal write. Durable row
onboarding_setup: workspace_id, admin_id, status, path, profile_snapshot
(jsonb), handoff_at, timestamps — one row per workspace (uniqueness = idempotency).
Services
OnboardingFormService (cursor engine), OnboardingAnswerService (orchestration +
side-effects), onboarding-gates.service.ts (AI normalize + summary),
onboarding-outcome.mapper.ts (status → outcome), builder-handoff.service.ts.
Validators
answer-validator.ts validates each answer by step.type; the phone step enforces
WhatsApp OTP verification. Connect-time gates (no per-message guard):
AI_V2_ONBOARDING_ENABLED, a valid handshake token, and super-admin workspace owner
(SEC-30) — rejection codes FORBIDDEN / AUTH_MISSING / AUTH_INVALID /
FEATURE_DISABLED.
API
WebSocket only — no REST routes; it does not appear in the API Explorer. The
authoritative wire contract (handshake, every event payload, error/rejection codes) is
the co-located API.md, published under Guides. Event summary:
- Client → server:
onboarding:answer { step_key, value },onboarding:back,onboarding:skip,onboarding:resend,onboarding:confirm,onboarding:refresh. - Server → client:
onboarding:ready,onboarding:question,onboarding:outcome,onboarding:summary,onboarding:error { code, message }.
ERD
Status → outcome mapping and the terminal handoff (the agent analogue of an entity-relationship diagram):
OnboardingNextType=builder | pending | empty_workspace.- Statuses:
pending, in_progress, completed, handed_off, handed_off_pending, skipped; terminal set =completed, handed_off, handed_off_pending, skipped. markHandoffBuilt()later flipshanded_off*→completedonce the builder has actually built (sonextbecomesempty_workspace).