MDX Limo
Narrative Skill-Configuring Onboarding V2 — Unified Plan

Unified Onboarding V2

Executive Summary

Replace the current 4-step generic onboarding with a 12-screen narrative flow that connects services, analyzes the user in the background, teaches them about Consul through their own data, configures every skill with inferred settings, verifies their phone via iMessage, generates their Consul email identity, hits a hard paywall, and launches them into a fully-configured system. Unpaid users enter a dormant state with iMessage re-engagement.


Current State

The existing onboarding lives in apps/web/app/start/page.tsx with components in apps/web/components/onboarding/. It writes zero skill configuration — only profile identity (name), timezone, and the onboarding_completed flag via apps/web/app/start/actions.ts.

All actual skill enablement happens through 6 fragmented side-effect pathways that auto-enable features independently of user intent:

PathwayFileAuto-Enables
Gmail OAuth (unified)app/api/auth/google-unified/callback/route.tsTriage, drafts, email reminders, email daily brief (if subscribed)
Gmail OAuth (direct)app/api/auth/gmail/callback/route.tsSame as above
Billing webhookapp/api/billing/webhook/route.tsTriage, drafts, email reminders, email daily brief, Gmail watches
Phone verificationapp/api/phone/verify/confirm/route.tsiMessage reminders, iMessage daily brief (if subscribed)
EA identity creationapp/api/ea/identities/route.tsscheduling_enabled=true
DB triggerseed_triage_tags_for_new_user()Default triage tags (on user_email_settings INSERT)

Locked Decisions

  • Paywall mode: Hard gate. No trial, no soft access.
  • State persistence: profiles.onboarding_state JSONB column (server-side, resumable).
  • Activation model: Staged config — desired settings are collected during onboarding, applied only on payment.
  • Architecture: Desired State + Reconciler (Approach A). Side-effect pathways are refactored to stop auto-enabling. Onboarding is the authority for skill configuration.
  • No feature flag: This is a direct replacement of V1. No canary gating — ship and replace.
  • Scope: New signups only. Existing users are not forced through re-onboarding.

Success Criteria

  • Users complete onboarding with all chosen skills preconfigured and activated only after payment.
  • Unpaid users are safely dormant and receive an onboarding completion nudge in iMessage/SMS.
  • All auto-enable side effects are removed or made onboarding-aware.
  • Home/skills UI reflects onboarding-selected settings deterministically.
  • Drop-off, conversion, and activation metrics are instrumented per step.

Desired Flow (12 Screens)

11. Connect (Google OAuth) 232. Analyzing You (background fetch + teach about Consul) → 2b. Discovery Survey 453. Your Profile (inferred identity + stats) 674. Phone Verification (Text Consul via iMessage) 895. Triage (inbox insights + tag setup) 10116. Drafting (writing style + auto-draft policy) 12137. Consul Email Identity (EA name + scheduling config) 14158. Reminders (channels + quiet hours) 16179. Daily Brief (real generated brief + config) 181910. Roadmap Vote (feature priority + freeform) 202111. Paywall ($50/mo → 75% lifetime discount) 22 ├─ Pay → 12. Launch (welcome email + explore app) 23 └─ Bounce → Dormant State (system off, Consul nudges via iMessage)

Screen-by-Screen Specification

Screen 1: Connect — "Let's connect to your tools"

Component: step-connect.tsx (refactored from step-google-connect.tsx)

What happens: Google OAuth popup — Gmail + Calendar as primary bundle, optional Docs/Drive/Contacts.

On success: Immediately kick off background analysis pipeline (Screen 2 depends on this).

Reuse: OAuth popup logic from step-google-connect.tsx, state HMAC signing from app/api/auth/google-unified/authorize/route.ts.

Skip path: Cannot skip — this is the foundation.

Writes: onboarding_state.step, selected services list.


Screen 2: Analyzing You — "Getting to know you..."

Component: step-analyzing.tsx (new)

