Skip to content

User Flows

Step-by-step diagrams of the eight key journeys through Objectuve — four core flows (acquisition, daily check-in, goal lifecycle, coach) and four supporting flows (community join, supporter upgrade, feedback submission, achievement unlock). Companion to user-guide.md (prose reference) and site-map.md (the full route tree).

Scope: consumer app only. Each flow maps to one or more sections in the user guide.

Source of truth: ionic_frontend/src/router/index.ts for routing and guards. User-guide section anchors are referenced in node labels. Regenerate PNG exports with cd docs_site && npm run docs:diagrams.

Diagram conventions:

  • Rounded rectangles [Screen] — pages/views the user sees
  • Diamonds {Decision} — branches driven by user choice
  • Hexagons {{Gate}} — route guards / system checks
  • Stadiums ([Terminal]) — entry and exit points
  • Trapezoids [/Action/] — user actions that emit events or trigger jobs

Flow A — Acquisition: sign-up → onboarding → profile completion

Goal: First-time visitor creates an account and lands on a usable Dashboard with at least one goal and a Coach configured.

Entry points:

  • Marketing landing CTA → /sign-up
  • Returning user hitting any authed URL while signed-out → redirected to /sign-in?redirect=…

Exit condition: User reaches /dashboard with onboardingCompletedAt set on their User record.

Maps to user-guide §1 Getting started and §7 Meet your Coach. Deeper architecture detail in onboarding-flow.md.

flowchart TD
  Start(["Visitor arrives"]):::terminal --> Entry{"Has account?"}
  Entry -- "No" --> SignUp["/sign-up"]:::screen
  Entry -- "Yes" --> SignIn["/sign-in"]:::screen

  SignUp --> Clerk[/"Clerk OAuth or email"/]:::action
  SignIn --> Clerk

  Clerk --> SsoCallback["/sso-callback"]:::screen
  SsoCallback --> TransferableNative Android Google,<br/>no existing account?:::gate
  Transferable -- "No" --> SyncUser[/"syncClerkUser mutation"/]:::action
  Transferable -- "Yes, transferable" --> Consent["AgreeToTermsModal<br/>Terms & Privacy consent"]:::screen
  Consent -- "Accept" --> SyncUser
  Consent -- "Decline / dismiss" --> Declined{"Review and agree,<br/>or back to sign in?"}
  Declined -- "Review and agree" --> Consent
  Declined -- "Back to sign in" --> SignIn
  SyncUser --> AuthGuardClerk session valid?:::gate
  AuthGuard -- "No" --> SignIn
  AuthGuard -- "Yes" --> OnboardingGateevaluateOnboardingGate<br/>shouldOnboard?:::gate

  OnboardingGate -- "Already onboarded" --> ProfileGate
  OnboardingGate -- "First run" --> WelcomeStart["/welcome/:slideId<br/>resumes via firstSlideId"]:::screen

  WelcomeStart --> S1["Slide 1 · Welcome"]:::screen
  S1 --> S2["Slide 2 · Life area"]:::screen
  S2 --> S3["Slide 3 · Goal name"]:::screen
  S3 --> S4{"Goal shape?"}
  S4 -- "Sprint" --> S4a["Target date required"]:::screen
  S4 -- "Arc" --> S4a
  S4 -- "Habit" --> S5
  S4 -- "Life List" --> S5
  S4a --> S5["Slide 5 · Why does it matter?"]:::screen
  S5 --> S6["Slide 6 · Pick Coach<br/>6 personas"]:::screen
  S6 --> S7["Slide 7 · Tone dial<br/>Gentle 1 - Tough 5"]:::screen
  S7 --> S8["Slide 8 · Focus areas"]:::screen
  S8 --> S9["Slide 9 · Rhythm / frequency"]:::screen
  S9 --> S10{"Attribution captured?"}
  S10 -- "No" --> S10a["Slide 10 · How you found us"]:::screen
  S10 -- "Yes, from sign-up link" --> Done
  S10a --> Done[/"DoneSlide.completeOnboarding"/]:::action
  Done --> ProfileGateevaluateProfileCompleteGate<br/>firstName + lastName + username + avatar?:::gate

  ProfileGate -- "Profile complete" --> Dashboard
  ProfileGate -- "Profile incomplete" --> ProfileAbout["/profile/complete/about"]:::screen
  ProfileAbout --> ProfilePhoto["/profile/complete/photo"]:::screen
  ProfilePhoto --> Dashboard(["/dashboard"]):::terminal

  S1 -. "abandon" .-> Abandoned[/"wizard_abandoned event<br/>per session"/]:::action
  S2 -. "abandon" .-> Abandoned
  S6 -. "abandon" .-> Abandoned

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef gate fill:#fce7f3,stroke:#be185d,color:#831843
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81

