Files
classroomio/prd/onboarding-platform-showcase/README.md
T

21 KiB
Raw Blame History

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:

  1. Org name + slug + full name
  2. 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:

  1. 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.
  2. 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)

  1. SaaS founders launching a product academy.
  2. Independent course creators selling self-paced cohorts.
  3. Operations leaders running employee/compliance training.
  4. Customer-success teams running customer enablement.

Target Jobs-To-Be-Done

  1. Quickly evaluate whether ClassroomIO can produce a school site that looks like the creator's brand.
  2. See what a "good" course structure looks like on the platform without having to build one to find out.
  3. Get to a publishable-feeling state in a single session.

Confirmed Product Decisions

  1. Theme picker is a real step (not a passive preview gallery). The user's selection is persisted to organization.landingPage.theme.
  2. Auto-seed goal-relevant demo courses — no extra picker step. The existing goal answer in metadata drives which course set lands in the org.
  3. Step order: org-setup → theme-picker (+ course preview) → user-metadata → seed-and-redirect.
  4. Course source: a new curated demo set authored in code (not the existing seedCourses() templates in packages/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 locale choice 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.svelte
  • apps/dashboard/src/lib/features/onboarding/api/onboarding.svelte.ts
  • apps/dashboard/src/lib/features/onboarding/utils/constants.ts
  • apps/dashboard/src/lib/features/onboarding/utils/types.ts
  • apps/dashboard/src/lib/features/onboarding/components/verify-email-modal.svelte
  • apps/dashboard/src/lib/features/onboarding/components/welcome-modal.svelte
  • apps/dashboard/src/lib/features/org/utils/landing-page.ts
  • apps/dashboard/src/lib/utils/types/org.ts
  • apps/dashboard/src/lib/features/settings/pages/landingpage.svelte
  • apps/dashboard/static/templates/*.png (8 theme preview PNGs)
  • packages/ui/src/custom/org-landing-page/ (theme component implementations)
  • apps/api/src/routes/onboarding/onboarding.ts
  • apps/api/src/services/onboarding.ts
  • apps/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.ts
  • packages/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.ts
  • prd/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)

  1. ONBOARDING_STEPS = { ORG_SETUP: 1, USER_METADATA: 2 } — two steps.
  2. createOrganizationWithOwner creates the org with landingPage = createDefaultLandingPageSettings() (theme 'minimal', default hero copy from defaultLandingPageHero in apps/dashboard/src/lib/features/org/utils/landing-page.ts:33).
  3. updateUserOnboarding saves goal/source/fullname, updates user locale, and the API class redirects to /org/{siteName}?welcomePopup=....
  4. No course creation happens during onboarding. The "create=true" query param on the courses page is unused.
  5. Theme picker exists only in /settings/landingpage — never surfaced to first-time users.
  6. The 8 theme preview PNGs at apps/dashboard/static/templates/{theme}.png are already shipped and used by the settings editor; we can reuse them as-is.

Proposed Experience

Step 1: Account + Org Basics (unchanged)

  • fullname
  • orgName
  • siteName

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 /courses uses) showing variety: one SELF_PACED, one LIVE_CLASS, one COMPLIANCE, 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=true marker — it was unused and the page is no longer empty).
  • The existing welcomePopup modal 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-theme accepts { theme: OrgLandingPageTheme }, validates against landingPageThemes (8 values), and patches organization.landingPage.theme using a jsonb_set so 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[]> in packages/db/src/utils/demo-courses/index.ts.
  • Each DemoCourseSpec includes: title, description, overview (markdown), type (SELF_PACED | LIVE_CLASS | COMPLIANCE | PUBLIC), bannerImage (URL to a static asset), optional certificate config, and a sections: 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 each type (broadest exposure)

FR-3 — Demo Course Seeding

  • New service seedDemoCoursesForGoal(orgId, profileId, goal) in apps/api/src/services/onboarding/seed-demo-courses.ts.
  • Internally calls a new query helper createDemoCoursesForOrg(orgId, ownerProfileId, specs) in packages/db/src/queries/onboarding/demo-courses.ts that runs inside a single transaction, mirroring the insert shape used by createCourse (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: false and isTemplate: false so 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-metadata handler awaits seedDemoCoursesForGoal with a 5-second timeout.
  • On timeout or thrown error: log structured warning (onboarding.seed_demo_courses.failed with orgId, 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 landingPageThemes in apps/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_STEPS to { ORG_SETUP: 1, THEME_PICKER: 2, USER_METADATA: 3 }.
  • The existing progress bar (apps/dashboard/src/routes/(auth)/onboarding/+page.svelte:24) uses Object.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 ZOnboardingSelectTheme in packages/utils/src/validation/onboarding/:
    export const ZOnboardingSelectTheme = z.object({
      theme: z.enum(LANDING_PAGE_THEMES)
    });
    
  • LANDING_PAGE_THEMES must be a single shared literal exported once (either from @cio/ui/custom/org-landing-page/types or @cio/utils) and reused by both the validator and the frontend landingPageThemes array.

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.ts files.
    • All copy lives in apps/dashboard/src/lib/utils/translations/en.json under onboarding.theme.*.

FR-9 — Observability

  • Emit funnel events (whatever analytics layer the dashboard currently uses — verify before instrumenting):
    • onboarding.theme_picker_viewed
    • onboarding.theme_selected (with theme property)
    • onboarding.demo_courses_seeded (with goal, count, latency_ms)
    • onboarding.demo_courses_seed_failed (with goal, 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 to selectOnboardingTheme(profileId, orgId, theme).
  • POST /onboarding/update-metadata (extended) — after updateUserOnboarding, awaits seedDemoCoursesForGoal(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) using jsonb_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/db so both API services and (future) self-host migration tools can import it.

Dashboard — apps/dashboard/src/lib/features/onboarding/

  • utils/constants.ts — add THEME_PICKER step.
  • utils/types.ts — extend OnboardingField with theme?: OrgLandingPageTheme; add request type for the new endpoint.
  • api/onboarding.svelte.ts — add submitTheme(theme) method; update step dispatcher; advance to USER_METADATA on 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.landingPage jsonb column.
  • Demo courses use the existing course, courseSection, lesson tables (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/card for theme tiles, with ui:ring-primary for selected state (color tokens prefixed ui: for theme resolution).
  • Use the same course-card primitive /courses uses 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/*-field wrappers if any inputs are needed (none required for v1, but applies if scope grows).

Rollout Plan

  1. 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.
  2. Phase 2 — 25% cloud rollout (week 2): bucket by user ID hash.
  3. Phase 3 — 100% cloud (week 3, assuming no regression): remove the env gate; legacy 2-step flow code is deleted.
  4. Phase 4 — self-host: ships with the next normal release; no toggle needed (no external dependencies).

Success Metrics

  1. +15% relative lift in onboarding completion (step-3 submit / step-1 view).
  2. +30% relative lift in "user views the public landing page in their first session" — proxy for "did the theme picker land?"
  3. +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.
  4. Median onboarding time stays under 4 minutes — adding a step must not bloat the flow.
  5. <2% seed-failure rate measured by onboarding.demo_courses_seed_failed over onboarding.theme_selected.

Risks and Mitigations

  1. Risk: Seeded demo courses confuse users who think they're real templates they have to keep. Mitigation: courses ship as isPublished: false drafts; welcome popup copy explicitly says "feel free to edit or delete"; a small "Sample" badge on the course card is a v1.1 candidate.
  2. 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.
  3. 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.
  4. Risk: Goal value drift (someone adds a 6th goal to GOALS) breaks the DEMO_COURSES_BY_GOAL lookup. Mitigation: TypeScript Record<OnboardingGoal, ...> makes this a compile-time error; CI catches it.
  5. Risk: Conflict with the parallel onboarding-ai-bootstrap PRD. 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 in prd/onboarding-ai-bootstrap/README.md once both are merged.
  6. 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-theme endpoint runs after and uses jsonb_set to patch only the theme field — confirmed by manual E2E in the verification plan.

Open Questions

  1. 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.
  2. Should the welcome popup link directly to one of the seeded courses to encourage immediate editing, vs. dropping the user on the courses list?
  3. 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?
  4. 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.)
  5. Should the theme picker also let the user upload a logo? (Recommendation: no — keep scope tight; logo upload lives in settings.)

Verification Plan

  1. Build: pnpm --filter @cio/db build && pnpm --filter @cio/api build && pnpm --filter @cio/dashboard check
  2. 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}/courses populated with 2 demo courses matching the chosen goal.
    • Public landing page /{slug} renders the picked theme (not minimal).
    • Settings → Landing Page reflects the picked theme.
    • Open a seeded course → confirm sections, lessons, and lesson notes render.
  3. Goal coverage: run the E2E once per goal value (5 runs) — confirm correct course set per goal.
  4. Idempotency: replay POST /onboarding/update-metadata via dev tools — confirm no duplicate courses created.
  5. Seed failure path: temporarily throw inside createDemoCoursesForOrg — confirm user still completes onboarding and lands on /courses (empty but functional), and that onboarding.demo_courses_seed_failed log line emits.
  6. Theme override: change theme via back button in step 2 multiple times — last selection wins.