Journeys · the user lens Six docs · start with the Overview. GTM names it · Dashboard measures it · Journeys shows it · Foundation builds it · Quality verifies it.
Customer journeys Marco Avila · 2026-05-15 · post-pull audit

Where users drop.
And what each drop costs the GTM.

Six journeys, mapped step by step against the actual code. The reported "I have to sign in twice" and "it's slow" symptoms trace to specific lines. Every dropped journey is a 5/1/3 activation that doesn't happen — and every pilot user (Indelible warm-circle first, TAI second) we lose is a sponsorship dollar we don't earn.

Specific bugs / friction
18
Each with file:line
Concrete recommendations
24
Each with effort + rationale
Total fix budget
~51h
For all journeys combined
Executive summary 6 bullets · 60 seconds
  1. The "sign in twice" report is a real bug with a specific root cause: prompt: 'login' in the Auth0 config forces re-authentication every time, even with a valid refresh token. One-line fix.
  2. "It's slow" is 14–20 sequential roundtrips on iOS first sign-in, including 5–7 uncached Auth0 Management API calls. ~1.5–4 seconds before the user sees anything. Caching the management token alone saves ~700ms.
  3. A second "sign in twice" bug is buried in CompletePhase: the onboarding-finished signal is fire-and-forget. If the GQL call fails or the app is backgrounded mid-call, the user redoes onboarding on the next session.
  4. Welcome screen has a 1400ms intentional animation delay before the Get Started button appears. Combined with the cold-start latency above, the first 3 seconds of every brand-new install are dead air.
  5. Every dropped journey costs a magic-number activation. The GTM measures users who hit 5 events + 1 magic moment + 3 returns in 14 days — starting with the Indelible Ventures warm-circle pilot, before the Tokyo beachhead opens. Every sign-in friction and onboarding bug halves the funnel into that metric.
  6. All 24 recommendations together are ~51 hours. Most are 1–3 hour fixes. The "sign in twice" primary cause is a one-line config change.

01 Magic numbers

What every journey is racing toward.

From the GTM (§05): 5 events · 1 magic moment · 3 return visits · in 14 days. The activation contract. Every dropped journey below is a user who didn't reach it.

5
Events

Five meaningful actions in the product — message, task, meeting prep, agent ask, calendar interaction. Each journey on this page is one of those events.

3
Return visits

Three sessions across 14 days. Below the threshold, the user won't form habit. Notifications and the unified inbox carry this load — see Foundation.

Why this matters for the journeys below. The TAI pilot's hybrid venue sponsorship pays per actual user join, not per seat. A user who can't sign in, gets stuck in onboarding, or hits an empty channel doesn't count toward the magic number — and doesn't count toward the sponsorship invoice. The fixes on this page are the difference between a pilot that pays back and one that doesn't.


02 Journey 1

First sign-in

Brand-new user opening the app for the first time

Today
1.5–4 s
14–20 blocking roundtrips
After recommendations
<1 s
6–8 roundtrips
Magic-number tie

Mobile users abandon pages taking >3s at 53% (Google). Today the iOS cold-start sits squarely in that range.