Flow: Acquisition

Cross-references:


Flow B — Daily check-in (the ~10 min/day activation loop)

Goal: Existing user logs habits, mood, and any urgent goal progress in a single short session, then leaves the app.

Entry points:

  • Push notification deep link → /dashboard
  • App icon launch → /dashboard
  • Streamlined widget → /check-in

Exit condition: User leaves the app (back to home screen, closes tab). The "Done" celebration screen in Focus mode is the explicit success state.

Maps to user-guide §2 Your dashboard, §5 Habits, §6 Mood check-in, §8 Insights and Coach modal.

flowchart TD
  Start(["Open app"]):::terminal --> Dashboard["/dashboard"]:::screen
  Dashboard --> ModeCheck{"Dashboard mode<br/>setting"}

  ModeCheck -- "Focus" --> CardStack["Focus mode<br/>card stack"]:::screen
  ModeCheck -- "Full" --> FullView["Full dashboard"]:::screen
  ModeCheck -- "Auto + nothing urgent" --> CardStack
  ModeCheck -- "Auto + has urgent" --> FullView

  subgraph FocusFlow["Focus mode card stack"]
    direction TB
    C1["Card 1 · Greeting + streak"]:::screen
    C2["Card 2..N · Habit cards<br/>auto-skip completed"]:::screen
    C3["Card · Mood check-in<br/>skipped if logged today"]:::screen
    C4{"Has urgent goal?"}
    C4a["Card · Urgent goal<br/>+ Add Progress"]:::screen
    C5["Card · Coach insight"]:::screen
    C6["Card · Social nudge<br/>partner / ally cue"]:::screen
    C7(["Done<br/>confetti + session stats"]):::terminal

    C1 --> C2 --> C3 --> C4
    C4 -- "Yes" --> C4a --> C5
    C4 -- "No" --> C5
    C5 --> C6 --> C7
  end

  subgraph FullFlow["Full dashboard"]
    direction TB
    F1["Momentum bar<br/>streak · check-ins · goals"]:::screen
    F2["Today's practice card<br/>up to 3 habits + mood + 1 insight"]:::screen
    F2a[/"Tap habit ring<br/>complete habit"/]:::action
    F2b[/"Tap mood emoji<br/>log mood"/]:::action
    F3{"Needs-attention card?"}
    F3a["Tap → /goal/:goalId"]:::screen
    F3b[/"Inline Update progress"/]:::action
    F4["Your goals · Social · Weekly activity"]:::screen

    F1 --> F2
    F2 --> F2a
    F2 --> F2b
    F2 --> F3
    F3 -- "Yes" --> F3a
    F3 -- "Yes" --> F3b
    F3 -- "No" --> F4
  end

  CardStack --> C1
  FullView --> F1

  C7 --> Leave
  F4 --> Leave
  F3a --> Leave
  F3b --> Leave

  Dashboard -. "swipe / nav" .-> CheckInPage["/check-in<br/>standalone surface"]:::screen
  CheckInPage --> Leave(["User exits app"]):::terminal

  F2a -. "emits" .-> XP[/"XP + streak feedback<br/>UserAction.trigger_badge"/]:::action
  F2b -. "emits" .-> XP
  C2 -. "emits" .-> XP
  C4a -. "emits" .-> XP

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81

Flow: Daily check-in

Cross-references:


Flow C — Goal lifecycle: create → progress → public

Goal: User creates a new goal, logs progress against it over time, and optionally publishes it for accountability.

Entry points:

  • Onboarding completion lands on /goals/create for the first goal
  • Dashboard "Set your first goal" CTA → /goals/create
  • /goals list + the floating goal-FAB → /goals/create

Exit conditions: Goal completed_at is set (success), goal deleted_at is set (soft-delete), or goal sits indefinitely in active state (no terminal).

Maps to user-guide §4 Goals.

