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.dart and lib/main_production.dart — flavor entry points (both delegate to runBlissApp() in lib/app.dart)
  • lib/app.dart — shared initialization, API base URL configuration (lib/app.dart:82-90)
  • lib/app/app_module.dartflutter_modular route table, navbar wiring, auth guards, deep-link route aliases
  • lib/app/routes/deep_link_handler.dartapp_links capture + Adjust shortlink resolution. See mobile-deep-linking
  • lib/features/core/network/ — Dio provider, custom interceptors (user-agent, accept-language, timezone, accept)
  • lib/features/core/data/services/configuration/feature_flags.dart — ConfigCat-backed flags
  • lib/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-companion
  • lib/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 in documentation/docs/features/sos_toolkit.md
  • lib/bliss_ui_library/ — internal UI component library (“Bliss” is internal naming for the design system)
  • Makefile — targets: generate, translations, lint, run, run_release, coverage, plus ci_* variants
  • codemagic.yaml — production build pipelines. See mobile-build-pipeline
  • documentation/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.yaml defines 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.yml builds MkDocs and uploads to the GCS bucket mobile.6mind.de (service account mobile-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) and https://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 via flutter_secure_storage 9.2.4. No supertokens SDK in pubspec.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/...), client SsoApiClient on the Dio registered as DioInstanceMapping.ssoApiDio. The service requires an allow-listed Origin header 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) via SsoOriginInterceptor. 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.9 for push only. No firebase_auth is listed in pubspec.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, endpoint api.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 as firebase_staging_options.dart and firebase_production_options.dart; secrets are injected at CI time.

  • Android flavors: zpp, staging (both ID-suffixed .stg, deeplink scheme sevenmindstg), production (no suffix, scheme sevenmind). Staging package ID is historically de.seven_mind.android.stg, production is de.sevenmind.android (the seven_mind vs sevenmind inconsistency is intentional and historical, do not “fix” it).

  • Deep-link route alias gotcha: /courseDetail is registered as an alias of /courseDetails at lib/app/app_module.dart:216-221 because 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’s AudioServiceFragmentActivity, which supplies the engine through the cached-engine path. Do not switch it back to FlutterFragmentActivity with provideFlutterEngine(): 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-level shouldDestroyEngineWithHost() override is ignored on that path, which is why the MIND-480 attempt did nothing. launchMode is singleTask so deep links never create a second instance on the same engine, and a lifecycle listener in MainActivity recreates 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 /home to a module’s initial route.

  • Patrol is wired only for the staging flavor in pubspec.yaml. Running e2e against the production flavor needs extra config.

  • Patrol platform stance (MIND-808): tests must not call $.platformAutomator.android directly; back navigation goes through the shared goBack helper (system back on Android, route pop on iOS) and notification-shade media pokes through togglePlayPauseFromSystemUi, which is a no-op on iOS by design (FTL cannot automate the iOS lock screen; manual coverage). sign_up_test deletes its throwaway account through the profile API. history_test clears the local recently-played cache before asserting, so it only passes when the backend recorded the session. A USER_TYPE dart-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 to main_production_dach.dart / main_staging_dach.dart / --flavor productionDach exist in the Makefile but the matching main files and Android flavor are not present. Treat these targets as dead.

  • Lint exclusions: lib/**.g.dart and lib/initializations/settings/** are excluded from analysis. Lints extend very_good_analysis with relaxations (no docs required, relaxed line length, no cascades enforcement).

  • Docs site: every change to documentation/docs/** on main redeploys mobile.6mind.de. Build is MkDocs material theme via GitHub Actions.

  • SOS toolkit ships against fixtures: SosToolkitDataSource.useFixtures gates a hardcoded payload standing in for two endpoints the backend has not built (sosToolkit on GET /content/home, and GET /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 createRectTween explicitly. Also note the animation given to a flightShuttleBuilder is 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 LocalStorageBoxKeys is prefixed with the user id from the profile uuid field before it reaches key_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 declared key UNIQUE 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 field legacyUuid (MIND-829) or from the moment the stored id switches from UUID to numeric on the device (UserSessionProvider.saveUserId); both store it as legacy_user_id in 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 on key again, 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/send forwards to the Gymondo SSO service but collapses UserNotFoundException and malformed addresses into 400 {"errors":[{"title":"Invalid email"}]} and returns 202 "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 against app.6mind.de/api first. The password reset flow treats UserNotFoundException like 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 in message), LimitExceededException (403, sixth consecutive request, self-clearing) and InternalErrorException are mapped when they arrive; the gateway shape is still parsed as a fallback.

  • Module singleton cubits keep state across screens: addSingletonCubit binds 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 in initState or bind it per screen.

  • Failures are breadcrumbs, not events: every FailureResponse only writes a Sentry breadcrumb via Logger.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 grep breadcrumbs.values). When a user-facing error matters, call Logger.logError as 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