Today 4.8s
Recommended 1.9s
Saves 2.9s
  1. 01
    Tap Sign in
    user
    instant
    unchanged
  2. 02
    Auth0 universal login
    loginWithRedirect / promptAsync
    Auth0
    600ms
    600ms
  3. 03
    PKCE code exchange
    client
    200ms
    200ms
  4. 04
    GET /userinfo
    Auth0
    300ms
    300ms
  5. 05
    100ms safety buffer
    AuthContext.tsx:401 — admitted workaround
    wait friction
    100ms
    removed
    remove

    Delete — comment admits it 'may work without it'.

    at AuthContext.tsx:399–405

  6. 06
    signInUser mutation
    GraphQL call to Laravel
    server
    200ms
    200ms
  7. 07
    POST /oauth/token (Mgmt API)
    iOS Apple Sign-In: token has no email claim
    Auth0 blocker
    500ms
    30ms
    cache

    Cache management token (24h TTL).

    at SignInUserMutation.php:94–123

  8. 08
    GET /api/v2/users/{id}
    Fetch email from Auth0 Management API
    Auth0 blocker
    400ms
    80ms
    cache

    Cache email lookup keyed on auth0_id.

    at SignInUserMutation.php:425–434

  9. 09
    Auth0 role + permissions
    3 more Mgmt API calls, all sequential
    Auth0 blocker
    900ms
    250ms
    parallel

    Parallelize + cache role/perm fetches.

    at Auth0RoleService.php:36–54

  10. 10
    Refresh permissions cache
    New Auth0 SDK instance + paginated fetch
    Auth0 friction
    600ms
    50ms
    cache

    Reuse SDK instance + cache page.

    at RefreshAuth0PermissionsService.php:21–55

  11. 11
    ensureUserHasPersonalPulses
    N+1 DB queries inside the request cycle
    db friction
    400ms
    instant
    background

    Dispatch as background job — not blocking.

    at SignInUserMutation.php:585–605

  12. 12
    getMeQuery
    Redundant — signInUser already returned the user
    server friction
    250ms
    removed
    remove

    Delete — signInUser already returns the user.

    at AuthContext.tsx:286–342

  13. 13
    Bootstrap org query
    server
    200ms
    200ms
  14. 14
    Bootstrap settings query
    Serial: needs org id first
    server friction
    200ms
    instant
    parallel

    Run in parallel with bootorg.

    at useBootstrapOrg.ts:116–194

  15. 15
    Onboarding mounts
    client
    instant
    unchanged
Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

"Native iOS keeps kicking users out — but PWA stays signed in"

PWA uses Auth0's `@auth0/auth0-react` SDK with `cacheLocation: 'localstorage'` and silent SDK-managed refresh — robust. Native iOS (Nova/Capacitor) reimplements the entire flow manually: `expo-auth-session` for the PKCE handshake, `expo-secure-store` (Keychain) for tokens, and a manual `POST /oauth/token` for refresh. Three known failure modes here: (a) Keychain access can throw on cold-start before the secure enclave is unlocked, returning null and triggering re-login; (b) the refresh-token POST has no retry on 5xx — a single network blip drops the user; (c) there's no AppState listener to refresh tokens when the app foregrounds after a long background. The PWA never hits any of this because the Auth0 SDK handles it. The fix is not to harden three different bugs — it's to consolidate native iOS onto the same Auth0 SDK behavior.

Found in nova/src/contexts/AuthContext.tsx (manual restore + refresh) · expo-secure-store usage · no AppState listener for token refresh
02

"Sometimes I have to sign in twice" — primary cause

`prompt: 'login'` is hardcoded in the Auth0 config, which forces Auth0 to ignore any cached SSO session and show the login screen even when a valid refresh token exists. A returning user with a fresh device session still gets the login prompt.

Found in AuthContext.tsx:88 (web) · nova/src/contexts/AuthContext.tsx:314 (mobile)
03

"Sign in twice" — secondary cause

`isInitialized` resets to false whenever Auth0 SDK briefly toggles `isAuthenticated` (which happens during silent refresh). The auth-flow effect then re-runs `signInUserMutation` + `getMeQuery` — a second full sign-in roundtrip.

Found in AuthContext.tsx:417–426
04

Silent redirect-to-login on transient error

`getToken` retries `getAccessTokenSilently` once with `cacheMode: 'off'`; if that also fails (network blip, 429), it calls `loginWithRedirect` with no warning. Indistinguishable from session expiry by the user.

Found in AuthContext.tsx:218–247
05

Management API token not cached

`fetchEmailFromAuth0` and `Auth0RoleService::addRole` independently fetch fresh Management API tokens on every login, even within the same request. The token has a 24-hour TTL — caching saves 2+ HTTP calls per login.

Found in SignInUserMutation.php:425–434 · Auth0RoleService.php:24–32

Recommendations

01

Consolidate native iOS auth onto a single SDK path

Why: PWA works because Auth0's React SDK handles token storage, silent refresh, and AppState transitions correctly. Native iOS reimplements all three manually and gets each one slightly wrong. Either (a) move Nova to Capacitor + Auth0 SDK for parity, or (b) wrap the manual flow with: AppState listener → silent refresh on foreground; retry-with-backoff on the refresh POST; deferred Keychain reads gated on `isReady`. The Keychain timing race is community-tracked at expo issue #23924. Pilot-blocker because it's the bug users feel most.