flowchart TD
  Entry(["Entry from Dashboard or /goals"]):::terminal --> List["/goals"]:::screen
  List --> CreateBtn[/"Tap New Goal"/]:::action
  CreateBtn --> Wizard["/goals/create<br/>shape · name · target · why · partner"]:::screen

  Wizard --> ShapeChoice{"Goal shape"}
  ShapeChoice -- "Sprint / Arc" --> NeedsDate["Set target date"]:::screen
  ShapeChoice -- "Habit / Life List" --> NoDate["No date required"]:::screen
  NeedsDate --> Created
  NoDate --> Created

  Created[/"CreateGoal interaction<br/>triggers goal_created badge"/]:::action --> Detail["/goal/:goalId"]:::screen

  Detail --> EventChoice{"What's the user doing?"}
  EventChoice -- "Log progress" --> Progress[/"Add progress event"/]:::action
  EventChoice -- "Mark milestone" --> Milestone[/"Tick milestone"/]:::action
  EventChoice -- "Take a freeze" --> Freeze[/"Freeze day<br/>preserves streak"/]:::action
  EventChoice -- "Complete" --> Completed[/"Mark complete"/]:::action
  EventChoice -- "Archive" --> Archived[/"Soft-delete via paranoia"/]:::action

  Progress --> Detail
  Milestone --> Detail
  Freeze --> Detail

  Detail -. "toggle visibility" .-> PublicToggle{"Make public?"}
  PublicToggle -- "Yes" --> PublicGoal["/public_goal/:goalId<br/>shareable URL"]:::screen
  PublicToggle -- "No" --> Detail
  PublicGoal --> Detail

  Completed --> Success(["Goal complete<br/>XP + Legendary badge eligible"]):::terminal
  Archived --> Archive(["Goal archived<br/>excluded from active queries"]):::terminal

  Detail -. "enhancement banner" .-> Coach[/"Coach-assisted goal refinement"/]:::action
  Coach --> Detail

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81

Flow: Goal lifecycle

Cross-references:


Flow D — Coach + insights: Meet Coach → daily Coach modal

Goal: User configures their Coach persona, tone, focus, and rhythm. The Coach then surfaces personalized insights inside Dashboard and Focus mode; the user acts on or dismisses them.

Entry points:

  • Onboarding slides 6–9 (first-run configuration — see Flow A)
  • Settings → "Meet your Coach" tab → /meet-coach
  • Dashboard Coach insight card → expands the Goal Coach modal

Exit conditions: Coach configuration saved (UpdateAiPreferences) and at least one insight has been viewed or dismissed.

Maps to user-guide §7 Meet your Coach and §8 Insights and Coach modal.

flowchart TD
  Entry(["Settings → Meet your Coach<br/>OR onboarding slides 6-9"]):::terminal --> MeetCoach["/meet-coach"]:::screen

  MeetCoach --> Persona{"Pick persona"}
  Persona -- "The Captain" --> Tone
  Persona -- "The Spark" --> Tone
  Persona -- "The Mirror" --> Tone
  Persona -- "The Rival" --> Tone
  Persona -- "The Sage" --> Tone
  Persona -- "The Analyst" --> Tone

  Tone["Tone dial<br/>Gentle 1 ↔ Tough 5"]:::screen --> Focus["Pick focus areas<br/>primary + optional secondary"]:::screen
  Focus --> Rhythm["Set rhythm / frequency<br/>daily · few-times-week · weekly"]:::screen
  Rhythm --> Optional{"Want enneagram input?"}
  Optional -- "Yes" --> Enneagram["/enneagram-assessment"]:::screen
  Optional -- "No" --> Save
  Enneagram --> Save[/"Save AI preferences<br/>UpdateAiPreferences mutation"/]:::action

  Save --> Dashboard["/dashboard"]:::screen

  Dashboard --> Insight["Coach insight card<br/>Today's practice or Focus mode"]:::screen
  Insight --> Choice{"User action"}
  Choice -- "Tap CTA" --> Action[/"Navigate to suggested action<br/>goal · check-in · habit"/]:::action
  Choice -- "Dismiss" --> Dismiss[/"useAiInsights.markDismissed<br/>stable insight ID + 24h TTL"/]:::action
  Choice -- "Open modal" --> Modal["Goal Coach modal<br/>conversational refinement"]:::screen

  Modal --> Action
  Modal --> Dismiss
  Action --> Done(["Insight acted on"]):::terminal
  Dismiss --> Done

  Dashboard -. "surfaces new insight" .-> Insight

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81

