Onboarding Platform Showcase PRD
Status
- Draft
Date
- May 20, 2026
Purpose
Redesign the new-user onboarding flow so creators see (a) what their public-facing org landing page can look like and pick a theme, and (b) land in a workspace that is already pre-populated with goal-relevant demo courses instead of an empty /courses page. The goal is to move first impression from "blank slate" to "this platform is alive and ready for me."
Problem Statement
Today's onboarding is a sterile 2-step form:
- Org name + slug + full name
- Goal + source + locale
Then the user is dropped onto /org/{slug}/courses?create=true — a page with zero courses, zero branding decisions made, and no visible evidence that ClassroomIO is anything more than a CRUD form for a course list. Two problems compound:
- The landing page feature is invisible. We ship 8 production-quality landing page themes (
minimal,bold,classic,saas,tech,studio,corporate,terminal) with full preview PNGs, but new users never see them — they have to discover Settings → Landing Page on their own. - The workspace looks empty. New users have no reference for what a "real" ClassroomIO course looks like (sections, lessons, video embeds, certificates). They have to imagine the product from a blank canvas.
Result: low activation, high time-to-first-meaningful-action, and the impression that the platform is thinner than it actually is.
ICP (Primary)
- SaaS founders launching a product academy.
- Independent course creators selling self-paced cohorts.
- Operations leaders running employee/compliance training.
- Customer-success teams running customer enablement.
Target Jobs-To-Be-Done
- Quickly evaluate whether ClassroomIO can produce a school site that looks like the creator's brand.
- See what a "good" course structure looks like on the platform without having to build one to find out.
- Get to a publishable-feeling state in a single session.
Confirmed Product Decisions
- Theme picker is a real step (not a passive preview gallery). The user's selection is persisted to
organization.landingPage.theme. - Auto-seed goal-relevant demo courses — no extra picker step. The existing
goalanswer in metadata drives which course set lands in the org. - Step order:
org-setup → theme-picker (+ course preview) → user-metadata → seed-and-redirect. - Course source: a new curated demo set authored in code (not the existing
seedCourses()templates inpackages/db/src/utils/seed/course.ts, which were built for E2E/dev seeding).
Non-Goals (v1)
- AI-generated courses from a URL — that is a separate initiative tracked in
prd/onboarding-ai-bootstrap/. - Custom theme editing inside onboarding (colors, hero text, logo upload). The full landing-page editor stays in
/settings/landingpage. - Letting the user pick which demo courses to seed. Goal-mapping is opinionated.
- Multi-language demo course content. v1 demo courses ship in English only; the user's
localechoice affects UI chrome but not seeded course text. - Surfacing demo courses on the public landing page automatically — they seed as unpublished/draft so the org owner reviews first.
Data Sources Checked
apps/dashboard/src/routes/(auth)/onboarding/+page.svelteapps/dashboard/src/lib/features/onboarding/api/onboarding.svelte.tsapps/dashboard/src/lib/features/onboarding/utils/constants.tsapps/dashboard/src/lib/features/onboarding/utils/types.tsapps/dashboard/src/lib/features/onboarding/components/verify-email-modal.svelteapps/dashboard/src/lib/features/onboarding/components/welcome-modal.svelteapps/dashboard/src/lib/features/org/utils/landing-page.tsapps/dashboard/src/lib/utils/types/org.tsapps/dashboard/src/lib/features/settings/pages/landingpage.svelteapps/dashboard/static/templates/*.png(8 theme preview PNGs)packages/ui/src/custom/org-landing-page/(theme component implementations)apps/api/src/routes/onboarding/onboarding.tsapps/api/src/services/onboarding.tsapps/api/src/services/course/course.ts(createCourse pattern)apps/api/src/services/course-import/course-import.ts(bulk lesson/section seed pattern)apps/api/src/routes/organization/course-import.tspackages/db/src/schema.ts(organization, course, courseSection, lesson, courseImportDraft)packages/db/src/utils/seed/course.ts(existing dev seed, reference only)packages/utils/src/validation/onboarding/onboarding.tsprd/onboarding-ai-bootstrap/README.md(sibling initiative, must not conflict)prd/org-landing-page-theme-picker [DONE]/README.md(foundation shipped Apr 2026)
Current Baseline (As-Is)
ONBOARDING_STEPS = { ORG_SETUP: 1, USER_METADATA: 2 }— two steps.createOrganizationWithOwnercreates the org withlandingPage = createDefaultLandingPageSettings()(theme'minimal', default hero copy fromdefaultLandingPageHeroinapps/dashboard/src/lib/features/org/utils/landing-page.ts:33).updateUserOnboardingsaves goal/source/fullname, updates user locale, and the API class redirects to/org/{siteName}?welcomePopup=....- No course creation happens during onboarding. The "create=true" query param on the courses page is unused.
- Theme picker exists only in
/settings/landingpage— never surfaced to first-time users. - The 8 theme preview PNGs at
apps/dashboard/static/templates/{theme}.pngare already shipped and used by the settings editor; we can reuse them as-is.
Proposed Experience
Step 1: Account + Org Basics (unchanged)
fullnameorgNamesiteName
On submit: existing createOrganizationWithOwner runs and the org is created with the default 'minimal' theme.
Step 2 (NEW): Theme Picker + Course Preview
Single screen, two stacked sections inside the existing centered onboarding card.
Top section — "Pick a look for your school"
- Responsive 4-up grid (collapsing to 2-up on small screens) of 8 theme cards.
- Each card:
/templates/{theme}.png+ theme display name + a subtle "Selected" ring when active. - Default selection:
minimal(matches what was already saved at org-create time). - Clicking a card sets local
fields.theme. No autosave — persistence happens on "Continue".
Bottom section — "Your workspace will come pre-loaded"
- 4 static course-tile mockups (using the same primitive
/coursesuses) showing variety: oneSELF_PACED, oneLIVE_CLASS, oneCOMPLIANCE, one with a certificate badge. - Subtext: "We'll add a few sample courses you can edit, publish, or delete."
- These tiles are previews only — not the actual courses that will be seeded. They communicate "you're not landing in an empty room."
Continue: calls a new POST /onboarding/select-theme with { theme }, then advances to step 3.
Back: returns to step 1 without losing org data (org is already persisted; back is just a step-state change).
Step 3: User Metadata (unchanged in shape, behavior change on submit)
goal(radio)source(radio)locale(select)
On submit: existing updateUserOnboarding runs, then the route invokes seedDemoCoursesForGoal(orgId, profileId, goal) synchronously (with a short timeout — see FR-4) before returning. Response includes seededCourseIds so the frontend can surface them in the post-redirect view.
Post-Onboarding
- Redirect to
/org/{siteName}/courses(drop the?create=truemarker — it was unused and the page is no longer empty). - The existing
welcomePopupmodal still fires and now references the seeded courses ("We added a few sample courses to get you started — feel free to edit or delete them.").
Functional Requirements
FR-1 — Theme Persistence
POST /onboarding/select-themeaccepts{ theme: OrgLandingPageTheme }, validates againstlandingPageThemes(8 values), and patchesorganization.landingPage.themeusing ajsonb_setso other landing-page fields (hero, navItems, footer) remain untouched.- Endpoint requires
authMiddleware. Caller must be the owner of the org tied to the current onboarding session. - Idempotent — re-submitting the same theme is a no-op success; re-submitting a different theme overwrites.
FR-2 — Curated Demo Course Set
- Define
DEMO_COURSES_BY_GOAL: Record<OnboardingGoal, DemoCourseSpec[]>inpackages/db/src/utils/demo-courses/index.ts. - Each
DemoCourseSpecincludes:title,description,overview(markdown),type(SELF_PACED|LIVE_CLASS|COMPLIANCE|PUBLIC),bannerImage(URL to a static asset), optionalcertificateconfig, and asections: Array<{ title, lessons: Array<{ title, note, videos? }> }>tree. - Lesson content: title + ~150-word markdown
note+ optional public YouTube embed reference. No file uploads required. - Target size: 2 courses per goal, 3 sections per course, 3 lessons per section — enough to look real, cheap to seed.
- Mapping (initial):
sell-online→ "Launch Your First Paid Course" (SELF_PACED), "Marketing Funnels for Course Creators" (SELF_PACED)teach-online→ "Designing Engaging Live Cohorts" (LIVE_CLASS), "Active Learning Toolkit" (SELF_PACED)employees→ "Workplace Compliance 101" (COMPLIANCE, with certificate), "Manager Essentials" (SELF_PACED)customers→ "Customer Onboarding Bootcamp" (SELF_PACED), "Product Mastery" (PUBLIC)expanding-platform→ one of eachtype(broadest exposure)
FR-3 — Demo Course Seeding
- New service
seedDemoCoursesForGoal(orgId, profileId, goal)inapps/api/src/services/onboarding/seed-demo-courses.ts. - Internally calls a new query helper
createDemoCoursesForOrg(orgId, ownerProfileId, specs)inpackages/db/src/queries/onboarding/demo-courses.tsthat runs inside a single transaction, mirroring the insert shape used bycreateCourse(apps/api/src/services/course/course.ts:181) — group + course + group-member + default section + welcome newsfeed — extended for the additional sections/lessons. - Seeded courses are created with
isPublished: falseandisTemplate: falseso they appear as drafts the owner can edit/publish/delete. - Idempotency: before seeding, the service counts the org's non-template courses; if
>= 1, it skips. This guards against double-submit of/update-metadata.
FR-4 — Seed Failure Doesn't Block Redirect
- The
/update-metadatahandler awaitsseedDemoCoursesForGoalwith a 5-second timeout. - On timeout or thrown error: log structured warning (
onboarding.seed_demo_courses.failedwithorgId,goal,error), return{ success: true, data: { ...metadata, seededCourseIds: [] } }so the user is not stranded. A background retry is not in scope for v1.
FR-5 — Theme Picker UI
- New
apps/dashboard/src/lib/features/onboarding/components/theme-picker.svelte. - Renders the 8-theme grid + the 4-tile preview block.
- Theme list is sourced from
landingPageThemesinapps/dashboard/src/lib/features/org/utils/landing-page.ts:22(single source of truth). - Theme labels are translation keys:
onboarding.theme.{themeId}. - Preview tiles reuse the existing course-card primitive (the one rendered at
/courses— verify the component name during implementation; if multiple exist, pick the one used in the published course list).
FR-6 — Step Constants & Progress Bar
- Update
ONBOARDING_STEPSto{ ORG_SETUP: 1, THEME_PICKER: 2, USER_METADATA: 3 }. - The existing progress bar (
apps/dashboard/src/routes/(auth)/onboarding/+page.svelte:24) usesObject.keys(ONBOARDING_STEPS).length, so it auto-scales. - Back button is enabled on steps 2 and 3 (currently only enabled when
step > ORG_SETUP).
FR-7 — Validation
- Add
ZOnboardingSelectThemeinpackages/utils/src/validation/onboarding/:export const ZOnboardingSelectTheme = z.object({ theme: z.enum(LANDING_PAGE_THEMES) }); LANDING_PAGE_THEMESmust be a single shared literal exported once (either from@cio/ui/custom/org-landing-page/typesor@cio/utils) and reused by both the validator and the frontendlandingPageThemesarray.
FR-8 — Frontend Type Hygiene
- Per CLAUDE.md frontend conventions:
- All request/response types live in
apps/dashboard/src/lib/features/onboarding/utils/types.ts, inferred from the API (typeof classroomio.onboarding['select-theme'].$post). - No type definitions in
.svelte.tsfiles. - All copy lives in
apps/dashboard/src/lib/utils/translations/en.jsonunderonboarding.theme.*.
- All request/response types live in
FR-9 — Observability
- Emit funnel events (whatever analytics layer the dashboard currently uses — verify before instrumenting):
onboarding.theme_picker_viewedonboarding.theme_selected(withthemeproperty)onboarding.demo_courses_seeded(withgoal,count,latency_ms)onboarding.demo_courses_seed_failed(withgoal,error)
- These are additive; do not remove existing onboarding analytics.
FR-10 — Self-Host Safety
- The demo-course seed payload is a static constant in
@cio/db. Self-hosted instances get the same behavior with no external dependencies (no LLM calls, no image hosting beyond static assets bundled with the dashboard). - Default theme
'minimal'continues to be applied at org-create time, so users who skip step 2 (e.g. close the tab) still end up with a working landing page.
Proposed API + Service Design
Validation — packages/utils/src/validation/onboarding/
ZOnboardingSelectTheme— see FR-7.ZOnboardingUpdateMetadata— no shape change; behavior change is server-side only.
Routes — apps/api/src/routes/onboarding/onboarding.ts
POST /onboarding/select-theme(new) — auth + zValidator(ZOnboardingSelectTheme); delegates toselectOnboardingTheme(profileId, orgId, theme).POST /onboarding/update-metadata(extended) — afterupdateUserOnboarding, awaitsseedDemoCoursesForGoal(orgId, profileId, goal)with timeout/log-on-fail behavior in FR-4. Returns{ success: true, data: { ...existing, seededCourseIds } }.
Services — apps/api/src/services/onboarding/
select-theme.ts—selectOnboardingTheme(profileId, orgId, theme). Authorizes profile is owner; calls query helper.seed-demo-courses.ts—seedDemoCoursesForGoal(orgId, profileId, goal). Idempotency check + delegate to query helper.
Queries — packages/db/src/queries/
organization/landing-page.ts—updateOrgLandingPageTheme(orgId, theme)usingjsonb_set.onboarding/demo-courses.ts—createDemoCoursesForOrg(orgId, profileId, specs)running in a single transaction.
Data — packages/db/src/utils/demo-courses/index.ts
- Static export
DEMO_COURSES_BY_GOAL+ types. Lives in@cio/dbso both API services and (future) self-host migration tools can import it.
Dashboard — apps/dashboard/src/lib/features/onboarding/
utils/constants.ts— addTHEME_PICKERstep.utils/types.ts— extendOnboardingFieldwiththeme?: OrgLandingPageTheme; add request type for the new endpoint.api/onboarding.svelte.ts— addsubmitTheme(theme)method; update step dispatcher; advance toUSER_METADATAon success.components/theme-picker.svelte(new) — the new step UI.routes/(auth)/onboarding/+page.svelte— extend the if/else block to a 3-way switch; wire back-button to step down through all 3 states.
Data Model Changes
None at schema level. Both new behaviors reuse existing tables:
- Theme selection writes to the existing
organization.landingPagejsonb column. - Demo courses use the existing
course,courseSection,lessontables (no new columns).
No migration is required. (Aligns with the user's standing rule: schema edits stop at a passing @cio/db build; migrations are handled outside this workflow.)
Translation Keys (new)
Under onboarding.theme.* in apps/dashboard/src/lib/utils/translations/en.json:
heading— "Pick a look for your school"subheading— "You can change this anytime in settings."demo_heading— "Your workspace will come pre-loaded"demo_subheading— "We'll add a few sample courses you can edit, publish, or delete."minimal,bold,classic,saas,tech,studio,corporate,terminal— display labels.
UX Quality Bar
Per the user's standing UI rules:
- Use
@cio/ui/base/cardfor theme tiles, withui:ring-primaryfor selected state (color tokens prefixedui:for theme resolution). - Use the same course-card primitive
/coursesuses for the preview tiles — do not invent a new card. If a suitable component does not exist, extract one rather than copy-pasting markup. - No hardcoded user-facing strings.
- No native
<input>/<label>in this UI; use@cio/ui/custom/*-fieldwrappers if any inputs are needed (none required for v1, but applies if scope grows).
Rollout Plan
- Phase 1 — internal alpha (week 1): ship behind a soft "feature flag" by gating step 2 on an env var (
PUBLIC_ONBOARDING_SHOWCASE_ENABLED) so we can test in staging without exposing to prod signups. - Phase 2 — 25% cloud rollout (week 2): bucket by user ID hash.
- Phase 3 — 100% cloud (week 3, assuming no regression): remove the env gate; legacy 2-step flow code is deleted.
- Phase 4 — self-host: ships with the next normal release; no toggle needed (no external dependencies).
Success Metrics
- +15% relative lift in onboarding completion (step-3 submit / step-1 view).
- +30% relative lift in "user views the public landing page in their first session" — proxy for "did the theme picker land?"
- +50% relative lift in "user edits at least one course in their first session" — proxy for the seeded courses being a real lure into the editor.
- Median onboarding time stays under 4 minutes — adding a step must not bloat the flow.
- <2% seed-failure rate measured by
onboarding.demo_courses_seed_failedoveronboarding.theme_selected.
Risks and Mitigations
- Risk: Seeded demo courses confuse users who think they're real templates they have to keep. Mitigation: courses ship as
isPublished: falsedrafts; welcome popup copy explicitly says "feel free to edit or delete"; a small "Sample" badge on the course card is a v1.1 candidate. - Risk: Demo course content drifts and looks dated. Mitigation: content is in code (
DEMO_COURSES_BY_GOAL) and reviewed every release; quarterly content refresh is owned by the content team. - Risk: Seed transaction is slow on large goal sets (5+ courses × 9 lessons each = lots of inserts). Mitigation: bulk insert with a single transaction; cap demo set at 2 courses × 3 sections × 3 lessons; FR-4 timeout protects the user-facing path.
- Risk: Goal value drift (someone adds a 6th goal to
GOALS) breaks theDEMO_COURSES_BY_GOALlookup. Mitigation: TypeScriptRecord<OnboardingGoal, ...>makes this a compile-time error; CI catches it. - Risk: Conflict with the parallel
onboarding-ai-bootstrapPRD. Mitigation: that PRD adds a website URL step and AI-generated courses; this PRD's theme step and curated demo set are orthogonal and can stack later (AI courses replace demo courses when AI flow runs; theme picker is shared). Document inprd/onboarding-ai-bootstrap/README.mdonce both are merged. - Risk: Theme choice in onboarding gets overwritten by the default-applied theme on org create. Mitigation: org create still applies
'minimal'default; the new/select-themeendpoint runs after and usesjsonb_setto patch only the theme field — confirmed by manual E2E in the verification plan.
Open Questions
- Should the preview tiles in step 2 be personalized with the org's just-entered name (e.g. "Welcome to {orgName}") to feel less canned? Adds complexity for marginal trust gain.
- Should the welcome popup link directly to one of the seeded courses to encourage immediate editing, vs. dropping the user on the courses list?
- Should
locale != 'en'users see a banner saying "Sample courses are in English — translate or replace as you like" instead of getting English-language content silently? - Do we want to display a "Sample" pill on the seeded courses in the course list so the owner remembers what's editable demo content? (Recommendation: yes, but ship in v1.1.)
- Should the theme picker also let the user upload a logo? (Recommendation: no — keep scope tight; logo upload lives in settings.)
Verification Plan
- Build:
pnpm --filter @cio/db build && pnpm --filter @cio/api build && pnpm --filter @cio/dashboard check - Manual E2E, fresh user per run:
- Sign up → confirm step 1 looks unchanged.
- Step 2: 8 themes render with correct preview PNGs; selection ring follows clicks; "Continue" persists theme.
- Step 3: submit metadata → confirm redirect to
/org/{slug}/coursespopulated with 2 demo courses matching the chosen goal. - Public landing page
/{slug}renders the picked theme (notminimal). - Settings → Landing Page reflects the picked theme.
- Open a seeded course → confirm sections, lessons, and lesson notes render.
- Goal coverage: run the E2E once per
goalvalue (5 runs) — confirm correct course set per goal. - Idempotency: replay
POST /onboarding/update-metadatavia dev tools — confirm no duplicate courses created. - Seed failure path: temporarily throw inside
createDemoCoursesForOrg— confirm user still completes onboarding and lands on/courses(empty but functional), and thatonboarding.demo_courses_seed_failedlog line emits. - Theme override: change theme via back button in step 2 multiple times — last selection wins.