1 day (option b) · 2–3 days (option a)
Where to fix services/nova/src/contexts/AuthContext.tsx (restore + refresh) · expo-secure-store calls · add AppState.addEventListener
02

Drop `prompt: 'login'`

Why: Returning users with valid refresh tokens should not see Auth0's login screen. This is the single biggest fix for the "sign in twice" report. Auth0 also recommends validating the auth_time claim to confirm freshness rather than relying on the prompt parameter alone.

1 hr
Where to fix lib/zunou-react/contexts/AuthContext.tsx:88 · nova/src/contexts/AuthContext.tsx:314
03

Cache Auth0 Management API token

Why: 24-hour TTL. Saves 2 HTTP calls per login (~700ms on iOS).

2 hr
Where to fix services/api/app/Services/Auth0RoleService.php:24–32 · SignInUserMutation.php:425–434
04

Make `ensureUserHasPersonalPulses` a background job

Why: Today it blocks the sign-in response with N+1 DB queries. The user's first session doesn't need pulses provisioned synchronously — the bootstrap query fetches them on demand. Tracked in Foundation Tier-1 as part of the notification-outbox + queueing rebuild.

3 hr
Where to fix services/api/app/GraphQL/Mutations/SignInUserMutation.php:407,585–605 → new Job class
05

Remove the redundant `getMeQuery`

Why: `signInUserMutation` already returns the user. The second call is dead weight — saves a full roundtrip.

30 min
Where to fix lib/zunou-react/contexts/AuthContext.tsx:286–342
06

Don't reset `isInitialized` on every `auth0IsAuthenticated` flip

Why: Use a one-shot ref to gate `handleAuthFlow`. Eliminates the double sign-in.

2 hr
Where to fix lib/zunou-react/contexts/AuthContext.tsx:417–426
07

Replace `loginWithRedirect` fallback with a typed retry toast

Why: Transient errors should surface a 'try again' UI, not silently bounce the user back through Auth0.

3 hr
Where to fix lib/zunou-react/contexts/AuthContext.tsx:218–247
08

Remove the 100ms safety buffer

Why: The comment already admits it 'may work without it.' Test and delete.

30 min
Where to fix lib/zunou-react/contexts/AuthContext.tsx:399–405
03 Journey 2

Onboarding (post sign-in)

First-time user after sign-in completes

Today
60–120 s
4–6 phases · 6–10 taps
After recommendations
30–60 s
3–5 phases · 5–7 taps
Magic-number tie

Onboarding's job is to deliver the first 5 events + 1 magic moment in this session.

Today 2.6s
Recommended 1.3s
Saves 1.3s
  1. 01
    Welcome
    1400ms animation delay before button appears
    user friction
    1.4s
    instant
    improve

    Show button instantly; play animation in parallel.

    at WelcomePhase.tsx (animation delay)

  2. 02
    Create org
    Required text field, blocking GQL mutation
    server
    800ms
    800ms
  3. 03
    Calendar (optional)
    Auto-skip flashes if already linked
    user friction
    400ms
    200ms
    improve

    Skip-and-fade UI for already-linked.

    at CalendarPhase.tsx:36–38

  4. 04
    Choose tabs
    user
    instant
    unchanged
  5. 05
    Choose layout
    user
    instant
    unchanged
  6. 06
    Meet the agent
    user
    instant
    unchanged
  7. 07
    Complete
    Fire-and-forget settings save — see bugs
    server blocker
    instant
    300ms
    improve

    Make blocking before navigation (+200ms for durability).

    at CompletePhase.tsx:191–215

Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

"Sign in twice" — true root cause (onboarding-side)

`CompletePhase.handleFinish` fires `upsertNovaSettingMetadata` as fire-and-forget before navigating to `/(app)`. If the app is backgrounded/killed in the sub-second window before the GQL mutation returns, the backend `onboardedMobile` flag never gets set. On next cold start (or iOS AsyncStorage wipe), `useBootstrapOrg` finds no `onboardedMobile=true`, `useOnboardingStore` reads `completed=false` from blank storage, and the user is sent back to /onboarding. User experience: 'I have to sign in again.'

