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 enumOnboardingStatusindicating the current call status:not_plannedplannedcompletednot_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) oradvisor_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:
handleBookingCreatedto markonboarding_status = Planned.handleBookingCancelledto revertonboarding_status = NotPlanned, usingonboarding_meeting_idlookup since cancellation payloads omit the originalspaceIdmetadata.
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.