Flow: Coach + insights

Cross-references:


Flow E — Community join

Goal: Signed-in user discovers a community, joins it, and triggers the right gamification side effects (first-community badge, optional founding-member badge).

Entry points:

  • /communities (Communities tab) → browse → tap community card
  • Direct link to /communities/:id (shared by another user)

Exit condition: A CommunityMember record exists for the user, the community appears on their dashboard tabs, and any first-time badges have been queued.

Maps to user-guide §11 Communities.

flowchart TD
  Start(["User opens /communities"]):::terminal --> CommList["/communities<br/>Browse list"]:::screen
  CommList --> Detail["/communities/:id<br/>Community.vue"]:::screen
  Detail --> JoinChoice{"Already a member?"}
  JoinChoice -- "Yes" --> Member["Show member view<br/>tabs · feed · challenges"]:::screen
  JoinChoice -- "No" --> JoinBtn[/"Tap Join · useCommunity.joinCommunity"/]:::action

  JoinBtn --> Mutation[/"joinCommunity GraphQL mutation"/]:::action
  Mutation --> InteractionSocial::JoinCommunity<br/>require_authorized · not_already_joined:::gate
  Interaction -- "Guard fails" --> Error(["Error toast<br/>409 conflict"]):::terminal
  Interaction -- "Pass" --> Create[/"CommunityMember.create!<br/>role: member"/]:::action

  Create --> FirstCheck{"First community joined?"}
  FirstCheck -- "Yes" --> FirstJob[/"Gamification::JoinFirstCommunityActionJob<br/>queues join_first_community (Common)"/]:::action
  FirstCheck -- "No" --> FoundingCheck

  FirstJob --> FoundingCheck{"Within founding window<br/>and not already awarded?"}
  FoundingCheck -- "Yes" --> FoundingJob[/"Gamification::FoundingMemberBadgeJob<br/>queues founding_member (Epic)"/]:::action
  FoundingCheck -- "No" --> Feed
  FoundingJob --> Feed

  Feed[/"CommunityFeedItem.add_join_feed_item<br/>public feed entry"/]:::action --> Member
  Member -. "tap challenge" .-> Challenge["/communities/:id/challenges/:challengeId"]:::screen
  Member --> Done(["Joined · badges queued"]):::terminal

  Member -. "later" .-> Achievement["Achievement unlock<br/>see Flow H"]:::screen

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81
  classDef gate fill:#fce7f3,stroke:#be185d,color:#831843

Flow: Community join

Cross-references:


Flow F — Supporter upgrade

Goal: A signed-in user upgrades to the paid Supporter tier via Stripe Checkout. On success the user's supporter_tier field is set and the in-app supporter perks unlock.

Entry points:

  • /settings?tab=supporter (the canonical entry)
  • /checkout/cancel redirect → bounces back to /settings?tab=supporter if the user abandons Stripe

Exit condition: User.supporter_tier is one of monthly/yearly/lifetime (with supporter_until set for time-bound plans) and CheckoutSuccess.vue confirms isSupporter: true.

Maps to user-guide §14 Supporter tier.