Found in CompletePhase.tsx:191–215 · useBootstrapOrg.ts:225–227
02

Race between create-org and bootstrap-org

After `createOrganization` resolves, the phase rebuilds itself and bumps the index. But `useBootstrapOrg` may still be running in parallel — when it lands, it calls `setOrg` which clears `pulseId`. Result: a flash of empty state.

Found in CreateOrgPhase.tsx:146–153
03

1400ms intentional delay on welcome screen

Button doesn't appear for 1.4 seconds. No skip. Compounds the slow-sign-in pain.

Found in WelcomePhase.tsx (animation delay)
04

Calendar auto-skip flash

If Google Calendar is already linked, the phase calls `nextPhase()` from a `useEffect` synchronously on mount. The 350ms cross-fade can't mask the single-frame flash of an empty screen.

Found in CalendarPhase.tsx:36–38
05

No idempotency on org creation if user re-enters

If user kills the app mid-onboarding after createOrg succeeded but before completion, restart re-shows create-org. A second submit creates a duplicate org (no client-side or server-side de-dupe).

Found in CreateOrgPhase.tsx

Recommendations

01

Make `upsertNovaSettingMetadata` blocking before navigation

Why: Eliminates the #1 user-reported bug. The user already waited through onboarding — 200ms more for durability is the right trade.

1 hr
Where to fix services/nova/src/components/onboarding/phases/CompletePhase.tsx:191–215
02

Server-side `onboardedAt` as source of truth

Why: Treat AsyncStorage as a cache; trust the backend. Cleanly recovers when iOS wipes AsyncStorage.

3 hr
Where to fix services/nova/src/hooks/useBootstrapOrg.ts:225–227 · settings.metadata schema
03

Kill the 1400ms welcome delay

Why: Animations should not gate input. Show the button immediately; play the animation in parallel.

30 min
Where to fix services/nova/src/components/onboarding/phases/WelcomePhase.tsx (animation/timeout)
04

Skip-and-fade for already-linked calendar

Why: Render the success state for 400ms with a 'Calendar already connected · →' affordance instead of vanishing.

1 hr
Where to fix services/nova/src/components/onboarding/phases/CalendarPhase.tsx:36–38
05

Idempotency key on createOrganization

Why: Client generates a UUID per onboarding session; backend dedupes. Prevents double-org bugs on app restart mid-flow.

2 hr
Where to fix services/nova/src/components/onboarding/phases/CreateOrgPhase.tsx:120–158 · CreateOrganizationMutation.php
04 Journey 3

Calendar setup (Google OAuth)

User connecting Google Calendar for first time

Today
15–40 s
5–7 taps (with Google account picker)
After recommendations
8–15 s
3–4 taps
Magic-number tie

Without calendar, the user sees an empty Schedule tab — the magic moment that Granola wins on.

Today 13s
Recommended 13s
  1. 01
    Tap 'Connect calendar'
    user
    instant
    unchanged
  2. 02
    ASWebAuthenticationSession opens
    iOS ephemeral session — no Safari cookie sharing
    client friction
    800ms
    800ms

    iOS platform limit — can't avoid.

  3. 03
    Google account picker
    Must type password even if signed in to Safari
    user friction
    8.0s
    8.0s

    User-controlled — out of our hands.

  4. 04
    Grant calendar scopes
    user
    3.0s
    3.0s
  5. 05
    PKCE code exchange
    client
    300ms
    300ms
  6. 06
    linkGoogleCalendar mutation
    Backend stores secondary access token
    server
    400ms
    400ms
    improve

    Enforce account match — reject mismatch with retry UI.

  7. 07
    syncCalendar (fire-and-forget)
    server
    instant
    unchanged
  8. 08
    Tap 'Continue'
    User must confirm after success — could auto-advance
    user friction
    instant
    removed
    remove

    Auto-advance after 1.5s success state.

Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

Wrong Google account selected silently links wrong calendar

`login_hint: user?.email` pre-fills but does not enforce. If a different Google account is selected, `linkGoogleCalendar` may succeed with the wrong identity — and there's no UI feedback flagging the mismatch.

