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:
| Pathway | File | Auto-Enables |
|---|---|---|
| Gmail OAuth (unified) | app/api/auth/google-unified/callback/route.ts | Triage, drafts, email reminders, email daily brief (if subscribed) |
| Gmail OAuth (direct) | app/api/auth/gmail/callback/route.ts | Same as above |
| Billing webhook | app/api/billing/webhook/route.ts | Triage, drafts, email reminders, email daily brief, Gmail watches |
| Phone verification | app/api/phone/verify/confirm/route.ts | iMessage reminders, iMessage daily brief (if subscribed) |
| EA identity creation | app/api/ea/identities/route.ts | scheduling_enabled=true |
| DB trigger | seed_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_stateJSONB 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)
2 ↓
32. Analyzing You (background fetch + teach about Consul) → 2b. Discovery Survey
4 ↓
53. Your Profile (inferred identity + stats)
6 ↓
74. Phone Verification (Text Consul via iMessage)
8 ↓
95. Triage (inbox insights + tag setup)
10 ↓
116. Drafting (writing style + auto-draft policy)
12 ↓
137. Consul Email Identity (EA name + scheduling config)
14 ↓
158. Reminders (channels + quiet hours)
16 ↓
179. Daily Brief (real generated brief + config)
18 ↓
1910. Roadmap Vote (feature priority + freeform)
20 ↓
2111. 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-styleon 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:
- Backend generates a unique verification code tied to the user's account (e.g.,
CONSUL-A7X9K2) via/api/phone/verify/initiate - User sees a "Text Consul" button
- Button opens
sms:+1XXXXXXXXXX&body=CONSUL-A7X9K2(native compose with Consul's number + prefilled code) - User hits send in their native messaging app
- 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
- Onboarding page polls
/api/phone/verify/statusevery 2-3 seconds - 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.comwith 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, setonboarding_completed_at onboarding_state.status = "completed"- Redirect to
/home
12-Screen Build Contract
| Step | Screen | Entry Condition | Exit Condition | Activation |
|---|---|---|---|---|
| 1 | Connect | Authenticated, not completed | OAuth success | None |
| 2 | Analyzing + Discovery | Step 1 done | Analysis complete + survey answered/skipped | None |
| 3 | Profile | Inferred data loaded | Profile confirmed | None |
| 4 | Phone Verify | Profile confirmed | phone_verified=true | None |
| 5 | Triage | Gmail connected | Triage config confirmed | None |
| 6 | Drafting | Gmail connected | Drafting config confirmed | None |
| 7 | Consul Identity + Scheduling | Prior done | Identity created + scheduling confirmed | None |
| 8 | Reminders | Prior done | Reminders config confirmed | None |
| 9 | Daily Brief | Prior done | Brief config confirmed | None |
| 10 | Roadmap Vote | Prior done | Vote submitted/skipped | None |
| 11 | Paywall | Prior done | Paid or bounced | Activate on paid only |
| 12 | Launch | Paid | Final complete | Already 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 → deactivatePrerequisites per skill:
| Skill | Prerequisites |
|---|---|
| Triage | Gmail connected + subscription active |
| Drafts | Gmail connected + subscription active |
| Scheduling | Calendar 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:
- Upsert
user_email_settingsfrom desired triage/drafting config - Ensure triage tags exist and apply tag selections; run Gmail label setup
- Upsert
user_scheduling_settings - Upsert
reminders_preferences - Upsert
daily_brief_preferencesand computenext_brief_at - Update
agent_preferencesquiet hours / channel-derived values - Set up Gmail watch if triage enabled
- Mark
onboarding_state.status = "completed"andprofiles.onboarding_completed = true
Before Payment Rules
- Write only
profiles, phone verification,ea_identities, andonboarding_stateupdates needed for progression - Do NOT set canonical skill
*_enabled = true - Do NOT trigger Gmail watches or label creation
Side-Effect Cleanup Matrix
| Current Side Effect | Source File | Current Behavior | Required Change |
|---|---|---|---|
| OAuth Gmail callback (unified) | google-unified/callback/route.ts | Auto-enables triage/drafts/channels | Remove auto-enable; only connect tokens + watch precheck |
| OAuth Gmail callback (direct) | gmail/callback/route.ts | Auto-enables triage/drafts/channels | Same removal |
| Billing webhook | billing/webhook/route.ts | Calls enableGmailAIFeatures() directly | If onboarding_state.version === 2, call activation from desired config. Else keep legacy behavior. |
| Phone verify confirm | phone/verify/confirm/route.ts | Enables iMessage prefs for subscribers | Stop auto-enable; only verification state updates |
| EA identity creation | ea/identities/route.ts | Auto-enables scheduling_enabled | Remove auto-enable; schedule only from desired config |
| DB trigger | seed_triage_tags_for_new_user() | Creates default tags on INSERT | Keep (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:
onboarding_state.status = "dormant"- All
*_enabledflags remain false profiles.onboarding_completedstays false (user cannot access/homenormally)- If user texts Consul via iMessage, the agent checks subscription status
- If no subscription → respond: "Would love to help! Complete your onboarding here: [link]"
- 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_stateJSONB (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/
| File | Screen | Notes |
|---|---|---|
step-connect.tsx | 1 | Refactored OAuth |
step-analyzing.tsx | 2 + 2b | Background analysis + teaching + discovery survey |
step-profile.tsx | 3 | Inferred profile + stats |
step-phone-verify.tsx | 4 | iMessage verification |
step-triage.tsx | 5 | Inbox insights + tag config |
step-drafting.tsx | 6 | Writing style + draft policy |
step-consul-identity.tsx | 7 | EA creation + scheduling |
step-reminders.tsx | 8 | Channels + quiet hours |
step-daily-brief.tsx | 9 | Show brief + config |
step-roadmap.tsx | 10 | Roadmap voting |
step-paywall.tsx | 11 | Pricing + payment |
step-launch.tsx | 12 | Welcome + redirect |
New Files — apps/web/app/api/onboarding/
| File | Purpose |
|---|---|
analyze/route.ts | Background analysis orchestrator (POST to start, GET to poll) |
send-scheduling-demo/route.ts | Demo scheduling email sender |
send-brief-demo/route.ts | Send pre-generated brief to user's phone via iMessage |
commit/route.ts | Desired config → canonical table writer |
state/route.ts | GET/PATCH server-side onboarding state |
Modified Files
| File | Change |
|---|---|
apps/web/app/start/page.tsx | Rewrite as 12-screen orchestrator with server-side state |
apps/web/components/onboarding/types.ts | Expanded types (OnboardingStep 1-12, all interfaces above) |
apps/web/app/start/actions.ts | Batch-write all skill tables on activation |
apps/web/components/onboarding/progress-indicator.tsx | 12-step progress with contextual labels |
apps/web/components/onboarding/step-layout.tsx | Layout variants (narrative, config panel, paywall) |
apps/web/app/api/phone/verify/confirm/route.ts | Onboarding-aware redirect |
apps/web/app/api/billing/webhook/route.ts | Activate desired state for V2 users |
apps/web/app/api/auth/google-unified/callback/route.ts | Remove auto-enable for V2 users |
apps/web/app/api/auth/gmail/callback/route.ts | Remove auto-enable for V2 users |
apps/web/app/api/ea/identities/route.ts | Remove auto-enable scheduling |
apps/web/components/onboarding/index.ts | Export new components |
Deprecated/Removed Files
| File | Reason |
|---|---|
step-value-preview.tsx | Replaced by per-skill screens |
step-aha-moment.tsx | Merged into per-skill screens |
step-google-connect.tsx | Refactored into step-connect.tsx |
step-personalize.tsx | Refactored into step-profile.tsx |
discovery-survey.tsx | Moved 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.tsxfor 12 steps - Server-sync state hydration: local state mirrors
profiles.onboarding_stateon 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
| Test | Type | Asserts |
|---|---|---|
| Fresh user completes all 12 steps + pays | E2E | All selected skills activate exactly once |
| Fresh user completes all, bounces at paywall | E2E | All skills inactive, dormant message returned on iMessage |
| User refreshes on every step | E2E | State resumes correctly from server |
| User closes browser after step 6, returns later | E2E | Resumes at step 6 with inferred cache intact |
| Gmail only connected (no Calendar) | E2E | Scheduling screen handles gracefully |
| Calendar only connected (no Gmail) | E2E | Triage/drafting skip behavior works |
| Phone verification link returns to onboarding | Integration | Advances step correctly |
| Phone verification outside onboarding | Integration | Non-onboarding redirect still works |
| Analysis partial failure (style service down) | Integration | Progression with fallback defaults |
| Founder demo email rate limiting | Integration | Rejects duplicate sends in same session |
| Billing webhook duplicate Stripe events | Integration | Activation remains idempotent |
Legacy users without onboarding_state | Integration | Normal behavior preserved |
| Reconciler: all prerequisites met | Unit | Skills activated, operational setup triggered |
| Reconciler: partial prerequisites | Unit | Only eligible skills activated |
| Reconciler: subscription canceled | Unit | All skills deactivated |
| OAuth callback post-refactor | Integration | Does NOT auto-enable, does trigger reconciler |
| Billing webhook post-refactor | Integration | Does NOT call enableGmailAIFeatures for V2 |
| LocalStorage corruption | Edge case | Server-resume state takes over |
Daily brief next_brief_at across DST | Unit | Correct calculation for spring/fall transitions |
| Onboarding state patch validation | Security | Rejects invalid shape via zod |
Risks + Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| iMessage deep-link fails on non-Apple | High | Medium | Platform detection → SMS fallback or manual code entry |
| Background inference takes >30s | Medium | Medium | Progressive loading (email stats first, style last), timeout with defaults |
| Reconciler introduces activation bugs for existing users | Medium | High | Only apply reconciler for users with onboarding_state.version === 2; existing users keep current pathways |
| Stripe checkout abandonment increases (paywall in onboarding) | Medium | High | Track funnel metrics, keep ConversionModal as fallback |
| OAuth re-auth stops enabling features for existing users | Medium | High | Only apply pathway changes for V2 users (check flag) |
| Demo scheduling email creates real founder expectation | Low | Medium | Rate-limit, consider preview/draft mode |
| Daily brief DB migration fails | Low | High | Test on staging first, validate RPC DST behavior |
Open Questions Requiring Decision
-
Pricing numbers: Spec says "75% off 25/mo (50% off $50)." Which is correct? This determines Stripe configuration.
-
iMessage fallback for non-Apple: (a) SMS-only fallback, (b) email verification, (c) skip phone verification entirely if not on Apple device?
-
"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?
-
Demo scheduling email: (a) Actually send (proves capability, creates real expectation), (b) Show preview/draft only, (c) Send only after payment?
-
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)?
-
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