Opt Out of Coach Features — PRD
Product: Objectuve (codename: Enkidu) Feature: A master switch + per-surface toggles that let users turn Coach off — globally or surface-by-surface Status: Exploration / Design Date: April 2026 Owner: Josh Lockhart PRD file: docs/product/coach-opt-out-prd.md
1. Problem Statement
Coach is a first-class feature in Objectuve, but it is also an opinionated one — it pushes insight cards onto the dashboard, generates daily check-in prompts, proposes milestones when a goal is created, and sits behind a floating modal across six pages. For most users that's a feature; for a meaningful minority it's friction they can't remove.
Three concrete problems:
No way to turn Coach off. A user who doesn't want AI-generated advice has no setting to disable it. They can dismiss individual insight cards (stored in
localStorage) and ignore the modal, but check-in prompts keep arriving asai_check_innotifications, milestone suggestions appear on every goal creation, and insight cards regenerate after cache expiry. Coach is always on.All-or-nothing would be wrong. The same user who finds daily Coach check-in prompts intrusive may still want milestone suggestions when creating a goal, or may want the modal available on demand but not the automated insight cards. A master switch alone is too blunt — the surfaces have different value densities and different opt-out rates.
Brand-promise gap. Objectuve positions itself as an anti-social, respectful app ("10 min/day, then leave"). Coach today opts users in by default across every surface with no way out short of hiding the app. This is out of step with the rest of the product's consent posture (
Go It Alone, Private Mode, per-goal privacy).
Opt Out of Coach Features fixes this with one master switch and a small set of per-surface toggles, storing preferences next to the existing coach_persona / coach_tone fields on user_detail.data, and gating every backend Coach path and every frontend Coach surface on those flags.
2. Goals & Success Metrics
Primary Goals
- Give every user a visible, trusted "Turn Coach off" setting that actually turns Coach off everywhere.
- Let users keep the Coach surfaces they value while turning off the ones they don't.
- Enforce opt-out in the backend — no Coach content should be generated for users who have opted out, not just hidden.
- Respect existing Coach configuration — turning Coach back on restores, not resets, the user's persona/tone/focus/frequency.
Success Metrics
| Metric | Target (60 days post-launch) |
|---|---|
| % of MAU who open the Coach settings panel | 15%+ |
| % of MAU who turn the master switch off | 3–8% (a real opt-out path exists; not the majority) |
| % of opt-out users who re-enable within 30 days | <20% (signal the opt-out stuck) |
| % of users who use per-surface toggles (without master-off) | 5–10% (signal that granularity is actually used) |
| Reduction in Coach inference calls for opt-out users | 100% for master-off; ≥90% for per-surface-off |
| Regression in Coach engagement among non-opt-out users | 0 (guardrail) |
| Support tickets containing "turn off Coach" / "disable AI" | ↓ 80% within 60 days |
Guardrails
- No silent generation. If a surface is off, backend must not call an LLM for that user on that surface. This is a cost guardrail and a trust guardrail.
- No cross-surface side effects. Turning off insight cards must not affect check-in prompts or modal access.
- Defaults preserve today's behavior. All toggles default to on for existing users and new users — nobody's experience changes until they touch a toggle.
- Master switch is absolute. When master is off, no Coach UI renders anywhere and no Coach job/mutation runs for that user, regardless of per-surface state.
- Anti-social discipline. Settings panel is a single scrollable screen; no nested wizards.
3. User Stories
- US-1: As a user who doesn't want AI in my goal tracking, I want a single "Turn off Coach" switch that reliably removes every Coach surface.
- US-2: As a user who likes Coach but finds daily check-in prompts noisy, I want to keep Coach enabled but turn off just the check-ins.
- US-3: As a user who only wants Coach on demand, I want to keep the Coach modal available but disable automatic insight cards and milestone suggestions.
- US-4: As a user who turned Coach off, I want the next time I open the Coach modal or land on a Coach surface to show a respectful "Coach is off — turn it back on?" affordance, not a blank page.
- US-5: As a user who configured Coach in Meet Coach and then turned it off, I want my persona/tone/focus/frequency preserved, so re-enabling restores my configuration exactly.
- US-6: As a user, I want the Coach opt-out setting discoverable from both Settings and directly from any Coach surface (e.g. a small kebab menu on an insight card → "Turn off Coach check-ins").
- US-7: As a user in an
ai_check_inpush notification flow, I want a one-tap way from that push to disable check-ins, so opt-out isn't buried in Settings. - US-8: As an admin looking at a user's profile, I want to see their Coach opt-out state so I can interpret low engagement correctly.
4. Feature Design
4a. The master switch
"Coach" — top of a new/expanded Coach section in Settings.
- Label: Coach
- Sub-label: "Personalized advice, insights, and check-ins built around your goals."
- Toggle: on by default.
When off:
- Every per-surface toggle below is visually disabled (grayed out) but retains its stored value.
- The Coach modal entry point is removed from every page (Dashboard, Goals, Goal detail, Badges, Communities, Settings itself hides the modal trigger but keeps the settings panel).
- All Coach mutations return
{ disabled: true, reason: 'coach_disabled_by_user' }without calling the LLM. GenerateAiCheckInPromptsJobskips the user at the query stage.ai_check_inpush notifications are suppressed atSocial::SendPushNotification.- Existing Coach history and preferences are preserved unchanged.
When toggled back on: all per-surface toggles become active again at their previously stored values.
4b. Per-surface toggles
Under the master switch, a stacked list of labeled rows. Each has: label, one-line description, toggle.
| Surface | Toggle ID | What turning this off does |
|---|---|---|
| Daily check-in prompts | coach_check_ins_enabled | GenerateAiCheckInPromptsJob skips this user. No ai_check_in notifications are created or pushed. CheckInPromptCard.vue never shows. |
| Insight cards | coach_insights_enabled | GetInsight mutation returns disabled. useAiInsights.ts short-circuits and renders nothing. Dashboard / goal / goal-detail insight slots collapse cleanly. |
| Milestone suggestions | coach_milestone_suggestions_enabled | GenerateMilestones mutation returns disabled. The "Suggest milestones with Coach" affordance in GoalCreate.vue / RoadmapBuilder.vue is hidden. |
| Goal description refinement | coach_description_refinement_enabled | RefineDescription mutation returns disabled. The "Refine with Coach" button in the goal form is hidden. |
| On-demand advice (Coach modal) | coach_modal_enabled | GetAdvice mutation returns disabled. AiCoachModal.vue trigger is hidden from every page. |
Default for all: on.
A small help link at the bottom of the section links to the Meet Coach page ("Prefer to change how Coach sounds instead? Tune your Coach →").
4c. In-context opt-out affordances
Settings should not be the only path. Each Coach surface exposes a quick opt-out:
- Insight card → kebab menu now includes: "Dismiss once" (existing), "Turn off Coach insights" (new).
- Check-in prompt card / push notification → overflow menu includes "Turn off Coach check-ins" (with a second-step confirmation toast: "Coach check-ins off. Change in Settings → Coach.").
- Coach modal → footer adds a subtle "Turn off Coach" link that deep-links to Settings with the master switch focused.
- Milestone suggestion inline → "Not useful? Turn off milestone suggestions."
Each in-context toggle writes the same preference the Settings panel writes. No parallel state.
4d. Settings panel placement & layout
New tab or section: Settings → Coach. It sits next to / between Privacy and Account. Structure:
Settings
├── Profile
├── Supporter
├── Coach ← new panel lives here
├── Privacy
├── Account
└── FeedbackPanel contents:
- Master switch (4a)
- Divider + heading: "What Coach does"
- Five per-surface toggles (4b) — stacked rows
- Divider + heading: "How Coach sounds"
- Link row: "Tune your Coach persona, tone, focus, and frequency →" (deep-link to
/meet-coach) - Footer: "Your preferences are preserved. Turn Coach off anytime without losing your setup."
4e. "Coach is off" states
When a user lands on a Coach-dependent entry point with Coach disabled:
- Modal trigger taps when master is off: no-op (the trigger is hidden, so this shouldn't happen; but as a defensive fallback, a respectful toast: "Coach is off. Turn it on in Settings → Coach." with a "Settings" button).
- Deep link to
/meet-coachwhile master is off: renders Meet Coach normally (so the user can re-configure) with a banner: "Coach is currently off. Turn it back on to apply changes." - Insight/check-in surfaces: simply do not render. No "Coach is off" placeholder in those slots — that would be noisier than the surface it replaced.
4f. Relationship to existing features
| Feature | Interaction |
|---|---|
| Meet Coach | Meet Coach governs how Coach sounds (persona/tone/focus/frequency). Coach opt-out governs whether Coach runs at all. Orthogonal. A user can configure Coach in Meet Coach, then disable it; preferences are retained. |
| Go It Alone (Privacy PRD) | Go It Alone already sets solo_mode in Coach prompts ("do not suggest community features"). It does not disable Coach. These are independent switches: a Go It Alone user may still want Coach; a Coach-off user may still want communities. |
ai_check_in notifications | When check-ins are off (via master or per-surface), these notifications are not generated at all. This is stronger than Go It Alone's pause-at-delivery approach — we want no inference cost for opted-out users. |
| Coach history | Not touched. Past chat/modal history remains if we ever expose it. Out of scope for v1. |
5. Data & API Changes
5a. Storage — extend user_detail.data JSONB
Coach preferences already live in user_detail.data (the existing JSONB bag that holds coach_persona, coach_tone, coach_focus_primary, coach_focus_secondary, coach_frequency, coach_configured_at). Add:
coach_enabled : boolean (default true)
coach_check_ins_enabled : boolean (default true)
coach_insights_enabled : boolean (default true)
coach_milestone_suggestions_enabled : boolean (default true)
coach_description_refinement_enabled : boolean (default true)
coach_modal_enabled : boolean (default true)
coach_disabled_at : timestamp (set when master flips off, cleared on on)No migration is required to the schema — the JSONB column already exists. The UpdateCoachingPreferences interaction and the accessor methods on User / UserDetail must be extended to read/write these keys with sensible defaults.
5b. User model helper
Add a predicate per surface plus a master helper:
# app/models/user.rb
def coach_enabled?
coach_pref(:coach_enabled, default: true)
end
def coach_check_ins_enabled?
coach_enabled? && coach_pref(:coach_check_ins_enabled, default: true)
end
# ... one per surfaceEvery backend Coach path calls the corresponding predicate before doing any work.
5c. GraphQL
CoachingPreferencesType(rails_api/app/graphql/types/coaching_preferences_type.rb) — add the 6 new boolean fields.UpdateCoachingPreferencesmutation — add the 6 new optional boolean arguments, plumbed throughUserIdentity::UpdateCoachingPreferences.- All Coach mutations — each is extended to return early with
{ disabled: true, reason: 'coach_disabled_by_user' }(or a surface-specific reason) when the relevant predicate is false:Mutations::Ai::GetAdvice— checkscoach_modal_enabled?Mutations::Ai::GetInsight— checkscoach_insights_enabled?Mutations::Ai::GenerateMilestones— checkscoach_milestone_suggestions_enabled?Mutations::Ai::RefineDescription— checkscoach_description_refinement_enabled?
5d. Backend jobs
rails_api/app/jobs/ai_coaching/generate_ai_check_in_prompts_job.rb— scopeUser.alltoUser.where("(user_details.data->>'coach_enabled')::boolean IS NOT FALSE AND (user_details.data->>'coach_check_ins_enabled')::boolean IS NOT FALSE")(or equivalent via aUser.coach_check_ins_enabledscope).rails_api/app/interactions/social/send_push_notification.rb— early return forai_check_inkind whenuser.coach_check_ins_enabled?is false. Belt-and-suspenders: the job shouldn't have created the notification in the first place, but push delivery should still guard.
5e. Frontend
ionic_frontend/src/constants/graphql/user.js(or the file that holdsUSER_QUERY) — extend the user fragment to include all 6 new coaching preference booleans.ionic_frontend/src/composables/useAiCoach.ts— checkuser.coachModalEnabledbefore opening the modal; respectdisabledresponse fromGetAdvice.ionic_frontend/src/composables/useAiInsights.ts— short-circuit the generation loop whenuser.coachInsightsEnabledis false; drop any cached insights for that user fromlocalStorageon the flip.ionic_frontend/src/components/dashboard/CheckInPromptCard.vue— conditionally render.ionic_frontend/src/components/ui/AiInsightCard.vue+components/dashboard/streamlined/InsightCard.vue— conditionally render + surface the in-context "Turn off Coach insights" action (4c).ionic_frontend/src/components/ui/AiCoachModal.vue— footer deep-link to Settings → Coach.ionic_frontend/src/views/Settings.vue— new Coach panel (4d).ionic_frontend/src/views/MeetCoach.vue— banner when Coach is off (4e).ionic_frontend/src/views/GoalCreate.vue(and theRoadmapBuilderfrom the Goal Roadmap PRD) — conditionally render "Suggest with Coach" / "Refine with Coach" affordances.
6. Rollout Plan
- No feature flag needed. The defaults preserve today's behavior for 100% of users — shipping the toggles doesn't ship a behavior change.
- Instrumentation (
UserActiontriggers):coach_disabled_mastercoach_enabled_mastercoach_surface_toggled(withsurface:attribute)coach_in_context_opt_out(withsurface:attribute — fires when users use 4c in-context toggles)
- Admin visibility: add a Coach section to the admin user detail view showing all 6 flags and the
coach_disabled_attimestamp. Exists primarily so operators can interpret "this user has zero Coach events" correctly. - Docs:
- Add this PRD to
docs/product/prd-index.mdunder Exploration / Design. - Cross-link from
docs/product/meet-coach-prd.md(add a one-line note that Coach opt-out is a separate, complementary surface). - Cross-link from
docs/product/privacy-controls-prd.md(clarify that Go It Alone and Coach opt-out are independent). - When shipped, add a user-guide section.
- Add this PRD to
7. Non-Goals (v1)
- Per-goal Coach settings. Users can't turn Coach on for some goals and off for others. All-or-nothing within the account.
- Coach history deletion. Turning Coach off preserves past content. A "delete my Coach history" control is a separate privacy PRD if ever needed.
- Persona / tone editing. Already owned by Meet Coach.
- Back-end inference budgeting per user. Cost guardrails beyond "don't run for opt-out users" belong in the dedicated AI service PRD.
- A "light" Coach mode. If users find even one surface too much, they turn that surface off. No intermediate "reduced Coach" preset in v1.
8. Open Questions
- Should the master switch also silence existing, unacknowledged
ai_check_innotifications (mark as acknowledged) when flipped off, or leave them as history? Recommendation: leave them — Coach-off is forward-looking, not revisionist. - When a user flips master on after being off for a long time, should we trigger a one-time "Welcome back — here's what Coach can do" dashboard card? Recommendation: yes, a small
GettingStartedCard-style reintroduction. Out of scope if it slips — fine to defer. - Should we show a one-time nudge after 14 days of never opening the Coach modal and zero interactions with insight cards, offering the opt-out? Worth testing, but deferred to a separate experiment PRD.
9. Key Files to Touch (implementation reference)
Backend
rails_api/app/models/user_detail.rb— new accessor methods on the JSONB bagrails_api/app/models/user.rb— predicate helpers (coach_enabled?,coach_check_ins_enabled?, ...)rails_api/app/interactions/user_identity/update_coaching_preferences.rb— accept + validate new boolean argsrails_api/app/graphql/types/coaching_preferences_type.rb— expose new fieldsrails_api/app/graphql/mutations/update_coaching_preferences.rb— accept new argsrails_api/app/graphql/mutations/ai/get_advice.rb— early-disabled returnrails_api/app/graphql/mutations/ai/get_insight.rb— early-disabled returnrails_api/app/graphql/mutations/ai/generate_milestones.rb— early-disabled returnrails_api/app/graphql/mutations/ai/refine_description.rb— early-disabled returnrails_api/app/jobs/ai_coaching/generate_ai_check_in_prompts_job.rb— scope usersrails_api/app/interactions/social/send_push_notification.rb—ai_check_inguardrails_api/spec/— coverage on every predicate + disabled path
Frontend
ionic_frontend/src/views/Settings.vue— new Coach panelionic_frontend/src/views/MeetCoach.vue— "Coach is off" bannerionic_frontend/src/views/GoalCreate.vue— gate milestone/refinement affordancesionic_frontend/src/composables/useAiCoach.ts— gate modal + handle disabled responseionic_frontend/src/composables/useAiInsights.ts— gate generation + cache cleanupionic_frontend/src/components/ui/AiCoachModal.vue— footer deep-linkionic_frontend/src/components/ui/AiInsightCard.vue— in-context opt-out actionionic_frontend/src/components/dashboard/CheckInPromptCard.vue— in-context opt-out action + conditional renderionic_frontend/src/components/dashboard/streamlined/InsightCard.vue— conditional renderionic_frontend/src/constants/graphql/user.js— extend user fragment- Storybook stories for the Settings Coach panel + "Coach is off" Meet Coach banner
- Vitest coverage for composable gating; Cypress flow for toggle off → no insights → toggle on → insights return
Next step: review problem framing and surface inventory, then split into two implementation phases (backend predicates + mutation gating → frontend panel + in-context affordances).
Last updated: 2026-05-04