Found in useLinkedAccounts.ts:117–139 (login_hint at :135)
02

iOS users retype Google password every link

ASWebAuthenticationSession creates an ephemeral session that doesn't share Safari cookies. Even users signed into Google in Safari must reauthenticate.

Found in expo-auth-session AuthRequest.promptAsync (iOS implementation)
03

Extra tap after success

After linkGoogleCalendar resolves and shows the 'Calendar Connected!' state, user must tap Continue to advance. Could auto-advance with a 1.5s success state.

Found in CalendarPhase.tsx

Recommendations

01

Auto-advance after successful link

Why: Show success state for 1.5s with a countdown indicator, then advance. Saves a tap and feels modern.

1 hr
Where to fix services/nova/src/components/onboarding/phases/CalendarPhase.tsx (success block)
02

Enforce account match on success

Why: Compare granted Google email vs auth user email; if mismatch, surface a clear error with retry. Prevents silent wrong-account linking.

2 hr
Where to fix services/nova/src/hooks/useLinkedAccounts.ts:117–139 (login_hint at :135)
03

Server-side calendar prefetch

Why: Start fetching events the moment OAuth succeeds, in parallel with the user dismissing the success screen. First Schedule tab paint is instant.

3 hr
Where to fix services/api/app/Jobs/FetchUserGoogleCalendarEventsJob.php · trigger after linkGoogleCalendar mutation
05 Journey 4

First message in a channel

Newly-onboarded user wanting to message someone

Today
<10 s
3 taps if personal pulse exists
After recommendations
<8 s
2–3 taps
Magic-number tie

Sending a first message = +1 event toward the 5/1/3 magic number.

Today 500ms
Recommended 300ms
Saves 200ms
  1. 01
    Tap 'Explore Zunou'
    user
    instant
    unchanged
  2. 02
    Land on Home (Feed)
    client
    200ms
    200ms
  3. 03
    Tap Chats
    user
    instant
    unchanged
  4. 04
    Channel list loads
    Personal pulse pre-selected (if it exists)
    server friction
    300ms
    100ms
    improve

    Add empty-state fallback + one-tap fix for failed provisioning.

  5. 05
    Tap composer
    user
    instant
    unchanged
  6. 06
    Type + send
    user
    instant
    unchanged
Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

Empty channel list if `ensureUserHasPersonalPulse` failed silently

The provisioning step in `signInUser` is best-effort. If it failed during sign-in (DB error, race), the user lands on Chats with no channel and no clear empty-state component — only the FAB. Bewildering first-message experience.

Found in SignInUserMutation.php:407 · chats/index.tsx (no empty-state in first 60 lines)
02

Personal pulse is invisible without context

Even when present, the personal pulse is just one row in the channel list with no 'this is your scratch space' framing. New users don't know they can message themselves.

Found in useBootstrapPulse.ts auto-select logic

Recommendations

01

Empty-state with one-tap fix

Why: If channel list is empty post-onboarding, show a card: 'Looks like we missed setting up your space. Tap to fix.' One tap re-triggers provisioning.

2 hr
Where to fix services/nova/src/app/(app)/(tabs)/chats/index.tsx · empty-state branch
02

First-run framing of personal pulse

Why: A subtle 'Your space · for drafts and reminders' label below the personal pulse for the first week.

1 hr
Where to fix services/nova/src/hooks/useBootstrapPulse.ts · channel row component in chats/index.tsx
03

Cross-link first-action prompts on Home

Why: The Feed already has card slots — surface a 'Send your first message' card until the user does.

3 hr
Where to fix services/nova/src/components/feed/FeedPanelContent.tsx · feed.ts FeedItemKind enum
06 Journey 5

Create a new channel

User starting a new team or DM thread

Today
10–20 s
3 taps for DM · 4–5 for team channel
After recommendations
8–15 s
3 taps for DM (already good) · 3–4 for team channel
Magic-number tie

A user who creates a channel is forming the social graph — strongest predictor of week-2 retention.