flowchart TD
  Start(["User opens Settings"]):::terminal --> Settings["/settings?tab=supporter"]:::screen
  Settings --> Plans[/"PLANS_QUERY · fetch tiers<br/>monthly · yearly · lifetime"/]:::action
  Plans --> Pick{"Pick plan"}
  Pick -- "Tap Upgrade" --> CreateSession[/"createCheckoutSession mutation<br/>planSlug · successUrl · cancelUrl"/]:::action

  CreateSession --> InteractionMutations::CreateCheckoutSession<br/>require_authentication:::gate
  Interaction --> Stripe[/"Billing::CreateCheckout<br/>creates Stripe session +<br/>PaymentRecord (pending)"/]:::action

  Stripe --> Redirect["Browser redirect to<br/>checkout.stripe.com"]:::screen
  Redirect --> StripeChoice{"Stripe outcome"}
  StripeChoice -- "Pay" --> Success["/checkout/success<br/>CheckoutSuccess.vue"]:::screen
  StripeChoice -- "Cancel" --> Cancel["/checkout/cancel<br/>(redirects to settings)"]:::screen
  Cancel --> Settings

  Success --> Poll[/"Poll USER_QUERY · 12s window<br/>watch isSupporter flag"/]:::action
  Poll --> Webhook[/"Webhooks::StripeController<br/>checkout.session.completed event"/]:::action
  Webhook --> SubscriberBilling::Subscribers<br/>billing.payment_processed:::gate
  Subscriber --> Apply[/"apply_supporter_tier<br/>supporter_tier · supporter_until"/]:::action

  Apply --> Updated[/"PaymentRecord.status = completed<br/>UserAction → supporter badge (Rare)"/]:::action
  Updated --> Celebrate["Celebration UI<br/>perks unlocked"]:::screen
  Celebrate --> Done(["Supporter tier active"]):::terminal

  Poll -. "webhook delayed > 12s" .-> Celebrate

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81
  classDef gate fill:#fce7f3,stroke:#be185d,color:#831843

Flow: Supporter upgrade

Cross-references:


Flow G — Feedback submission

Goal: A signed-in user submits a feedback post (idea, bug report, or general comment). The post is created, auto-upvoted by its author, and may trigger a first-time-feedback badge.

Entry points:

  • /settings?tab=feedback (canonical)
  • /feedback redirect → resolves to the same Settings tab
  • "Feedback" link in the footer / overflow menu

Exit condition: A FeedbackPost record exists with the submitting user as author and a self-vote, and any qualifying badges have been queued via Gamification::CreateFeedbackBadgeJob.

Maps to user-guide §15 Settings and profile.

flowchart TD
  Start(["User opens Settings"]):::terminal --> Feedback["/settings?tab=feedback<br/>Feedback.vue"]:::screen
  Feedback --> List[/"FEEDBACK_POSTS_QUERY · list posts"/]:::action
  List --> NewIdea{"Submit new post?"}
  NewIdea -- "No · upvote existing" --> Upvote[/"upvoteFeedbackPost mutation"/]:::action
  Upvote --> Done(["Vote recorded"]):::terminal

  NewIdea -- "Yes · tap New Idea" --> Modal["Modal · title · description · category"]:::screen
  Modal --> Submit[/"createFeedbackPost mutation"/]:::action
  Submit --> InteractionFeedback::CreateFeedbackPost<br/>require_authentication · validate:::gate
  Interaction -- "Invalid (title > 200 / bad category)" --> Err(["Error toast"]):::terminal
  Interaction -- "Valid" --> Create[/"FeedbackPost.create!<br/>+ self-upvote (FeedbackVote)"/]:::action

  Create --> BadgeCheck{"Eligible for feedback badge?"}
  BadgeCheck -- "First post" --> FirstBadge[/"CreateFeedbackBadgeJob<br/>queues voice_heard (Common)"/]:::action
  BadgeCheck -- "10+ posts" --> TenBadge[/"CreateFeedbackBadgeJob<br/>queues community_voice (Rare)"/]:::action
  BadgeCheck -- "Neither" --> Refresh
  FirstBadge --> Refresh
  TenBadge --> Refresh

  Refresh[/"Refetch FEEDBACK_POSTS_QUERY<br/>(Apollo cache invalidation)"/]:::action --> Posted["Post appears at top<br/>self-vote pre-applied"]:::screen
  Posted --> Done2(["Post submitted"]):::terminal
  Posted -. "badge queued" .-> Achievement["Achievement unlock<br/>see Flow H"]:::screen

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81
  classDef gate fill:#fce7f3,stroke:#be185d,color:#831843

Flow: Feedback submission

Cross-references:

  • View: Feedback.vue
  • Mutation: CREATE_FEEDBACK_POST_MUTATION in graphql/feedback.js
  • Interaction: Feedback::CreateFeedbackPost — author auto-upvote happens here
  • Anonymous users cannot submit — the interaction enforces require_authentication. There is no anonymous-feedback path today.
  • /feedback is a router redirect, not a real route — see site-map.md.

Flow H — Achievement unlock (passive)

Goal: Document the asynchronous path from an interaction-side UserAction.generate / trigger_badge call to the user seeing a celebration UI on their device.

Entry points: Implicit — any interaction that emits a UserAction is a possible trigger. Common call sites:

Exit condition: The user has seen the celebration (toast, gold glow, or legendary modal) and the badge is marked seen in localStorage.seen_achievements_*.

Maps to user-guide §10 Achievements, XP, and ranks.

flowchart TD
  Trigger(["Interaction emits UserAction"]):::terminal --> CheckJob[/"Gamification::CheckBadgesJob.perform_async<br/>(or specific-badge job)"/]:::action
  CheckJob --> RuleEngineRun rule engine<br/>iterate 6 badge categories:::gate
  RuleEngine --> Qualified{"Any badge qualifies?"}
  Qualified -- "No" --> NoOp(["No-op · job ends"]):::terminal
  Qualified -- "Yes" --> Award[/"UserAction.generate(badge_key)<br/>persists action row"/]:::action

  Award --> Rarity{"Badge rarity"}
  Rarity -- "Common · weight 1" --> Common[/"BadgeCatalog: e.g. join_first_community<br/>create_first_goal · early_bird"/]:::action
  Rarity -- "Rare · weight 2" --> Rare[/"BadgeCatalog: habit_former<br/>supporter · planner"/]:::action
  Rarity -- "Epic · weight 3" --> Epic[/"BadgeCatalog: founding_member<br/>dedicated · executor"/]:::action
  Rarity -- "Legendary · weight 4" --> Legendary[/"BadgeCatalog: super_fan<br/>visionary · achiever"/]:::action

  Common --> Stored[/"UserAction persisted<br/>visible to USER_QUERY"/]:::action
  Rare --> Stored
  Epic --> Stored
  Legendary --> Stored

  Stored --> Watcher["AchievementWatcher.vue<br/>polls USER_QUERY every 10s"]:::screen
  Watcher --> Diff[/"Diff user.actions vs<br/>localStorage seen_achievements_*"/]:::action
  Diff --> NewOnes{"New action(s)?"}
  NewOnes -- "No" --> Wait(["Wait for next poll"]):::terminal
  NewOnes -- "Yes" --> Process[/"useAchievementNotifications.processBadges"/]:::action

  Process --> FocusCheck{"Focus mode active?"}
  FocusCheck -- "Yes" --> FocusSink[/"Route to focusCaptureSink<br/>queued for end-of-session"/]:::action
  FocusCheck -- "No" --> RarityUi{"Rarity?"}

  RarityUi -- "Legendary" --> Modal["BadgeModal.vue<br/>full-screen unlock"]:::screen
  RarityUi -- "Common · Rare · Epic" --> Glow[/"fireAchievementGlow<br/>+ achievement toast 3.5–5.5s"/]:::action

  Modal --> Seen
  Glow --> Seen
  FocusSink --> Seen
  Seen[/"Mark in localStorage<br/>seen_achievements_<userId>"/]:::action --> Done(["Achievement seen"]):::terminal

  classDef screen fill:#fef3c7,stroke:#d97706,color:#78350f
  classDef terminal fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef action fill:#e0e7ff,stroke:#4338ca,color:#312e81
  classDef gate fill:#fce7f3,stroke:#be185d,color:#831843

Flow: Achievement unlock

Cross-references:

  • Renderless watcher: AchievementWatcher.vue — polls USER_QUERY every 10s
  • Composable: useAchievementNotifications.tsprocessBadges, glow + toast routing
  • Legendary modal: BadgeModal.vue
  • Job: Gamification::CheckBadgesJob — central rule engine
  • Badge taxonomy + rarity weights: BadgeCatalog (5 Common · 6 Rare · 5 Epic · 5 Legendary)
  • Focus-mode capture pattern: any achievements unlocked while the user is in Focus mode are queued to a sink and surfaced after they exit, so the loop is never interrupted mid-flow.

Regenerating diagrams

bash
cd docs_site
npm run docs:diagrams

This extracts every Mermaid block from site-map.md + user-flows.md and writes PNGs into docs/assets/. Re-run after editing any diagram source. The Mermaid blocks in this file are the source of truth — PNGs are derived.


Last updated: 2026-08-16 (v4.5.1) — Flow A gained the native Android Google transfer + legal-consent branch at /sso-callback (OBJ-2599). PNG re-render still pending — see "Regenerating diagrams" above; no working node_modules in this run to do it here.

Loading…