Background (parallel API calls via /api/onboarding/analyze):

  • Fetch inbox stats (unread count, category breakdown, sender frequency, weekly volume)
  • Fetch sent emails for writing style analysis (delegates to existing /generate-style on agents service)
  • Fetch last ~50 sent email subjects/recipients/snippets (for Screen 6 drafting animation)
  • Fetch calendar events for working hour/duration inference (last 30 days)
  • Extract email signature from recent sent emails (scan last 20 sent)
  • Begin pre-generating a daily brief (reuse daily brief executor steps)
  • Infer professional context (job title, company from email patterns)
  • Compute relationship summary (unique contacts, interaction frequency)

What user sees: Animated teaching sequence with real data streaming as background visuals:

  • "Consul is an executive assistant that works in the background..."
  • "...to clean your inbox, manage your calendar, relationships..."
  • "...and help you stay on top of what matters"
  • Real emails, calendar events, contacts animate across the screen as they load

Transition: Discovery survey overlay once data fetch completes (or after minimum animation time of ~8-10s).

Partial failure handling: Each analyzer runs independently with retry/backoff. One failure does not block the flow — fallback defaults fill in for any missing data.


Screen 2b: Discovery Survey — "How did you find us?"

Component: Inline in step-analyzing.tsx (refactored from discovery-survey.tsx)

Options: YC, LinkedIn, Podcast, Friend, Twitter/X, Google, Other

Writes: profiles.discovery_source (TEXT[] array)

Note: This replaces the current post-onboarding 30-second delayed survey. Moving it here captures attribution while user is engaged and data is loading.

Exit condition: Survey answered or skipped + analysis complete.


Screen 3: Your Profile — "Here's what we've found about you"

Component: step-profile.tsx (refactored from step-personalize.tsx + new inference display)

What user sees:

  • Inferred name, photo (from Google profile), timezone (auto-detected)
  • Inferred professional context: "What you do / who you are" (from email patterns)
  • Stats: "You get X emails per day, interact with N people per week, with X meetings per day"
  • Personality/humor commentary based on volume ("That's a lot!")
  • Animation showing their work life

User confirms/edits: Name, timezone, photo, professional context.

Data source: inferred payload from Screen 2 analysis.

Writes: Desired profile values in onboarding_state.


Screen 4: Phone Verification — "Text Consul"

Component: step-phone-verify.tsx (new)

Core pattern: A single "Text Consul" button that opens the user's native iMessage/SMS app with a prefilled message containing a unique verification code. No phone number input — the number is captured automatically when Consul receives the text.

