Onboarding Wizard for SaaS Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

SaaS onboarding wizard

Onboarding determines the activation rate — the percentage of users who reach the moment of perceiving product value. A wizard is a step-by-step scenario that guides the user to first success.

Designing steps

The goal of onboarding is to get to the "aha moment" in minimum time. Each extra step reduces completion rate.

Step 1: Company profile (name, type, size)
Step 2: Invite first team member
Step 3: Create first project (key action)
Step 4: Connect integration (Slack/GitHub/Jira)
→ Aha moment: first active project with team

Data schema

model OnboardingProgress {
  id          String   @id @default(cuid())
  tenantId    String   @unique
  currentStep Int      @default(0)
  completedAt DateTime?
  steps       Json     // { "profile": true, "invite": false, "project": false, "integration": false }
  startedAt   DateTime @default(now())

  tenant Tenant @relation(fields: [tenantId], references: [id])
}

Wizard component

// components/onboarding/OnboardingWizard.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

interface Step {
  id: string;
  title: string;
  component: React.ComponentType<StepProps>;
  optional?: boolean;
}

const STEPS: Step[] = [
  { id: 'profile', title: 'About your company', component: ProfileStep },
  { id: 'invite', title: 'Invite team', component: InviteStep, optional: true },
  { id: 'project', title: 'First project', component: CreateProjectStep },
  { id: 'integration', title: 'Connect tools', component: IntegrationStep, optional: true },
];

export function OnboardingWizard({
  initialStep,
  completedSteps,
}: {
  initialStep: number;
  completedSteps: Record<string, boolean>;
}) {
  const [currentStep, setCurrentStep] = useState(initialStep);
  const [completed, setCompleted] = useState(completedSteps);
  const router = useRouter();

  const step = STEPS[currentStep];
  const StepComponent = step.component;

  const handleNext = async (skipValidation = false) => {
    if (!skipValidation) {
      // Mark step as completed
      await updateStepProgress(step.id);
      setCompleted(prev => ({ ...prev, [step.id]: true }));
    }

    if (currentStep < STEPS.length - 1) {
      setCurrentStep(prev => prev + 1);
    } else {
      // Onboarding complete
      await completeOnboarding();
      router.push('/dashboard');
    }
  };

  return (
    <div className="max-w-2xl mx-auto py-12 px-4">
      {/* Progress bar */}
      <div className="mb-8">
        <div className="flex items-center gap-2">
          {STEPS.map((s, idx) => (
            <div key={s.id} className="flex items-center gap-2">
              <div className={`
                w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
                ${completed[s.id]
                  ? 'bg-green-500 text-white'
                  : idx === currentStep
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-200 text-gray-500'
                }
              `}>
                {completed[s.id] ? '✓' : idx + 1}
              </div>
              {idx < STEPS.length - 1 && (
                <div className={`flex-1 h-0.5 ${completed[s.id] ? 'bg-green-500' : 'bg-gray-200'}`} />
              )}
            </div>
          ))}
        </div>
        <p className="mt-2 text-sm text-gray-500">
          Step {currentStep + 1} of {STEPS.length}: {step.title}
        </p>
      </div>

      {/* Step content */}
      <StepComponent
        onNext={handleNext}
        onSkip={step.optional ? () => handleNext(true) : undefined}
      />
    </div>
  );
}

Wizard steps

// components/onboarding/steps/ProfileStep.tsx
export function ProfileStep({ onNext }: StepProps) {
  const form = useForm<ProfileForm>({
    resolver: zodResolver(profileSchema),
  });

  const onSubmit = async (data: ProfileForm) => {
    await updateOrganizationProfile(data);
    onNext();
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <h2 className="text-2xl font-bold mb-6">Tell us about your company</h2>

      <FormField
        label="Company name"
        error={form.formState.errors.name?.message}
      >
        <Input {...form.register('name')} placeholder="Acme Corp" autoFocus />
      </FormField>

      <FormField label="Team type" error={form.formState.errors.teamType?.message}>
        <Select {...form.register('teamType')}>
          <option value="startup">Startup</option>
          <option value="agency">Agency</option>
          <option value="enterprise">Enterprise</option>
          <option value="freelancer">Freelancer</option>
        </Select>
      </FormField>

      <FormField label="Team size">
        <Select {...form.register('teamSize')}>
          <option value="1">Just me</option>
          <option value="2-10">2–10 people</option>
          <option value="11-50">11–50 people</option>
          <option value="50+">50+ people</option>
        </Select>
      </FormField>

      <Button type="submit" className="w-full mt-6" isLoading={form.formState.isSubmitting}>
        Continue
      </Button>
    </form>
  );
}

Onboarding tracking

// Track onboarding completion rate by step
export async function trackOnboardingStep(step: string, tenantId: string) {
  posthog.capture('onboarding_step_completed', {
    distinct_id: tenantId,
    step,
    timestamp: new Date().toISOString(),
  });
}

// In analytics, track funnel:
// onboarding_started → profile_completed → team_invited → project_created
// Find the largest drop-off

Resume onboarding

// Progress is saved — user can return later
// On each login, check onboarding status

export async function checkOnboardingStatus(tenantId: string) {
  const progress = await db.onboardingProgress.findUnique({
    where: { tenantId }
  });

  if (!progress?.completedAt) {
    // Redirect to next incomplete step
    return {
      completed: false,
      currentStep: progress?.currentStep ?? 0,
    };
  }

  return { completed: true };
}

Developing an onboarding wizard with tracking and progress recovery — 3–5 working days.