Skip to main content

Onboarding Flow

@doc-version: 1.0.0 @last-updated: 2026-01-10 The onboarding flow guides new users through the essential setup steps: establishing identity, connecting to their ward, and choosing their civic role.

Overview

Core Principle: “You’re not joining a platform — you’re stepping into your place.” Onboarding should feel like returning home, not signing up for another social network.

The User Journey

Sign Up (Auth)

┌─────────────────────────────────┐
│         ONBOARDING WIZARD       │
│                                 │
│  Step 1: Identity               │
│  - Display name                 │
│  - Avatar (optional)            │
│                                 │
│  Step 2: Ward Selection         │
│  - Geolocation or search        │
│  - Confirm home ward            │
│                                 │
│  Step 3: Role Selection         │
│  - Choose civic role            │
│  - (Fixer, Watcher, Organizer)  │
└─────────────────────────────────┘

  Ward Dashboard

Architecture

Route Structure

/onboarding              → Main onboarding page
  └── OnboardingWizard   → Client component wizard
        ├── StepIdentity
        ├── StepWardSelect
        └── StepRole

Components

ComponentPurpose
OnboardingWizardState management and step orchestration
StepIdentityDisplay name and avatar setup
StepWardSelectGeolocation + ward confirmation
StepRoleCivic role selection

The Onboarding Wizard

// components/onboarding/OnboardingWizard.tsx
export type OnboardingData = {
  wardId?: string
  wardName?: string
  displayName?: string
  avatarPath?: string
  role?: string
}

export function OnboardingWizard() {
  const [step, setStep] = useState(1)
  const [data, setData] = useState<OnboardingData>({})

  const finish = async () => {
    await completeOnboarding({
      ward_code: data.wardId,
      full_name: data.displayName,
      role: data.role
    })
  }

  // Step rendering with AnimatePresence for smooth transitions
}

Step 1: Identity

Users establish their display identity:
// components/onboarding/steps/StepIdentity.tsx
export function StepIdentity({ data, updateData, onNext }) {
  return (
    <motion.div>
      <h2>Who are you?</h2>
      
      {/* Display Name Input */}
      <Input 
        value={data.displayName}
        onChange={(e) => updateData({ displayName: e.target.value })}
        placeholder="Your name"
      />
      
      {/* Avatar Upload (Optional) */}
      <AvatarUpload 
        onUpload={(path) => updateData({ avatarPath: path })}
      />
      
      <Button onClick={onNext}>Continue</Button>
    </motion.div>
  )
}
Design Choices:
  • Display name is optional but encouraged
  • Avatar is fully optional — no gatekeeping
  • Skip button available for quick setup

Step 2: Ward Selection

Users connect to their physical ward:
// components/onboarding/steps/StepWardSelect.tsx
export function StepWardSelect({ data, updateData, onNext, onBack }) {
  // Uses LocationPicker component for geolocation
  
  return (
    <motion.div>
      <h2>Where do you live?</h2>
      
      <LocationPicker 
        onWardSelected={(ward) => updateData({
          wardId: ward.ward_code,
          wardName: ward.name
        })}
      />
      
      {/* Alternative: Search by area */}
      <WardSearchInput />
      
      <div className="flex gap-4">
        <Button variant="outline" onClick={onBack}>Back</Button>
        <Button onClick={onNext}>Confirm Ward</Button>
      </div>
    </motion.div>
  )
}
Ward Detection Flow:
  1. Request geolocation permission
  2. Call findWardByLocation(lat, lng) RPC
  3. Display ward name and municipality
  4. Allow manual search as fallback

Step 3: Role Selection

Users choose their civic identity:
// components/onboarding/steps/StepRole.tsx
const ROLES = [
  {
    id: 'fixer',
    title: 'Fixer',
    description: 'I want to help solve problems in my community',
    icon: Wrench
  },
  {
    id: 'watcher',
    title: 'Watcher',
    description: 'I want to stay informed about what happens locally',
    icon: Eye
  },
  {
    id: 'organizer',
    title: 'Organizer',
    description: 'I want to coordinate community action',
    icon: Users
  }
]

export function StepRole({ data, updateData, onNext, onBack, isLoading }) {
  return (
    <motion.div>
      <h2>How will you participate?</h2>
      
      <div className="grid gap-4">
        {ROLES.map(role => (
          <RoleCard 
            key={role.id}
            role={role}
            selected={data.role === role.id}
            onClick={() => updateData({ role: role.id })}
          />
        ))}
      </div>
      
      <Button onClick={onNext} disabled={isLoading}>
        {isLoading ? 'Setting up...' : 'Enter My Ward'}
      </Button>
    </motion.div>
  )
}
Role Implications:
  • Roles affect default notification preferences
  • Roles influence dashboard widget ordering
  • Roles are displayed on profile badges
  • Roles can be changed later in settings

Server Action: Complete Onboarding

// app/actions/onboarding.ts
export async function completeOnboarding(data: {
  ward_code: string;
  full_name?: string;
  role: string;
}) {
  const user = await getCurrentUser();
  if (!user) throw new Error('Unauthorized');

  const supabase = await createClient();

  // Update profile with onboarding data
  const { error } = await supabase
    .from('profiles')
    .update({
      ward_code: data.ward_code,
      full_name: data.full_name,
      role: data.role,
      onboarded_at: new Date().toISOString()
    })
    .eq('id', user.id);

  if (error) throw error;

  // Redirect to ward dashboard
  redirect(`/ward/${data.ward_code}`);
}

Onboarding Guard

Incomplete profiles are redirected to onboarding:
// middleware.ts or layout checks
if (!profile.onboarded_at && !pathname.startsWith('/onboarding')) {
  redirect('/onboarding');
}

Animation & UX

The wizard uses Framer Motion for step transitions:
<AnimatePresence mode="wait">
  <motion.div
    initial={{ opacity: 0, x: 20 }}
    animate={{ opacity: 1, x: 0 }}
    exit={{ opacity: 0, x: -20 }}
    transition={{ duration: 0.3 }}
  >
    {/* Step content */}
  </motion.div>
</AnimatePresence>
Progress Indicator:
  • Visual progress bar showing completion %
  • Step counter “Step 2 of 3”
  • Animated bar width transition


Status: Active
Last Updated: January 2026