Business Logic & Data Flow
Deep-dive on the assessment/journey domain: how a user moves through a streak of six cognitive assessments, how each assessment submits and scores, and how the pieces (mobile app, BFF, Assessment Service, Result Service, global-asr-service) talk to each other. For paths and commands, open the repository map →. For infrastructure topology, open deployment and infrastructure →.
Actors & Components
| Actor / Component | Role |
|---|---|
| Patient/end user | Registers, completes onboarding, runs the daily assessment streak. |
c2fit-app-frontend | Flutter mobile app — all UI, local orchestration state (journey feature), no scoring logic. |
c2fit-bff | First-party API (auth, user, bundle, journey lifecycle, streak) + reverse proxy/gateway to the two services below and to the speech WebSocket. |
Assessment Service (c2fit-assessment-result, cmd/assessment, port 8081) | Serves question/training content and per-user assessment journey paths. |
Result Service (c2fit-assessment-result, cmd/result, port 8082) | Accepts submissions, scores them (task-specific engines + external ASR for audio tasks), stores/retrieves results. |
global-asr-service | Shared internal AIMET speech-recognition microservice (same platform used by digital-thai-moca), consumed via WebSocket for real-time transcription and via the Result Service's speech-service client. |
c2fit-assessment-dashboard | Deprecated/unsupported local content-authoring tool (internal name dm_question_maker, not a deployed service) — authors into a local SQLite file, exported as JSON and manually copied into c2fit-assessment-result/seed/*.js seed scripts, then applied to MongoDB via a manual mongosh-based migration script (seed/migrate.sh). |
Journey / Assessment Lifecycle
A "journey" is one pass through a bundle of assessments. Source: c2fit-app-frontend/docs/12-assessment-orchestration.md.
journey vs result: journey (c2fit-bff first-party, MongoDB collection journey) tracks the in-progress run — current task/phase, state, POST /api/v1/journey/{start,next,restart-assessment,skip-task,force-end-assessment}, GET /api/v1/journey/state. result (proxied through to Result Service) is for historical browsing of completed results, independent of any active journey.
Known limitation: resuming an in-progress journey after an app restart doesn't work today — the journey-state response doesn't carry enough data to restore progress, so the resume path is disabled client-side pending a backend response-shape change. (Source: c2fit-app-frontend/docs/05-core-user-flows.md.)
Key Statuses
| Enum | Values | Source |
|---|---|---|
JourneyStatus | STARTED, ENDED | c2fit-bff/pkg/model/enum/journey.go |
ScoreStatus | NOT_SCORED, SCORED | c2fit-assessment-result/pkg/model/enum/result.go |
UserReaction | LIKE, DISLIKE | Thumbs-up/down feedback on the AssessmentResult screen, submitted via POST /api/v1/journey/result/user-reaction, stored on the result document. Frontend enum: ResultFeedback.like/.dislike. |
Assessment Task / Phase Reference
A journey path follows the hierarchy assessment → task → phase. A task identifies the current assessment activity or control screen, such as INTRO, DSS_FORWARD, RESTART_MODAL, or RESULT. The frontend represents it with Task (lib/feature/journey/domain/enum/task.dart), and the backend uses the corresponding TaskName values (c2fit-assessment-result/pkg/model/enum/assessment.go). Most assessment tasks contain an ordered list of phases, such as TRAINING and TESTING_1..N; each phase identifies the current step or trial within that task. INTRO and RESULT are tasks and have no phases in the current journey-path seed data.
| Assessment | Notable task values |
|---|---|
| Attention Hold/Release | ahrSingle, ahrComplex |
| Digit Span Speak | dssForward, dssBackward |
| Drawing Memory | dmMatchingSimple, dmMatchingComplex, dmMirrorSimple, dmMirrorComplex, dmDelay1Simple, dmDelay1Complex, dmDelay2Simple, dmDelay2Complex, dmDelay3Complex |
| Modified Visuospatial | mvBaseline, mvForward, mvBackward |
| Verbal Memory | vmEncode, vmImmediateRecall, vmDelayedRecall, vmDelayedRecognition |
| Trail Making Test | tmtNumber, tmtSymbol, tmtNumberWithDistraction, tmtNumberAndSymbol |
Cross-checked against c2fit-assessment-result/pkg/model/enum/assessment.go and the current Drawing Memory, Modified Visuospatial, and Verbal Memory journey-path seed files.
Authentication model
The sequence runs from top to bottom. The mobile app authenticates through c2fit-bff, stores the returned tokens, and uses the access_token for later requests.
- Email login uses the Cognito password flow and returns tokens in one request. Phone login is a two-request flow: request an OTP, then validate it. After OTP validation, the BFF uses Cognito custom authentication to issue tokens.
- The API field named
access_tokencontains a Cognito ID token. The app stores it with the refresh token inFlutterSecureStorage. - JWT protection applies to the journey, streak, bundle, user, and report APIs; the Assessment Service and Result Service proxies; the speech WebSocket; and the authenticated change-password flow.
- The
/admin/v1/*routes are separate from Cognito user authentication.SecretKeyMiddlewarecompares thesecret-keyheader withAPP_SECRET_KEY. In the study ECS deployment,APP_SECRET_KEYis injected from the AWS Secrets Manager secret namedc2fit-bff-study. Open the Secrets Manager section →. - Registration supports email or phone number. Both paths require
POST /api/v1/auth/register/validate-otpbefore the BFF returns tokens. - Forgot password uses
POST /api/v1/auth/forgot-password,/validate-otp, then/change-password. - The BFF implements an external IdP flow for Google and Facebook through
GET /api/v1/auth/oauth2andPOST /api/v1/auth/oauth2/authorize. No caller for these endpoints exists in the current frontend code. - When a protected request returns 401, the app's shared error handler requests a new token and retries the failed operation once. It logs the user out if refresh fails.
Main User-Facing Flow
Home tabs
Home is the app's main screen and contains four tabs.
| Tab | What it contains |
|---|---|
| Streak | This week's activity status and today's ordered assessment list. Users start or continue the daily assessment journey here. |
| Bundle | Available assessment bundles. Users select a bundle to start an assessment journey outside the daily streak. |
| Result | Overall and per-assessment scores and history. If medical history is incomplete, the result charts are blurred and locked until the questionnaire is submitted. |
| Account | Profile and weekly activity summary, Medical History access, and Settings. Settings includes profile editing, contact information, password changes for email accounts, consent management, support, app information, account deletion, and logout. |
Questionnaires
- Entry Questionnaire: Required once per account, on the user's first entry while
completed_onboardingis false. Its eight steps collect name, date of birth, gender, education, cognitive-game use, motivation, cognitive focus areas, and how the user learned about C2Fit. After submission, the app returns to Home and does not require the questionnaire again for that account. Users can later edit the main profile fields from Account → Settings → My Profile. - Medical History Questionnaire: Collects cognitive-tool use during the past year, memory concerns, diagnosed conditions, family dementia history, monthly income, and weekly exercise. An incomplete user can start it from Account, and must complete it before viewing the locked result charts. After submission, the app opens the Result tab. Diagnosed conditions and family dementia history can later be edited from My Profile.
Frontend sources: home/constant.dart, home/presentation/page/home_page.dart, the entry_questionnaire and medical_history_questionnaire feature folders, result/presentation/page/result_all_page.dart, result/presentation/widget/graph_and_score_section.dart, and the Account pages under c2fit-app-frontend/lib/feature/.
Assessment Detail
The assessment requirements are split by app version:
- Open production assessment detail →. It covers the
prodversion on the frontend and backendmainbranches. - Open study assessment detail →. It covers the study version on the frontend
studybranch and BFForigin/studybranch. It also states where the local Assessment/Result Service lacks study source code.
Both files document the task and phase flows, user-visible requirements, special branches, and scoring rules for all six assessments.
Submission & Scoring — Sequence Diagram
Digit Span Speak shown as the representative audio-bearing flow (the only assessment whose scoring depends on an external ASR call).
Real-time speech (used for live captions during Digit Span Speak / Verbal Memory recording, separate from the above async scoring path) instead goes through a WebSocket proxy: FE --WSS--> BFF /ws/v1/speech --WS proxy--> global-asr-service.
The async scoring path also always calls global-asr-service, not Google Speech-to-Text directly — Result Service's Google credentials/config exist but have no consumer anywhere in the codebase, so that's dead/unused config rather than a real fallback path. The call is a plain blocking call, not queued — the scoring flow waits on it directly.
Admin
The project has no staff flow or admin UI. c2fit-bff exposes two API-only /admin/v1/* routes (internal/app/bff/router/admin/v1/admin.go) for back-door operations. Both use SecretKeyMiddleware and a secret-key header instead of Cognito user authentication. In the study deployment, the expected value comes from the AWS Secrets Manager secret c2fit-bff-study.
CloudWatch Logs Insights for the 90-day query window across /ecs/c2fit-bff-{dev,prod,study} shows that POST /admin/v1/import-users was called manually through curl or Bruno. No traffic was found for the password-change route in the same window.
| Route | Purpose | Real traffic (90-day window) |
|---|---|---|
POST /admin/v1/import-users | Bulk user import | 28 hits (21 study, 7 prod, 0 dev), 2026-05-27 to 2026-07-20, all from curl/8.7.1 or bruno-runtime from 3 static IPs |
POST /admin/v1/change-user-password | Change user password | 0 hits in the same window |
Core Data Entities
Collection names below are read directly from each repo's Go source (db.Collection("...") constants), not inferred from route names.
QUESTION_POOL and RESULT_PER_ASSESSMENT are each a stand-in for 7 separate per-assessment MongoDB collections, not single collections — the actual names are:
c2fit-bff(dbc2fit-bff, open the repository map →):user_profile,delete_account_request,journey,streak,bundle,app_version. Session/OTP data (OTPSession,ForgotPasswordSession) lives in Redis, not MongoDB. Notifications (email/SMS) go directly to AWS SES/SNS — no notification collection.c2fit-assessment-result(dbassessment-result, 15 collections): 7 question/reference collections (question_attention_hold_release,question_digit_span_speak,question_modified_visuospatial,symbol_set_modified_visuospatial,question_drawing_memory,question_verbal_memory,question_trail_making_test) +assessment_path, and 7 result collections (result_attention_hold_release,result_digit_span_speak,result_modified_visuospatial,result_drawing_memory,result_verbal_memory,result_trail_making_test_char,result_trail_making_test_dice).- Foreign-key-style relationships above (e.g.
journey.userId→user_profile) are inferred from field/domain naming, not from a schema/index definition — MongoDB is schemaless and no explicit relationship documentation exists.
Async vs Real-Time
| Flow | Mode | Path |
|---|---|---|
| Assessment submission (all 6) | Synchronous request/response | Mobile app → BFF (proxy) → Result Service → (external ASR call inline for audio tasks) → Mongo → response |
| Live captions while speaking (Digit Span Speak, Verbal Memory) | Real-time streaming | Mobile app --WSS--> BFF /ws/v1/speech --WS proxy--> global-asr-service |
| Journey state polling | Synchronous | GET /api/v1/journey/state |
| Streak status | Synchronous, parallel per-day fetch | fetchStreakStatuses() fires Future.wait over N days, each with independent cache policy (past days cached indefinitely, today's re-fetched every 15s) |
The inline ASR call in the synchronous submit path is a direct, blocking call through global-asr-service's Go client (score_digit_span_speak_eng.go) — no message queue (Kafka/SQS) exists in c2fit-assessment-result, unlike digital-thai-moca's Kafka-based async AI scoring. Open deployment and infrastructure for the full data-flow breakdown →.
Build Flavors as Environment Variants
c2fit has no staff/patient dual-frontend split like digital-thai-moca (single mobile app only). Instead, environment variation happens through 3 Flutter build flavors, each mapped to a distinct Firebase project. Open the repository map for the exact values →.
releaseDev—c2fit-app-devreleaseProd—c2fit-appreleaseStudy—c2fit-app-studyFirebase project, app id suffixed.study; version conventionx.y.z-study-N
Each flavor's BASE_URL is baked in via generate_env.sh at CI build time (not switched at runtime), and each points at a genuinely separate backend environment: dedicated ECS services, task definitions, and Cognito user pools exist per environment. Dev, prod, and study run as distinct services on the c2fit-backend ECS cluster. Open deployment and infrastructure for the environment details →. Whether "study" (research cohort) has any functional behavior difference beyond app identity/backend isolation (e.g. different consent copy, different assessment set) is tracked in unknown.md — no flavor-conditional business logic was found in the frontend docs read for this pass, and infrastructure separation alone doesn't imply behavioral differences.