Onboarding
How a new user comes to understand and use SkillNet. This document sets the model (principles + flows), the architecture that supports it, and a phased plan. It is the measuring stick: any future onboarding screen is checked against this.
Related: degraded-mode-ux.md (states without a key), personalization.md (learner profile), audience-modes.md (organization / individual).
1. Guiding principle
It should set free, not tie down. SkillNet sells freedom and adaptation; onboarding has to feel the same. The filter for every decision: does this stop the user or free them?
From there, six pillars:
- The platform is never empty. It starts with a demo course already set up (seed). No blank dashboard.
- Guidance yes, but the door always open. A “step 1 of N” by default (many people want it and it is the path that converts the most), closable, reopenable, and remembers progress. Closing it does not punish with emptiness: you land on the full platform.
- Value is discovered by touching it, instantly. The first win is pre-generated content (fast, no key, no waiting). Never a live generation as a welcome (slow = defeat).
- The differentiator is the generative UI. These are not templates with swapped text: each lesson’s interface is composed for that content and that person. It is made evident with an ignorable nudge (“look at the same lesson for another person → it is different”).
- Personalization is offered or inferred, never imposed. Capturing “how you learn” is optional; what is not asked is learned from behavior.
- The key (API key) unlocks, it does not charge. Without a key you explore the demo; with a key you do it with your own content. You reach the key once value has already been seen and is wanted.
2. The architectural idea: capability-driven onboarding
The elegant point: onboarding, no-key degradation, and smart defaults are the same problem —
“based on what is available and who you are, show one thing or another”. They are solved with
a single source of truth instead of if (hasKey) scattered across the code.
┌──────────────────────┐
settings/env │ Capabilities │ ai, generation, tutor, tts, images
keys │ (what AI is there?) │ → GET /capabilities (or /setup/status)
└──────────┬───────────┘
│ useCapabilities()
┌───────────────────┼────────────────────┐
▼ ▼ ▼
Onboarding steps UI elements Degraded banners
(filtered) (<Gated requires>) (degraded-mode-ux)
2.1 Capabilities — the source of truth
An object derived from the presence (and validity) of keys, exposed by the backend:
interface Capabilities {
ai: boolean // is there a usable LLM (nothing works without this)
generation: boolean // generate courses/lessons
tutor: boolean // tutor chat
tts: boolean // voice (mascot / podcast) — degrades to offline, see degraded-mode
images: boolean // infographics
}
- Backend: computed in one place (presence of
LLM_API_KEY,TTS_API_KEY,OPENROUTER_API_KEY). TodayGET /setup/statusalready exists and is the natural place (we already addedonboarding_enabledto it). - Frontend: a
useCapabilities()hook. Any AI piece consults it; nobody hardcodes “there is a key”.
2.2 Declarative gating (static element vs. needs-a-key)
Instead of scattering conditionals, a single component/hook:
<Gated requires="tutor">
<TutorPromptChip /> {/* only renders if capabilities.tutor */}
</Gated>
Rule: the AI element turns on only if its capability is present. Without a key it is not shown (not a dead end/error). Examples:
| Always (static / pre-baked) | requires (turns on with a key) |
|---|---|
| Demo course (seed), viewing it | Free-form “other” option in questions (generation/ai) |
| Contrast nudge (two people, seed) | Generated preview / “create your first course” (generation) |
| Fixed presets for the questions | Chat with the tutor (tutor) |
| Tour (joyride) | Personalization that regenerates on the fly (generation) |
Design key: the no-key side must feel complete (the pre-baked content carries the value). The AI part is additive, not “the good stuff was hidden”.
2.3 Onboarding as data, not as a hardcoded flow
The tour is a declarative list of steps; joyride just consumes it. Adding/removing/reordering = editing data.
interface OnboardingStep {
id: string
role: 'employee' | 'admin'
target: string // selector of the element to highlight
title: string; body: string
requires?: keyof Capabilities // step is skipped if the capability is missing
order: number
}
Runtime filter: steps.filter(role).filter(cap => !s.requires || capabilities[s.requires]).
A step that talks about the tutor disappears without a key, with no ad-hoc branches.
2.4 Onboarding state — per user, persisted, reopenable
interface OnboardingState { completed: boolean; dismissedAt?: string; lastStepId?: string }
- Orthogonal to routing: it never forces a redirect (the
ONBOARDING_ENABLEDflag already exists). - MVP:
localStorage. Later phase: per-user field (cross-device). - Reopenable from a persistent “?”;
lastStepIdgives the “remember where I was” behavior.
2.5 Smart defaults — a resolver from the org archetype
The admin gives a minimal hint (education / enterprise) → a resolver maps archetype → defaults.
type Archetype = 'education' | 'enterprise' | ...
function resolveDefaults(a: Archetype): OrgSettings // e.g. enterprise ⇒ mascot off
- One single resolver, not scattered conditionals.
- Values are defaults, always overridable (user/org).
- Every automatic decision is shown and reversible (“We turned off the mascot because this is an enterprise environment — change it here”). Never silent magic.
2.6 Shared demo = first-class asset
The test course is a seed present in every deployment (is_demo), an example for both roles:
the admin sees “this is what gets generated”, the employee does it. Visible without a key
(already generated). With a key, the same course becomes conversable (tutor) and
adaptable/regenerable — the key does not change what you see, it changes how much you can do
with it.
3. Flows (one pattern, two fillings)
Branches only on the role; each branch is minimal. Common structure: framing → one capture that matters → first win.
3.1 Employee (account created by the admin; never touches the key)
- Login → non-empty home (enrolled in the demo / their courses) + a gentle “start here”.
- Default, closable tour: “open your first lesson” → tap a lesson that is already pre-generated and personalized (instant, prewarm). Aha = rich and adapts.
- Optional micro-capture: “how do you like things explained? (optional)” → presets + “other” (the “other” only with a key). Skippable → default profile + behavioral inference.
- Contrast nudge (ignorable): “here is how we explain it to another person” → same material, different UI → captures personalization.
- From there, just learn.
3.2 Admin / owner (self-hosts and sets up the organization)
- Minimal setup (already exists) + one archetype question (education/enterprise → defaults).
- Non-empty dashboard with the demo course + closable tour.
- Discover the generative UI by touching the demo (same two-person contrast).
- “Make it yours” moment: “Create your first course”. If there is no key → “connect your AI” (why + link to the provider + paste + live validation). It is not a toll: you arrive here already convinced. Meanwhile, everything is explorable; AI actions show “connect a key” in their place.
- Real generation = in the background + notification. Never a loading bar as a welcome.
- Invite employees.
4. Phased plan (cheap → expensive; nothing complex up front)
Tour engine: react-joyride (spotlight, “step 1 of N”, skip/close, step control). We provide the data and state (§2.3, §2.4), not the engine.
Status (2026-08-21): Phase 0 ✅, Phase 2 infra (
Capabilities/<Gated>) ✅, and the degraded-mode banner ✅ are done and onmain; the admin tour (Phase 1) too. What remains, with a decision/design step in between: the 3-way archetype +resolveDefaults, the contrast nudge, the API key from the UI, background generation, and Phase 3.
Phase 0 — MVP ✅ DONE
- Non-empty home with the demo (seed). ✅
- Joyride tour for employees (home → first pre-generated lesson), closable,
localStorage. ✅ - Non-blocking profile (
ONBOARDING_ENABLED+ existing capture). ✅ - Only static elements. ✅
Phase 1 — make the differentiator visible ◑ PARTIAL (remainder: pending design)
- ○ Contrast nudge (two people, seed data) — pending.
- ✅ Admin tour — done: role-aware
ProductTour(same joyride, admin steps). - ○ 1 smart default: enterprise/education archetype →
resolveDefaults(enterprise turns off the mascot) + the line that explains it — pending (needs archetype backend).
Phase 2 — the self-hosted unlock (the AI layer) ◑ PARTIAL
- ✅
Capabilities+useCapabilities()+<Gated>(§2.1–2.2) — infra done (backend derives it from the keys and exposes it at/setup/status; the frontend consumes it). - ✅ Degraded-mode banners (
CapabilityHealthBannerin admin) — done. - ○ API key from the UI (paste + validate) — pending (storage/security decision).
- ○ Turn on the
requires: free-form “other”, generated preview, tutor chat. - ○ Background generation + notification.
Phase 3 — the complex part, last ○ PENDING
- Infer the profile from behavior (what they open/re-read/skip). Separable signals module.
- Cross-device onboarding state, finer archetypes.
5. What already exists vs. what’s missing
| Already exists (✅) | Missing (○) |
|---|---|
| Setup wizard + welcome (logo/mascot/degraded), login, home | Archetype question + resolveDefaults |
| Demo course (seed) + pre-generated lessons (prewarm) | Contrast nudge (two people) |
| Role-aware joyride tour (employee + admin), reopenable, per-role state | API key in the UI (paste+validate) — today in .env |
Profile capture + ONBOARDING_ENABLED flag |
Background generation + requires (“other”/preview/tutor) |
Capabilities + useCapabilities + <Gated> + degraded-mode banner |
Phase 3 (infer profile, cross-device) |
6. Summary in one sentence
Full platform from the start · guidance by default but always closable and reopenable · value by touching, not by telling · profile offered or inferred, never imposed · self-chosen and reversible defaults · the key unlocks, it does not charge — all under a clean engine: the app declares what it needs (capability × role) and a resolver decides what to show and how to degrade.