logoAffluent docs
Onboarding

Onboarding session

How users book a session during their onboarding.

Overview

During the onboarding flow, users are required to schedule a 30‑minute call via Cal.com before they can access the main application. This requirement is enforced through space metadata updates, redirect guards, webhook processing, and real‑time UI synchronization.

Metadata storage

We extended the space.metadata JSONB column (modeled by SpaceMetadata) with two new properties:

  • onboarding_status: A string enum OnboardingStatus indicating the current call status:
    • not_planned
    • planned
    • completed
    • not_required
  • onboarding_meeting_id: The Cal.com meeting UID recorded when a booking is created.

These fields are updated via our server actions and webhook handlers:

// 152:166:apps/app/src/app/api/webhooks/cal/route.ts
async function updateOnboardingStatus(
  spaceId: string,
  status: OnboardingStatus,
  meetingId: string | null,
  payload: CalWebhookPayload
) {
  const metadata: SpaceMetadata = { onboarding_status: status };
  if (meetingId !== null) {
    metadata.onboarding_meeting_id = meetingId;
  }
  await updateSpace(spaceId, { metadata });
}

Default initial onboarding status

At space creation time, we enforce which users require an onboarding call via a database migration and trigger defined in 20250416093106_enforced-onboarding.sql:

-- 37:68:apps/app/supabase/migrations/20250416093106_enforced-onboarding.sql
IF v_invite_type = 'individual' OR v_invite_type = 'advisor_client' THEN
    v_onboarding_status := 'not_required';
ELSIF v_initial_signup_type = 'advisor' THEN
    v_onboarding_status := 'not_required';
ELSE
    v_onboarding_status := 'not_planned';
END IF;
  • not_required: Exempts invite flows (individual (= space members) or advisor_client) and advisor signups from scheduling.
  • not_planned: Marks self-signed-up users as needing to plan their onboarding call.

Redirection logic

Our shared determineRedirectUrl evaluates space.metadata.onboarding_status to guard routes:

// 15:24:apps/app/src/lib/auth/utils/redirect-logic.ts
if (isOnboardingRequired(selectedSpace as UserSpace)) {
  return routes.onboardingSession;
}

On the onboarding session page (page.tsx), we allow access only when the status is NotPlanned or Planned; all other statuses redirect the user to their assets or advisor dashboard.

Cal.com webhook integration

We embed the Cal.com widget with a metadata[spaceId] parameter so that every booking event carries the space ID:

// ... in onboarding-session-client.tsx ...
<Cal
  namespace="30min"
  calLink={CAL_ONBOARDING_URL}
  config={{
    ...,
    "metadata[spaceId]": selectedSpace.spaceId,
  }}
/>

Incoming webhook events at /api/webhooks/cal trigger:

  1. handleBookingCreated to mark onboarding_status = Planned.
  2. handleBookingCancelled to revert onboarding_status = NotPlanned, using onboarding_meeting_id lookup since cancellation payloads omit the original spaceId metadata.

Real‑time UI updates

The client component subscribes to Supabase Realtime updates on the space table:

// 20:33:apps/app/src/app/[locale]/space/onboarding-session/components/onboarding-session-client.tsx
useEffect(() => {
  const channel = client
    .channel("space-changes")
    .on(
      "postgres_changes",
      { event: "UPDATE", schema: "public", table: "space" },
      (payload) => {
        if (
          JSON.stringify(payload.new.metadata) !==
          JSON.stringify(payload.old.metadata)
        ) {
          queryClient.invalidateQueries({ queryKey: CORE_DATA_KEY });
        }
      }
    )
    .subscribe();
  return () => client.removeChannel(channel);
}, [queryClient, client]);

When the metadata changes, we invalidate the core data cache, causing the component to refetch and automatically update the UI (showing a blur over the calendar and a confirmation once a meeting is planned).

Development utilities

In non-production environments, the OnboardingSessionDevTool component renders a dev panel allowing maintainers to simulate booking and cancellation:

// apps/app/src/app/[locale]/space/onboarding-session/components/onboarding-session-dev-tool.tsx
<Button onClick={() => simulateMeetingBooking(spaceId)}>
  Book fake meeting
</Button>
<Button onClick={() => simulateMeetingCancellation(spaceId)}>
  Cancel fake meeting
</Button>

These buttons invoke the simulateMeetingBooking and simulateMeetingCancellation server actions (in actions.ts), which update the space.metadata (onboarding_status and onboarding_meeting_id) and trigger the real‑time UI updates described above.