Summary: The 7Mind iOS and Android consumer app. One Flutter codebase, three flavors (zpp, staging, production), single brand despite the “monorepo” naming. Sources: direct code inspection (pubspec.yaml, lib/app.dart, lib/app/app_module.dart, android/app/build.gradle, codemagic.yaml, .github/workflows/, Makefile, .fvmrc, documentation/mkdocs.yaml) Last updated: 2026-09-18
Purpose
The primary 7Mind consumer product: a Flutter app delivering guided meditations, courses, sleep content, breathing exercises, and an Aury AI companion (German-locale, feature-flagged). All business logic, content, subscriptions, and identity are served by elixir-backend. Despite the plural “mobile-apps-monorepo” name, only the 7Mind brand ships from this repo. There is no 7Sleep app code here. The repo also publishes a public MkDocs site for internal mobile engineering documentation.
Stack
- Flutter 3.41.9 pinned via
.fvmrc, Dart SDK>=3.8 <4.0 - State / routing / DI:
flutter_bloc 8.1.6,flutter_modular 6.4.1(modular routing + DI;Modular.get<T>()pattern) - HTTP:
dio 5.9.0+retrofit 4.7.3(codegen-driven API clients) - Local storage:
drift 2.28.2(SQLite ORM) - Localization:
easy_localization+intl 0.20.2(locales: en, de, fr, it, es; fallback en) - Codegen:
build_runner 2.6.0,json_serializable 6.10.0,copy_with_extension_gen 15.0.1,drift_dev 2.28.2,flutter_gen_runner - Lints:
very_good_analysis 6.0.0 - Testing:
flutter_test,bloc_test 9.1.7,mocktail 1.0.4,patrol 3.15.2(e2e, staging flavor only)
Key Files / Entry Points
lib/main_staging.dartandlib/main_production.dart— flavor entry points (both delegate torunBlissApp()inlib/app.dart)lib/app.dart— shared initialization, API base URL configuration (lib/app.dart:82-90)lib/app/app_module.dart—flutter_modularroute table, navbar wiring, auth guards, deep-link route aliaseslib/app/routes/deep_link_handler.dart—app_linkscapture + Adjust shortlink resolution. See mobile-deep-linkinglib/features/core/network/— Dio provider, custom interceptors (user-agent, accept-language, timezone, accept)lib/features/core/data/services/configuration/feature_flags.dart— ConfigCat-backed flagslib/features/core/data/services/configuration/amplitude_configurations_provider.dart— Amplitude Experiment provider (api.lab.amplitude.com)lib/features/ai_companion/— Aury in-app AI chat companion. See aury-companionlib/features/sos_toolkit/— SOS emergency entry point on the home screen (Barmer pitch, MIND-773). Client complete, fixture-backed: the two backend endpoints it needs do not exist yet. Contentful model and endpoint contract are written up indocumentation/docs/features/sos_toolkit.mdlib/bliss_ui_library/— internal UI component library (“Bliss” is internal naming for the design system)Makefile— targets:generate,translations,lint,run,run_release,coverage, plusci_*variantscodemagic.yaml— production build pipelines. See mobile-build-pipelinedocumentation/docs/index.md+documentation/mkdocs.yaml— published MkDocs site.fvmrc— pins Flutter version
Deployment
Mobile builds are split across two CI systems. See mobile-build-pipeline for the full breakdown.
- Production iOS / Android builds: Codemagic only.
codemagic.yamldefines 7 workflows for iOS (TestFlight) and Android (Google Play internal track) across staging, production, and ZPP submission flavors. Patrol e2e runs on Firebase Test Lab via Codemagic. Since MIND-808 the two e2e workflows run FTL synchronously (a Patrol failure fails the Codemagic build), the iOS device runs locale de so the prevention flow is exercised, and a GitHub Actions cron (nightly_e2e.yml) starts both workflows nightly via the Codemagic API (needs CODEMAGIC_API_TOKEN and CODEMAGIC_APP_ID secrets). - GitHub Actions (in
.github/workflows/): CI-only. Active workflows:test.yml,analyze.yml,internal_builds.yml,verify_code_gen.yml,localize-gen.yml,codecov.yml,title-validation.yml,translations.yml,release_drafter.yml,release_drafter_release.yml,publish_catalog.yml,publish_catalog_pr.yml,supernova.yml. None produce shipping iOS or Android artifacts. - Docs publish:
publish_documentation.ymlbuilds MkDocs and uploads to the GCS bucketmobile.6mind.de(service accountmobile-docs-write@mind-f62c0.iam.gserviceaccount.com). Doc site:https://mobile.6mind.de/site.
Dependencies
External services:
- Backend API: elixir-backend at
https://magic.6mind.de/(staging) andhttps://magic.7mind.de/(production) - Chargebee (
chargebee_flutter 0.4.8) for subscription billing. See chargebee-billing - SSO providers:
google_sign_in 6.2.2,sign_in_with_apple 6.1.4,flutter_facebook_auth 7.1.2. Tokens stored viaflutter_secure_storage 9.2.4. NosupertokensSDK inpubspec.yaml; sessions are mediated by the backend. See supertokens-auth - Gymondo SSO service (Cognito): since the 2026-09 user migration, identity is served by Gymondo’s Cognito-based SSO service behind Kong, not by the Elixir backend. The app calls it directly for the password reset:
POST https://app.7mind.de/api/v2/users/reset-password(test:https://app.6mind.de/api/...), clientSsoApiClienton the Dio registered asDioInstanceMapping.ssoApiDio. The service requires an allow-listedOriginheader and matches 7Mind on Origin only; the app sends the web app origin of the current environment (https://app.7mind.de/https://app.6mind.de, agreed with Gymondo 2026-09-18) viaSsoOriginInterceptor. Contract: Gymondo “SSO password reset” spec v1 (code,message=sso.<lowercased code>,debugMessage; branch only on the first two). Login and signup still go through the legacy routes on magic.* (/user_identity/*) - Firebase:
firebase_core 3.15.0+firebase_messaging 15.2.9for push only. Nofirebase_authis listed inpubspec.yaml - Sentry (
sentry_flutter 9.8.0) for crash and error reporting - Rudderstack (
rudder_sdk_flutter 3.3.0) as the primary CDP. See mobile-analytics-stack - Adjust (
adjust_sdk 5.4.4) for install attribution and iOS shortlink resolution - Amplitude Experiment for runtime configuration and A/B testing (via
AmplitudeConfigurationsProvider, endpointapi.lab.amplitude.com) - ConfigCat (
configcat_client 4.1.1) for feature flags (e.g.isAuryEnabled) - Braze (
braze_plugin 16.0.0) for engagement, in-app messaging, push - Algolia (
algoliasearch 1.41.1,algolia_client_search 1.34.1) for content search - Firebase Test Lab for Patrol e2e (driven by Codemagic)
Internal: Consumes elixir-backend. No path or git dependencies on other 7Mind repos in pubspec.yaml. api-contracts is not referenced directly; serialization is hand-rolled via json_serializable and Retrofit-generated clients.
Integration Points
- Calls elixir-backend Public API for all business logic, content, billing reconciliation, and identity
- Receives deep links from the Aury companion webview (see mobile-deep-linking and aury-companion)
- Emits events to Rudderstack and Braze in parallel with the backend (see mobile-analytics-stack)
- Push notifications delivered via FCM and APNs (
firebase_messaging) - Content search resolved against Algolia indices (separate indices per brand on the backend side, but only the 7Mind index is consumed here)
Conventions
-
Environment is selected by main file, not by
--dart-define. To switch staging vs production, run a different--target lib/main_*.dart. Firebase options are checked in asfirebase_staging_options.dartandfirebase_production_options.dart; secrets are injected at CI time. -
Android flavors:
zpp,staging(both ID-suffixed.stg, deeplink schemesevenmindstg),production(no suffix, schemesevenmind). Staging package ID is historicallyde.seven_mind.android.stg, production isde.sevenmind.android(theseven_mindvssevenmindinconsistency is intentional and historical, do not “fix” it). -
Deep-link route alias gotcha:
/courseDetailis registered as an alias of/courseDetailsatlib/app/app_module.dart:216-221because the Aury companion webview emits the singular form. Preserve both. See mobile-deep-linking. -
Android host activity must not own the Flutter engine (MIND-817): the engine is shared with the audio_service foreground service, so
MainActivity(all three flavors) extends audio_service’sAudioServiceFragmentActivity, which supplies the engine through the cached-engine path. Do not switch it back toFlutterFragmentActivitywithprovideFlutterEngine(): on that path the FlutterFragment treats the engine as its own, destroys it when the activity is destroyed and leaves the dead engine in the engine cache, so the next launch crashes on the first layout pass with “FlutterJNI is not attached to native”. This single mistake was 94% of Android user-perceived crashes in summer 2026 and pushed the app over the Play Store bad-behaviour threshold. An activity-levelshouldDestroyEngineWithHost()override is ignored on that path, which is why the MIND-480 attempt did nothing.launchModeissingleTaskso deep links never create a second instance on the same engine, and a lifecycle listener inMainActivityrecreates the activity (and reports to Sentry) if the engine is ever destroyed under it anyway. -
AuthGuard()protects most routes: unauthenticated users see splash → welcome → auth flows only. -
Modular initial-route helper: routes commonly chain
.toModuleDefaultMainRoute()which appends/hometo a module’s initial route. -
Patrol is wired only for the
stagingflavor inpubspec.yaml. Running e2e against the production flavor needs extra config. -
Patrol platform stance (MIND-808): tests must not call
$.platformAutomator.androiddirectly; back navigation goes through the sharedgoBackhelper (system back on Android, route pop on iOS) and notification-shade media pokes throughtogglePlayPauseFromSystemUi, which is a no-op on iOS by design (FTL cannot automate the iOS lock screen; manual coverage).sign_up_testdeletes its throwaway account through the profile API.history_testclears the local recently-played cache before asserting, so it only passes when the backend recorded the session. AUSER_TYPEdart-define (premium or freemium) is plumbed through, but a freemium CI account and gating test do not exist yet. -
Stale Makefile targets:
runDach,runStagingDach, and references tomain_production_dach.dart/main_staging_dach.dart/--flavor productionDachexist in the Makefile but the matching main files and Android flavor are not present. Treat these targets as dead. -
Lint exclusions:
lib/**.g.dartandlib/initializations/settings/**are excluded from analysis. Lints extendvery_good_analysiswith relaxations (no docs required, relaxed line length, no cascades enforcement). -
Docs site: every change to
documentation/docs/**onmainredeploysmobile.6mind.de. Build is MkDocs material theme via GitHub Actions. -
SOS toolkit ships against fixtures:
SosToolkitDataSource.useFixturesgates a hardcoded payload standing in for two endpoints the backend has not built (sosToolkitonGET /content/home, andGET /content/sos_toolkit/{id}). The api and data models already encode the agreed shape, so going live is a one-file change. Do not treat the SOS content on staging as real. -
Hero transitions need a linear rect tween: MaterialApp’s default hero tween arcs the rect, which desynchronises height from any linearly interpolated child geometry. The SOS panel passes
createRectTweenexplicitly. Also note the animation given to aflightShuttleBuilderis only normalised from-to on a push; on a pop it is the popping route’s animation and runs 1 to 0. -
Local storage is one Drift key-value table with user-prefixed boxes (MIND-828): every box name in
LocalStorageBoxKeysis prefixed with the user id from the profileuuidfield before it reacheskey_value_table. The identity migration changed that value from a UUID to the numeric Back Office id, so all rows written before it (downloads, playback positions, recent searches, the active session marker) became unreachable and looked deleted. Schema version 1 also declaredkeyUNIQUE on its own, so writing a key that already existed under the old prefix failed with “UNIQUE constraint failed”: every re-download of a previously downloaded piece showed “Download failed” in an endless dialog loop, and the active session marker failed on every save. Version 2 drops that constraint.LegacyUserDataMigration(core module, run at startup and after every profile call) moves Drift rows, the per-user onboarding and trial paywall flags and the Apple Health preferences from the old id to the current one. The exact old id comes either from the backend fieldlegacyUuid(MIND-829) or from the moment the stored id switches from UUID to numeric on the device (UserSessionProvider.saveUserId); both store it aslegacy_user_idin SharedPreferences. Without it, a heuristic adopts only the download boxes, and only when exactly one UUID-shaped foreign prefix exists: logout never clears per-user boxes, so on a shared device a foreign prefix may belong to someone else and playback positions or search history must not move on a guess. Any new per-user key must be added to that migration. Tracking keeps the numeric id as identity and sends the legacy UUID as an extra attribute (legacy_uuid) to Rudderstack, Braze, Adjust and Sentry; Amplitude Experiment deliberately gets only the numeric id so variants do not move twice. Never add a standalone unique constraint onkeyagain, and treat “Failed to save object in Drift” in Sentry as a storage-prefix problem first. -
*Legacy identity routes on magic. are a proxy that hides SSO errors (MIND-830)**:
magic.*/user/password/reset/sendforwards to the Gymondo SSO service but collapsesUserNotFoundExceptionand malformed addresses into400 {"errors":[{"title":"Invalid email"}]}and returns202 "ok"for social login accounts even when the SSO service would reject them. Do not derive identity behaviour from the local Elixir clone either, its auth handlers are no longer what runs. Verify with curl againstapp.6mind.de/apifirst. The password reset flow treatsUserNotFoundExceptionlike success and shows neutral copy (“If there is an account with this email address …”) on purpose: agreed with Gymondo so no client depends on how the service handles social login accounts (a legal review may change it).RegisteredViaSocialLogin(provider inmessage),LimitExceededException(403, sixth consecutive request, self-clearing) andInternalErrorExceptionare mapped when they arrive; the gateway shape is still parsed as a fallback. -
Module singleton cubits keep state across screens:
addSingletonCubitbinds survive navigation, so a screen that is pushed again shows the previous attempt’s state on a fresh form (seen on the password reset screen, MIND-830). Reset the cubit ininitStateor bind it per screen. -
Failures are breadcrumbs, not events: every
FailureResponseonly writes a Sentry breadcrumb viaLogger.log, so a failing flow is invisible in Sentry unless something else in the same session raises an event. Sentry cannot search breadcrumb text; the way to find such sessions is the events API (list events, then read each event’s JSON and grepbreadcrumbs.values). When a user-facing error matters, callLogger.logErroras the download cubit now does.
Agent Change Log
2026-09-18 — MIND-830: password reset calls the Gymondo SSO service directly (app.7mind.de/api/v2/users/reset-password, Origin header required) instead of the legacy magic.* route that swallowed SSO errors; reset screen renders error states, neutral success copy, unknown user treated like success, Cognito codes mapped (social login provider, rate limit); shared reset cubit is reset on open
2026-09-15 — MIND-828: downloads of pre-migration content failed and the error dialog looped. Root cause: standalone UNIQUE(key) on the Drift key-value table combined with the user id prefix changing from UUID to numeric id. Schema v2 drops the constraint, legacy UUID-prefixed rows are adopted at startup, the dialog is shown once, download failures become Sentry events
2026-09-09 — MIND-817: Android crash rate over the Play Store threshold traced to MainActivity owning the shared audio_service engine; MainActivity now extends AudioServiceFragmentActivity, launches singleTask, and recreates itself if the engine dies under it
2026-05-15 — stub created, awaiting content seeding from code inspection
2026-05-15 — initial knowledge extraction from codebase
2026-09-03 — MIND-808 follow-up: suite runs green locally (12 of 13 cases, old backend). Two app bugs fixed: the splash redirect no longer resets the stack while the user is in an auth screen, and the welcome buttons no longer share widget keys with the auth submit buttons. Known gap: speaker-variant plays do not reliably surface in recently played
2026-09-01 — MIND-808: made the Patrol suite a real gate: FTL runs synchronous, nightly GH Actions trigger, login/signup assert session cookies, history test decoupled from the local cache, iOS paths via shared helpers, example test deleted
2026-08-24 — added the SOS toolkit feature (MIND-773, Barmer emergency player): home entry point, detail screen, shared wavy Bliss surface with a hero transition. Fixture-backed pending two new content endpoints; Contentful model proposal documented in the repo docs site