Today 4.6s
Recommended 3.0s
Saves 1.6s
  1. 01
    Tap FAB (+)
    user
    instant
    unchanged
  2. 02
    Spring menu expands
    DM / Team Channel options
    client
    200ms
    100ms
    improve

    Pre-populate menu with last-3 contacted.

  3. 03
    Tap DM or Team Channel
    user
    instant
    unchanged
  4. 04
    Search user (DM) or fill name (Team)
    user
    4.0s
    2.5s
    improve

    Instant search on first keypress (100ms debounce).

  5. 05
    createPulse mutation
    server
    400ms
    400ms
    improve

    Server-side DM idempotency by (creator, recipient).

Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

DM duplicate guard depends on client cache

`useChatsPage.ts:2673–2683` checks cached `chatChannels` for existing ONETOONE before creating. On a cache miss (first load, or after reinstall), a duplicate is created. Backend has no de-dupe.

Found in useChatsPage.ts:2673–2683
02

300ms search debounce + 1-char minimum delays results

Search activates after 1 character with 300ms debounce. Feels sluggish vs Slack's instant-on-keypress. Compounds in the 'find someone' flow.

Found in useChatsPage.ts (search debounce config)

Recommendations

01

Server-side DM idempotency by (creator, recipient)

Why: Treat DM creation as an upsert. Eliminates duplicate-DM bugs once and for all.

2 hr
Where to fix services/api/app/GraphQL/Mutations/CreatePulseMutation.php · upsert by (creator_id, recipient_id, pulse_type=ONETOONE)
02

Instant search on first keypress

Why: Drop the 1-char gate and debounce to 100ms. Search results feel like Slack.

1 hr
Where to fix services/nova/src/hooks/useChatsPage.ts · search debounce config
03

Pre-populate FAB menu with last-3-contacted

Why: Most DM compose actions go to recent people. One tap from FAB if they're top-3.

3 hr
Where to fix services/nova/src/app/(app)/(tabs)/chats/index.tsx · FAB menu component
07 Journey 6

Create a task

User capturing a to-do

Today
<5 s inline · 15 s full form
2 taps inline (no assignee) · 5 taps full form (with assignee)
After recommendations
<5 s
2 taps inline (with quick-assign)
Magic-number tie

Captured tasks = ongoing engagement events. Linear-level capture speed = Linear-level retention.

Today 3.3s
Recommended 3.3s
  1. 01
    Tap Tasks tab
    user
    instant
    unchanged
  2. 02
    Tap inline composer
    user
    instant
    unchanged
  3. 03
    Type title
    user
    3.0s
    3.0s
  4. 04
    Return / send
    user
    instant
    unchanged
  5. 05
    (Optional) Open task to assign
    +3 taps via full form sheet
    user friction
    instant
    removed
    remove

    Inline assignee chip — no extra screen.

  6. 06
    createTask mutation
    server
    300ms
    300ms
    improve

    Optimistic insert + snackbar-undo on failure.

Source user client server Auth0 db wait Fix remove cache parallel background improve

Bugs & friction found

01

No quick-assign on inline-create

Inline create captures title only. Assignment requires opening the full TaskFormSheet — 3 extra taps. Linear does it inline.

Found in TaskInlineCreate.tsx · TaskFormSheet.tsx:72

Recommendations

01

Inline assignee chip on the composer row

Why: An always-visible 'Me' chip with a tap-to-change picker. Brings tap count to 2 even when assigning. Linear-tier.

4 hr
Where to fix services/nova/src/components/tasks/TaskInlineCreate.tsx · assignee picker sheet
02

Optimistic insert with snackbar-undo

Why: Task appears immediately on send. If backend fails, snackbar offers 'Tap to retry' instead of an error dialog.

3 hr
Where to fix services/nova/src/components/tasks/TaskInlineCreate.tsx · createTask mutation onMutate / onError

Where this gets us

The smallest fixes here have the largest leverage on the pilot.

One Auth0 config line for "sign in twice." One promise-await for the fire-and-forget onboarding bug. Two hours for a Management API token cache. The pilot funnel doesn't widen with grand architecture — it widens by closing the specific paper cuts users feel in the first 90 seconds.

The activation contract is 5 / 1 / 3. Every journey on this page is a step toward it — or away from it.