Flow:

  1. Backend generates a unique verification code tied to the user's account (e.g., CONSUL-A7X9K2) via /api/phone/verify/initiate
  2. User sees a "Text Consul" button
  3. Button opens sms:+1XXXXXXXXXX&body=CONSUL-A7X9K2 (native compose with Consul's number + prefilled code)
  4. User hits send in their native messaging app
  5. Consul's messaging gateway receives the text, matches the code to the user's account, extracts the sender's phone number, and marks the phone as verified
  6. Onboarding page polls /api/phone/verify/status every 2-3 seconds
  7. When verified, step auto-advances with a brief showcase of iMessage capabilities ("Consul cleans your inbox and pings you when important items come up — actually important")

Why this works: No phone number input field needed. The act of sending the text both proves ownership of the number AND gives us the number. One button, zero form fields.

Fallback (desktop/non-mobile): Show the verification code prominently with instructions: "Open iMessage or your texting app and send this code to [Consul's number]." Include a "copy code" button. Polling still detects verification.

Key change: /api/phone/verify/confirm redirect must detect onboarding context and redirect to /start?step=4&phoneVerified=true instead of current /skills/reminders?phoneVerified=true.

Writes: profiles.phone_number (extracted from incoming text), profiles.phone_verified, agent_preferences.phone_number, onboarding_state.phone.


Screen 5: Triage — "Consul cleans your inbox"

Component: step-triage.tsx (new, borrows from triage-content.tsx)

What user sees:

  • Inbox insights: "In the last week you've gotten N marketing emails, Y emails needed response, X newsletters..."
  • Recommended triage tags shown as cards (reuse existing tag card UI)
  • Pre-checked notification toggles for high-signal tags
  • Toggle triage on/off, adjust tag selections

Tag conflict handling: Load the user's existing Gmail labels on entry to detect conflicts with recommended Consul triage tags. If a user already has labels that overlap with Consul's default set (e.g., they already have a "Newsletters" label), surface this in the UI so they can choose to merge, rename, or skip conflicting tags. This prevents label duplication and confusion post-activation.

Operational setup deferred to payment: Gmail label creation (/api/triage/setup-labels) and Gmail watch setup are not triggered during this step. They are part of the activation sequence that runs only after successful payment in Screen 11. This step only captures the user's desired triage configuration.

Writes: desiredConfig.triage in onboarding_state (selected tags, notification preferences, enabled flag). Canonical table writes deferred to activation.

Skip if: Gmail not connected.


Screen 6: Drafting — "Consul drafts responses in your voice"

Component: step-drafting.tsx (new, borrows from drafting-content.tsx)

Entry animation: The last ~50 sent emails (preloaded during Screen 2 analysis) cascade across the screen as small cards — subject lines, recipients, snippets — visually demonstrating Consul "reading" their writing style. Cards animate quickly (staggered, ~100ms apart) with a scanning/highlighting effect as they flow past, converging into the inferred style summary. This should feel like the system just consumed their entire writing history in seconds.

What user sees (after animation settles):

  • "We read your sent emails and constructed this tone" — show inferred writing style description
  • Sample email needing a reply + AI-generated draft in their voice
  • Toggle: "We recommend having the AI decide when to respond (based upon your relationships)"
  • Edit writing style, signature

Data source: Writing style from /generate-style (called in Screen 2 background), signature extracted from sent emails, email subjects/snippets cached in onboarding_state.inferred.drafting.

Writes: desiredConfig.drafting in onboarding_state.

Skip if: Gmail not connected or fewer than 5 sent emails.


Screen 7: Consul Email Identity + Scheduling — "Consul has its own email address"

Component: step-consul-identity.tsx (new)

What user sees:

  • "Generate the name for your Consul" — show generated localpart@consulmail.com with option to customize
  • "It looks like you take meetings between X & Z and are in this timezone"
  • Recommended scheduling config (working hours inferred from calendar, default duration from most common meeting length)
  • Action: "We've sent a scheduling email to the founder for product feedback" — actually sends from their new Consul EA to alton@generative.inc

Reuse: EAIdentityService.createDefaultIdentity() for EA creation, scheduling settings UI patterns from scheduling-content.tsx.

New API: POST /api/onboarding/send-scheduling-demo — sends real email from user's new EA to target address. Production-gated and rate-limited to prevent abuse. Rejects duplicate sends within same session.

Writes: ea_identities (created during this step), onboarding_state.identity, desiredConfig.scheduling.

Custom domain upsell: Below the localpart@consulmail.com generator, show a subtle card: "Want Consul on your own domain?" displaying a preview of consul@{theirdomain.com} (domain inferred from their email address). Card links to a contact/interest form or says "Ask our team about setting up Consul on your own domain." This is informational only — no action required to proceed.


Screen 8: Reminders — "Consul will send you reminders"

Component: step-reminders.tsx (new, borrows from reminders-content.tsx)

What user sees:

  • Channel configuration: iMessage (pre-enabled since phone verified in Screen 4), email toggle
  • Quiet hours: inferred from working hours (Screen 7) or default 10pm-8am
  • Example reminder shown inline

Writes: desiredConfig.reminders in onboarding_state.


Screen 9: Daily Brief — "Consul drafts a brief about your day"

Component: step-daily-brief.tsx (new, borrows from daily-brief-content.tsx)

What user sees:

  • Visual explanation of how the brief works
  • A REAL pre-generated brief (created during Screen 2 background analysis) displayed on screen
  • Configure delivery time (default 8:00 AM), delivery channel (iMessage/email)

Live demo: When the user reaches this screen, send the pre-generated brief to their verified phone number via iMessage using a unique hash/link. The user's phone buzzes with a real brief from Consul while they're looking at the screen — this is the "aha moment" for this skill. The brief contains their actual calendar events, email summary, and priorities for the day. This proves the value immediately and tangibly.

Writes: desiredConfig.dailyBrief in onboarding_state.

Prerequisite: Migration must add next_brief_at / last_sent_at columns and calculate_next_brief_at RPC. Phone must be verified (Screen 4).


Screen 10: Roadmap Vote — "What matters most to you?"

Component: step-roadmap.tsx (new)

What user sees:

  • Multi-select cards of future roadmap items
  • Freeform text box for additional requests/wishes

Writes: profiles.roadmap_votes JSONB, desiredConfig.roadmap in onboarding_state.

Purpose: Product signal collection while user is maximally engaged.


Screen 11: Paywall

Component: step-paywall.tsx (refactored from pricing-step.tsx + conversion-modal.tsx)

What user sees:

  • "We've discounted Consul 75% for life for our first customers"
  • "$50/mo" crossed out, show discounted price
  • "$200 of consumption" included
  • PAY NOW → Stripe checkout

If pay: Billing webhook fires → activation sequence runs → all configured skills go active → redirect to Screen 12.

If bounce: Set onboarding_state.status = "dormant". All skills remain inactive. iMessage agent responds with re-engagement nudge + completion link when user texts Consul.

Reuse: Stripe payment link via STRIPE_PROMO_LINK, webhook handling from /api/billing/webhook.

Key change: Billing webhook checks for onboarding_state.version === 2. If found, activates desired config instead of applying auto-defaults.


Screen 12: Launch — "Welcome to the future of executive assistance"

Component: step-launch.tsx (new)

What user sees:

  • "We've sent you an email with everything you need to know"
  • "Explore our app and download the desktop companion"
  • Links to app and desktop download

Actions on complete:

  • Send welcome email from Consul EA
  • Mark onboarding_completed = true, set onboarding_completed_at
  • onboarding_state.status = "completed"
  • Redirect to /home

12-Screen Build Contract

StepScreenEntry ConditionExit ConditionActivation
1ConnectAuthenticated, not completedOAuth successNone
2Analyzing + DiscoveryStep 1 doneAnalysis complete + survey answered/skippedNone
3ProfileInferred data loadedProfile confirmedNone
4Phone VerifyProfile confirmedphone_verified=trueNone
5TriageGmail connectedTriage config confirmedNone
6DraftingGmail connectedDrafting config confirmedNone
7Consul Identity + SchedulingPrior doneIdentity created + scheduling confirmedNone
8RemindersPrior doneReminders config confirmedNone
9Daily BriefPrior doneBrief config confirmedNone
10Roadmap VotePrior doneVote submitted/skippedNone
11PaywallPrior donePaid or bouncedActivate on paid only
12LaunchPaidFinal completeAlready active

Core Types

1type OnboardingStep = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; 2 3type OnboardingStatus = 4 | "in_progress" 5 | "awaiting_phone_verification" 6 | "awaiting_payment" 7 | "dormant" 8 | "completed"; 9 10interface InferredAnalysis { 11 profile: { 12 firstName?: string; 13 lastName?: string; 14 timezone?: string; 15 professionalContext?: string; 16 }; 17 inbox: { 18 unreadCount: number; 19 needsResponse: number; 20 newsletters: number; 21 meetingRequests: number; 22 weeklyVolume: number; 23 }; 24 relationships: { 25 activePeoplePerWeek: number; 26 topContacts: Array<{ 27 name: string; 28 email: string; 29 interactionScore: number; 30 }>; 31 }; 32 drafting: { 33 writingStyle?: string; 34 signature?: string; 35 sampleEmail?: { subject: string; from: string; body: string }; 36 sampleDraft?: string; 37 recentSentSnippets?: Array<{ subject: string; to: string; snippet: string }>; // last ~50 sent emails for Screen 6 animation 38 }; 39 scheduling: { 40 meetingsPerDay: number; 41 workingHours: { start: string; end: string; days: number[] }; 42 defaultDurationMinutes: number; 43 }; 44 brief: { 45 sampleBrief?: string; 46 generatedAt?: string; 47 }; 48} 49 50interface DesiredSkillConfig { 51 triage: { 52 enabled: boolean; 53 selectedTagIds: string[]; 54 notificationTagIds: string[]; 55 }; 56 drafting: { 57 enabled: boolean; 58 writingStyle: string; 59 defaultSignature: string; 60 autoRespondPolicy: "auto" | "manual"; 61 }; 62 scheduling: { 63 enabled: boolean; 64 workingHours: { start: string; end: string; days: number[] }; 65 defaultDurationMinutes: number; 66 minNoticeMinutes: number; 67 }; 68 reminders: { 69 allowImessage: boolean; 70 allowSms: boolean; 71 allowEmail: boolean; 72 quietHoursEnabled: boolean; 73 quietStart: string; 74 quietEnd: string; 75 }; 76 dailyBrief: { 77 enabled: boolean; 78 sendTimeLocal: string; 79 allowImessage: boolean; 80 allowEmail: boolean; 81 }; 82 roadmap: { 83 votes: string[]; 84 freeform?: string; 85 }; 86} 87 88interface OnboardingStateV2 { 89 version: 2; 90 step: OnboardingStep; 91 status: OnboardingStatus; 92 analysis: { 93 id?: string; 94 status: "idle" | "queued" | "running" | "complete" | "failed"; 95 progressPct: number; 96 errors: string[]; 97 }; 98 inferred?: InferredAnalysis; 99 desiredConfig: DesiredSkillConfig; 100 phone: { number?: string; verified: boolean }; 101 identity: { 102 eaIdentityId?: string; 103 eaEmail?: string; 104 founderDemoSent: boolean; 105 }; 106 payment: { 107 attempted: boolean; 108 paid: boolean; 109 stripeCustomerId?: string; 110 stripeSubscriptionId?: string; 111 }; 112 updatedAt: string; 113}

API Endpoints

POST /api/onboarding/analyze

Starts background analysis job after OAuth success.

  • Request: { services: string[], force?: boolean }
  • Response: { analysisId: string, status: "queued" | "running" | "complete" | "failed" }

GET /api/onboarding/analyze?analysisId=...

Polls analysis state and partial results.

  • Response: { status, progress, inferred, errors }

POST /api/onboarding/send-scheduling-demo

Sends founder demo scheduling email from user EA identity.

  • Request: { targetEmail?: string }
  • Response: { success: boolean, messageId?: string }
  • Rate-limited: 1 per session, production-gated.

POST /api/onboarding/send-brief-demo

Sends the pre-generated daily brief to the user's verified phone via iMessage during Screen 9.

  • Request: { briefContent: string } (or uses cached brief from analysis)
  • Response: { success: boolean, messageId?: string }
  • Rate-limited: 1 per session. Requires verified phone number.

POST /api/onboarding/commit

Commits desired config to canonical skill tables (called at activation time).

  • Request: { mode: "prepay" | "activate" }
  • Response: { success, appliedTables, warnings }

GET /api/onboarding/state

Returns server-resumable onboarding state from profile JSONB.

PATCH /api/onboarding/state

Patch-safe updates to onboarding state. Strict zod validation.

GET /api/phone/verify/confirm (modified)

Add onboarding-aware redirect: ?context=onboarding → return to /start?step=4&phoneVerified=true.


Architecture: Activation Model

Desired State + Reconciler

Onboarding writes desired skill configuration to onboarding_state.desiredConfig. A reconciler function computes active state by checking prerequisites. Side-effect pathways stop auto-enabling and instead trigger the reconciler.

Reconciler logic (~100 lines):

1For each skill: 2 if desired_enabled AND all_prerequisites_met → activate 3 elif desired_enabled AND NOT all_prerequisites_met → store as desired but inactive 4 elif NOT desired_enabled → deactivate

Prerequisites per skill:

SkillPrerequisites
TriageGmail connected + subscription active
DraftsGmail connected + subscription active
SchedulingCalendar connected + EA identity exists + subscription active
Reminders (iMessage)Phone verified + subscription active
Reminders (email)Gmail connected + subscription active
Daily Brief (iMessage)Phone verified + subscription active
Daily Brief (email)Gmail connected + subscription active

Called by: onboarding completion, OAuth callbacks, billing webhook, phone verification.

Detailed Activation Sequence (on payment success)

Executed idempotently:

  1. Upsert user_email_settings from desired triage/drafting config
  2. Ensure triage tags exist and apply tag selections; run Gmail label setup
  3. Upsert user_scheduling_settings
  4. Upsert reminders_preferences
  5. Upsert daily_brief_preferences and compute next_brief_at
  6. Update agent_preferences quiet hours / channel-derived values
  7. Set up Gmail watch if triage enabled
  8. Mark onboarding_state.status = "completed" and profiles.onboarding_completed = true

Before Payment Rules

  • Write only profiles, phone verification, ea_identities, and onboarding_state updates needed for progression
  • Do NOT set canonical skill *_enabled = true
  • Do NOT trigger Gmail watches or label creation

Side-Effect Cleanup Matrix

Current Side EffectSource FileCurrent BehaviorRequired Change
OAuth Gmail callback (unified)google-unified/callback/route.tsAuto-enables triage/drafts/channelsRemove auto-enable; only connect tokens + watch precheck
OAuth Gmail callback (direct)gmail/callback/route.tsAuto-enables triage/drafts/channelsSame removal
Billing webhookbilling/webhook/route.tsCalls enableGmailAIFeatures() directlyIf onboarding_state.version === 2, call activation from desired config. Else keep legacy behavior.
Phone verify confirmphone/verify/confirm/route.tsEnables iMessage prefs for subscribersStop auto-enable; only verification state updates
EA identity creationea/identities/route.tsAuto-enables scheduling_enabledRemove auto-enable; schedule only from desired config
DB triggerseed_triage_tags_for_new_user()Creates default tags on INSERTKeep (operational), but onboarding customizes tags after seeding

Critical safety: Side-effect changes are gated by onboarding_state.version === 2. Existing users without V2 onboarding state keep current auto-enable behavior.


Dormant State + Re-engagement

When a user completes all config screens but bounces at the paywall:

  1. onboarding_state.status = "dormant"
  2. All *_enabled flags remain false
  3. profiles.onboarding_completed stays false (user cannot access /home normally)
  4. If user texts Consul via iMessage, the agent checks subscription status
  5. If no subscription → respond: "Would love to help! Complete your onboarding here: [link]"
  6. Link returns them to the paywall step with all previous config preserved

Database Migrations

Migration A: Onboarding state + roadmap

  • Add profiles.onboarding_state JSONB NOT NULL DEFAULT '{}'::jsonb
  • Add profiles.roadmap_votes JSONB NOT NULL DEFAULT '[]'::jsonb
  • Add index: CREATE INDEX ON profiles ((onboarding_state->>'status')) for operational queries

Migration B: Daily brief schedule reconciliation

  • Add daily_brief_preferences.next_brief_at TIMESTAMPTZ NULL
  • Add daily_brief_preferences.last_sent_at TIMESTAMPTZ NULL
  • Create RPC: calculate_next_brief_at(p_send_time_local TEXT, p_timezone TEXT) RETURNS TIMESTAMPTZ
  • Add migration test script validating RPC DST behavior across spring/fall transitions

Migration C: Backfill and normalization

  • Backfill existing users with onboarding_state = '{"version": 2}' only when null/empty
  • Keep legacy users operational with no forced re-onboarding
  • One-time script to reconcile inconsistent daily brief rows

State Management

Dual-layer persistence:

  • Client: localStorage with 24hr expiry (for fast UI hydration)
  • Server: profiles.onboarding_state JSONB (source of truth, survives browser close)

On each step commit, client state syncs to server. On page load, server state is hydrated if localStorage is empty or stale. Server state wins on conflict.

State contains: current step, background analysis results (cached), skill configurations per step, phone verification status, EA identity created flag, payment status.


File Changes

New Files — apps/web/components/onboarding/

FileScreenNotes
step-connect.tsx1Refactored OAuth
step-analyzing.tsx2 + 2bBackground analysis + teaching + discovery survey
step-profile.tsx3Inferred profile + stats
step-phone-verify.tsx4iMessage verification
step-triage.tsx5Inbox insights + tag config
step-drafting.tsx6Writing style + draft policy
step-consul-identity.tsx7EA creation + scheduling
step-reminders.tsx8Channels + quiet hours
step-daily-brief.tsx9Show brief + config
step-roadmap.tsx10Roadmap voting
step-paywall.tsx11Pricing + payment
step-launch.tsx12Welcome + redirect

New Files — apps/web/app/api/onboarding/

FilePurpose
analyze/route.tsBackground analysis orchestrator (POST to start, GET to poll)
send-scheduling-demo/route.tsDemo scheduling email sender
send-brief-demo/route.tsSend pre-generated brief to user's phone via iMessage
commit/route.tsDesired config → canonical table writer
state/route.tsGET/PATCH server-side onboarding state

Modified Files

FileChange
apps/web/app/start/page.tsxRewrite as 12-screen orchestrator with server-side state
apps/web/components/onboarding/types.tsExpanded types (OnboardingStep 1-12, all interfaces above)
apps/web/app/start/actions.tsBatch-write all skill tables on activation
apps/web/components/onboarding/progress-indicator.tsx12-step progress with contextual labels
apps/web/components/onboarding/step-layout.tsxLayout variants (narrative, config panel, paywall)
apps/web/app/api/phone/verify/confirm/route.tsOnboarding-aware redirect
apps/web/app/api/billing/webhook/route.tsActivate desired state for V2 users
apps/web/app/api/auth/google-unified/callback/route.tsRemove auto-enable for V2 users
apps/web/app/api/auth/gmail/callback/route.tsRemove auto-enable for V2 users
apps/web/app/api/ea/identities/route.tsRemove auto-enable scheduling
apps/web/components/onboarding/index.tsExport new components

Deprecated/Removed Files

FileReason
step-value-preview.tsxReplaced by per-skill screens
step-aha-moment.tsxMerged into per-skill screens
step-google-connect.tsxRefactored into step-connect.tsx
step-personalize.tsxRefactored into step-profile.tsx
discovery-survey.tsxMoved inline to Screen 2b

Implementation Phases

Phase 0: Analytics + Event Schema (1 day)

  • Add analytics event schema before feature work (step viewed, completed, skipped, etc.)
  • Done when: Event contracts defined and ready for instrumentation during build

Phase 1: Schema + Core Contracts (2-3 days)

  • Ship migrations A/B/C
  • Add shared TypeScript contracts for onboarding state and analysis payloads
  • Add strict zod validation for state patch operations
  • Done when: DB + types are stable; no runtime references to missing fields/RPC

Phase 2: Analysis Pipeline (4-5 days)

  • Build analyze job orchestrator with polling API
  • Parallel tasks: inbox stats, writing style, calendar patterns, signature extraction, brief pre-generation, relationship summary
  • Persist partial progress in onboarding_state.analysis
  • Add retry/backoff and partial-failure strategy
  • Done when: Screen 2 can show progressive completion and final inferred payload

Phase 3: Flow Orchestrator + Shared UI (4 days)

  • Rewrite start/page.tsx for 12 steps
  • Server-sync state hydration: local state mirrors profiles.onboarding_state on each step commit
  • Implement shared step shell variants: narrative, config panel, paywall
  • Add skip and prerequisite guards per step
  • Done when: Deterministic step transitions and refresh-safe resume

Phase 4: Screens 1-4 (5-6 days)

  • Connect screen with service matrix
  • Analyzing screen with inline discovery survey
  • Inferred profile screen with editable fields
  • Phone verify screen with prefilled iMessage button pattern + polling + onboarding-aware confirm redirect
  • Done when: User reaches skill configuration with connected services + verified phone

Phase 5: Screens 5-10 (6-8 days)

  • Triage setup with recommended tags, notification choices, and existing label conflict detection
  • Drafting setup with sent email card animation, inferred style/signature, and sample draft
  • Identity + scheduling with custom domain upsell card and founder demo email API
  • Reminders with quiet hours inference
  • Daily brief with pre-generated brief preview + live iMessage send demo
  • Roadmap voting with freeform capture
  • Done when: Full desired config captured and validated pre-payment

Phase 6: Paywall, Activation, Dormant, Launch (5-6 days)

  • Hard paywall step and status transitions
  • Activation path: apply desired config to canonical tables on checkout
  • Modify billing webhook for V2 users
  • Dormant routing rule in messaging pipeline for unpaid users
  • Launch screen with welcome send and completion mark
  • Done when: Paid users active, unpaid users dormant with safe messaging behavior

Phase 7: QA + Ship (3-4 days)

  • Full test matrix and regression fixes
  • Internal dogfood with team
  • Ship and replace V1
  • Done when: Conversion and reliability targets met, V1 components removed

Total estimate: ~28-35 days of engineering work across phases.


Analytics + Monitoring

Events to Emit

onboarding_v2_step_viewed, step_completed, step_skipped, analysis_started, analysis_completed, analysis_failed, paywall_viewed, checkout_started, checkout_completed, dormant_entered, dormant_reengagement_clicked

Dashboard Metrics

  • Conversion rate by step (funnel)
  • Analysis latency p50/p95
  • Paywall conversion rate
  • Activation success rate
  • Dormant re-engagement clickthrough

Alert Thresholds

  • Analysis failure rate > 10%
  • Activation failure rate > 2%
  • Billing webhook apply failure > 1%

Test Matrix

TestTypeAsserts
Fresh user completes all 12 steps + paysE2EAll selected skills activate exactly once
Fresh user completes all, bounces at paywallE2EAll skills inactive, dormant message returned on iMessage
User refreshes on every stepE2EState resumes correctly from server
User closes browser after step 6, returns laterE2EResumes at step 6 with inferred cache intact
Gmail only connected (no Calendar)E2EScheduling screen handles gracefully
Calendar only connected (no Gmail)E2ETriage/drafting skip behavior works
Phone verification link returns to onboardingIntegrationAdvances step correctly
Phone verification outside onboardingIntegrationNon-onboarding redirect still works
Analysis partial failure (style service down)IntegrationProgression with fallback defaults
Founder demo email rate limitingIntegrationRejects duplicate sends in same session
Billing webhook duplicate Stripe eventsIntegrationActivation remains idempotent
Legacy users without onboarding_stateIntegrationNormal behavior preserved
Reconciler: all prerequisites metUnitSkills activated, operational setup triggered
Reconciler: partial prerequisitesUnitOnly eligible skills activated
Reconciler: subscription canceledUnitAll skills deactivated
OAuth callback post-refactorIntegrationDoes NOT auto-enable, does trigger reconciler
Billing webhook post-refactorIntegrationDoes NOT call enableGmailAIFeatures for V2
LocalStorage corruptionEdge caseServer-resume state takes over
Daily brief next_brief_at across DSTUnitCorrect calculation for spring/fall transitions
Onboarding state patch validationSecurityRejects invalid shape via zod

Risks + Mitigations

RiskLikelihoodImpactMitigation
iMessage deep-link fails on non-AppleHighMediumPlatform detection → SMS fallback or manual code entry
Background inference takes >30sMediumMediumProgressive loading (email stats first, style last), timeout with defaults
Reconciler introduces activation bugs for existing usersMediumHighOnly apply reconciler for users with onboarding_state.version === 2; existing users keep current pathways
Stripe checkout abandonment increases (paywall in onboarding)MediumHighTrack funnel metrics, keep ConversionModal as fallback
OAuth re-auth stops enabling features for existing usersMediumHighOnly apply pathway changes for V2 users (check flag)
Demo scheduling email creates real founder expectationLowMediumRate-limit, consider preview/draft mode
Daily brief DB migration failsLowHighTest on staging first, validate RPC DST behavior

Open Questions Requiring Decision

  1. Pricing numbers: Spec says "75% off 50/mo."CurrentPricingCardshows"50/mo." Current `PricingCard` shows "25/mo (50% off $50)." Which is correct? This determines Stripe configuration.

  2. iMessage fallback for non-Apple: (a) SMS-only fallback, (b) email verification, (c) skip phone verification entirely if not on Apple device?

  3. "Desktop companion": Referenced in Screen 12 but no companion app exists in codebase. Is this a planned future feature to mention, or cut from flow?

  4. Demo scheduling email: (a) Actually send (proves capability, creates real expectation), (b) Show preview/draft only, (c) Send only after payment?

  5. Dormant aggressiveness: (a) Hard block — Consul refuses ALL interactions until paid, (b) Soft nudge — helps with 1-2 things then prompts, (c) Time-limited trial (24-48 hrs)?

  6. Existing user re-onboarding: If an existing user re-visits /start, should the new flow handle pre-existing settings? Or strictly new signups only?


Non-Goals

  • Replacing skill settings pages — /skills/* pages remain for post-onboarding changes
  • Building a desktop companion app — out of scope
  • Annual pricing toggle — not part of this change
  • Migrating existing users through new onboarding — new signups only
  • Changing agent architecture — tools, workflows, memory patterns stay the same
Narrative Skill-Configuring Onboarding V2 — Unified Plan | MDX Limo