Skip to content

MyHeart Counts Firebase Project Repository

License

Notifications You must be signed in to change notification settings

StanfordBDHG/MyHeartCounts-Firebase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Build and Test CodeQL Deployment

My Heart Counts

Firebase cloud hosting infrastructure for the Stanford MyHeart Counts project.

The iOS Application can be found in the StanfordBDHG/MyHeartCounts-iOS repository, the repository for the data analysis side of this study can be found over at StanfordBDHG/MyHeartCounts-DataAnalysis.

The study itself with its contents is defined in StanfordBDHG/MyHeartCounts-StudyDefinitions.

Key features of the backend infrastructure include:

  • User account setup using blocking functions
  • Decoding of archived sensor- and healthdata
  • Questinaire parsing
  • User State Handeling
  • Physical Activity Trial with personalized coaching messages using a Large Language Model (LLM) to generate personalized physical activity nudges in a blind study approach to compare predefined nudges and LLM nudges

Note

Do you want to learn more about the Stanford Spezi Template Application and how to use, extend, and modify this application? Check out the Stanford Spezi Template Application documentation.

Data Structure

My Heart Counts Firebase makes extensive usage of both the Firestore Database (NoSQL cloud database) and Firebase Cloud Storage (object storage service).

Variables in the Data Structure

Variable Origin Example
{USER-ID} The Firebase-Generated Account User-ID vqzvMTfki9hD0yqTcVVW8XsKf6g2
{UUID} Randomly generated Sample ID BCD7D622-0CDC-4194-A008-3452C9C95546
{HEALTHKIT.IDENTIFIER} HealthKit Identifier / HKQuantityTypeIdentifier HKClinicalTypeIdentifierAllergyRecord
{SENSORKIT.IDENTIFIER} Sensor identifier name from the SensorKit Framework com.apple.SensorKit.ambientPressure
{MHCCUSTOM.IDENTIFIER} Custom Sample Type defined for the My Heart Counts Study MHCHealthObservationTimedWalkingTestResultIdentifier
{TIMESTAMP} ISO 8601 Timestamp, delimited by an underscore (_) for time ranges 2025-11-17T22:44:09Z_2025-11-17T23:44:09Z

Firestore Database

Path Purpose Fields
/feedback/{UUID} Collection for Participant-Submitted Feedback accountId, appBuildNumber, appVersion, date, deviceInfo (model, osVersion, systemName), message, timeZone (identifier)
/users/{USER-ID} User Document biologicalSexAtBirth, bloodType, comorbidities (Disease : year), dateOfBirth, dateOfEnrollment, didOptInToTrial, disabled, fcmToken, futureStudies, heightInCM, householdIncomeUS, language, lastActiveDate, lastSignedConsentDate, lastSignedConsentVersion, latinoStatus, mhcGenderIdentity, mostRecentOnboardingStep, participantGroup, preferredNotificationTime, preferredWorkoutTypes, raceEthnicity, timeZone, usRegion, weightInKG
/users/{USER-ID}/questionnaireResponses/{UUID} FHIR questionnaire responses See FHIR questionnaireresponse documentation
/users/{USER-ID}/notificationBacklog/{UUID} Backlog of Notifications to send body, category, generatedAt, id, isLLMGenerated, timestamp, title
/users/{USER-ID}/notificationHistory/{UUID} History of send notifications body, errorMessage, generatedAt, isLLMGenerated, originalTimestamp, processedTimestamp, status, title
/users/{USER-ID}/notificationTracking/{UUID} Tracks the Notification Status event, notificationId, timeZone, timestamp
/users/{USER-ID}/SensorKitObservations_deviceUsageReport/{UUID} Debug Info about Sensor Kit Hardware Environment FHIR Observation for custom MHC sample
/users/{USER-ID}/HealthObservations_{HEALTHKIT.IDENTIFIER}/{UUID} FHIR Observation for given health kit type See FHIR observation documentation
/users/{USER-ID}/HealthObservations_{SENSORKIT.IDENTIFIER}/{Timestamp} FHIR Observation for given sensor kit type See FHIR observation documentation

Firebase Cloud Storage

Path Purpose
/public/mhcStudyBundle.spezistudybundle.aar This it the Study definition bundle auto-build by the workflow in MyHeartCounts-StudyDefinitions
/user/{USER-ID}/consent PDF Files of every consent the user gave (this could be multiple in the case of consent revisions or re-signup by the user.)
/user/{USER-ID}/historicalHealthSamples/{HEALTHKIT.IDENTIFIER}{UUID}.json.zstd We collect health samples that were recorded before the user enrolled into the app, compress them via zstd and store them as-is in the folder historicalHealthSamples for future analytics
/user/{USER-ID}/liveHealthSamples/{UUID}.json.zstd Most recorded ongoing (new) health samples get directly uploaded into the Firestore NoSQL Database - however, if a large amount of data has accumulated, we archive these samples for server-side decoding and upload them into liveHealthSamples. This folder will be empty most of the time! On Upload, the function onArchivedLiveHealthSampleUploaded.ts gets triggered which upon successful unpacking and storing into the Firestore Database deletes the live health sample archive.
/user/{USER-ID}/SensorKit/{SENSORKIT.IDENTIFIER}/{UUID}.csv.zstd Samples from Apple's SensorKit Framework, sorted in sub-folders.

Development

This section contains developer information to kickstart local- and cloud development using the ressources from this repository.

Infrastructure Overview

To use Firebase functions for your own project or to emulate them for client applications, this section will help to give an overview of the different packages in use and how to install, build, test and launch them.

This repository contains two separate packages.

  • The package located in functions/models contains model types including decoding/encoding functions and useful extensions that are shared between the Firebase functions.
  • The package located in functions contains the Firebase functions and services that are called from these functions. This package has a local dependency on the package in functions/models. Therefore, the functions package does not work (e. g. for linting, building, etc) without building the models package first.

Project Scripts

To make this structure simpler to use, we provide different scripts as part of the package.json file in the root directory of this repository. The file ensures execution order between the two packages. We only document the scripts located in this file, since they cover the most common use cases, feel free to have a look at the individual package.json files of the respective packages to get a deeper understanding and more package-focused operations.

Command Purpose
npm run install Installs dependencies (incl. dev dependencies) for both packages.
npm run clean Cleans existing build artifacts for both packages.
npm run build Builds both packages. If you have added or removed files in one of the packages, make sure to clean before using this command.
npm run lint Lints both packages. Make sure to build before using this command. You may want to append :fix to fix existing issues automatically or :strict to make sure the command does not succeed with existing warnings or errors.
npm run prepare Combines cleaning, installing and building both packages.
npm run test:ci Tests the Firebase functions with emulators running and with test coverage collection active.
npm run serve:seeded Starts up the relevant emulators for MyHeart Counts and seeds them. Make sure to build the project first before executing this command.

For using the emulators for client applications, it is probably easiest to call npm run prepare whenever files could have changed (e.g. when changing branch or pulling new changes) and then calling npm run serve:seeded to start up the emulators in a seeded state. Both of these commands are performed in the root directory of this repository.

Otherwise, you may want to use Docker to run the emulators. For this, you can use the following command:

docker compose up

This can be especially useful if you're using an operating system like Windows, as scripts contain OS-specific commands that may not work the same way across different platforms.

Testing

We aim for 70% test covarage in this project. Please be sure to rebuild the project after making changes by running npm run prepare or npm run build before executing npm run test:ci.

Deployment Overview

For this study, we choose to have three environments to test, stage and then run the code in production:

Data Flows

Questionaire Processing

flowchart TD
    A[User uploads Questionnaire Response] -->|Firestore write event| B[onUserQuestionnaireResponseWritten]
    B -->|Converts Firestore data| C[TriggerService.questionnaireResponseWritten]
    C -->|Determines if new/updated| D{After document exists?}

    D -->|No| E[End - Document deleted]
    D -->|Yes| F[MultiQuestionnaireResponseService.handle]

    F -->|Iterates through components| G[DietScoringService]
    F -->|Iterates through components| H[NicotineScoringService]
    F -->|Iterates through components| I[HeartRiskNicotineScoringService]
    F -->|Iterates through components| J[HeartRiskLdlParsingService]

    G -->|Checks questionnaire URL| K{Matches Diet questionnaire?}
    K -->|Yes| L[Calculate Diet Score]
    K -->|No| M[Skip - Return false]

    H -->|Checks questionnaire URL| N{Matches Nicotine questionnaire?}
    N -->|Yes| O[Extract smoking status]
    N -->|No| P[Skip - Return false]

    I -->|Checks questionnaire URL| Q{Matches Heart Risk Nicotine?}
    Q -->|Yes| R[Process Heart Risk Nicotine]
    Q -->|No| S[Skip - Return false]

    J -->|Checks questionnaire URL| T{Matches LDL questionnaire?}
    T -->|Yes| U[Parse LDL values]
    T -->|No| V[Skip - Return false]

    L -->|Score calculated| W[Create FHIR Observation]
    O -->|Convert to score 0-4| X[Create FHIR Observation]
    R -->|Process data| Y[Create FHIR Observation]
    U -->|Parse cholesterol data| Z[Create FHIR Observation]

    W -->|Store in Firestore| AA[users/USER-ID/HealthObservations_MHCCustomSampleTypeDietMEPAScore]
    X -->|Store in Firestore| AB[users/USER-ID/HealthObservations_MHCCustomSampleTypeNicotineExposure]
    Y -->|Store in Firestore| AC[users/USER-ID/HealthObservations_MHCCustomSampleTypeHeartRiskNicotine]
    Z -->|Store in Firestore| AD[users/USER-ID/HealthObservations_MHCCustomSampleTypeLDL]

    AA --> AE[Log Success]
    AB --> AE
    AC --> AE
    AD --> AE
    AE --> AF[Return handled status]

    M --> AF
    P --> AF
    S --> AF
    V --> AF

    AF --> AG{Any service handled?}
    AG -->|Yes| AH[Log: Handled questionnaire response]
    AG -->|No| AI[Log: No handler found]

    AH --> AJ[End]
    AI --> AJ
    E --> AJ

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#fff4e1
    style F fill:#ffe1f5
    style G fill:#e1ffe1
    style H fill:#e1ffe1
    style I fill:#e1ffe1
    style J fill:#e1ffe1
    style W fill:#f5e1ff
    style X fill:#f5e1ff
    style Y fill:#f5e1ff
    style Z fill:#f5e1ff
    style AA fill:#ffe1e1
    style AB fill:#ffe1e1
    style AC fill:#ffe1e1
    style AD fill:#ffe1e1
    style AJ fill:#d3d3d3
Loading

User Signup Blocking Function

flowchart TD
    A[Firebase Auth Event] -->|beforeUserCreated| B[Extract userId & email]
    B --> C{Email present?}
    C -->|No| D[Throw auth/invalid-email]
    C -->|Yes| E[userService.enrollUserDirectly]
    E --> F[Create user document in Firestore]
    F --> G[Trigger userEnrolled event]
    G --> H[Return custom claims]

    I[Firebase Auth Event] -->|beforeUserSignedIn| J[Extract userId]
    J --> K[userService.getUser]
    K --> L[Retrieve user document]
    L --> M{User found?}
    M -->|Yes| N[Extract custom claims]
    M -->|No| O[Return empty claims]
    N --> P[Return claims & session claims]
    O --> P

    F -->|Store in| Q[users/USER-ID]
    D --> R[End - Signup blocked]
    H --> S[End - User enrolled]
    P --> T[End - Sign-in allowed]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style E fill:#ffe1f5
    style F fill:#f5e1ff
    style G fill:#ffe1f5
    style Q fill:#ffe1e1
    style I fill:#e1f5ff
    style K fill:#ffe1f5
    style L fill:#f5e1ff
Loading

Generate Nudges Function

flowchart TD
    A[Scheduled: Daily 08:00 UTC] --> B[Fetch all users from Firestore]
    B --> C[Filter users with triggerNudgeGeneration]
    C --> D{User in trial & opted in?}
    D -->|No| E[Skip user]
    D -->|Yes| F[Check participantGroup & days enrolled]

    F --> G{Days since enrollment?}
    G -->|7 days| H[Generate predefined nudges]
    G -->|14 days, Group 1| H
    G -->|14 days, Group 2| I[Call OpenAI GPT-5.2]

    I --> J[Build personalized context]
    J -->|age, diseases, stage, education, language| K[LLM generates 7 nudges]
    K --> L{LLM success?}
    L -->|No, retry 3x| M[Continue retries]
    M --> L
    L -->|Yes| N[Parse LLM response]

    H --> O[Select 7 predefined messages]
    N --> P[Validate message structure]
    O --> Q[Schedule 7 nudges]
    P --> Q

    Q --> R[Write to notificationBacklog]
    R -->|For each nudge| S[users/USER-ID/notificationBacklog/UUID]
    S -->|Fields| T[title, body, timestamp, category, isLLMGenerated, generatedAt]

    T --> U[Reset triggerNudgeGeneration: false]
    U --> V[Log processed count]
    E --> V
    V --> W[End]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style I fill:#ffe1f5
    style K fill:#ffe1f5
    style H fill:#e1ffe1
    style R fill:#f5e1ff
    style S fill:#ffe1e1
    style W fill:#d3d3d3
Loading

Archived Sample Functions

flowchart TD
    A[File upload event] -->|users/USER-ID/liveHealthSamples/filename| B[Extract userId from path]
    B --> C[Download compressed file]
    C --> D[Decompress with fzstd]
    D --> E[Parse JSON content]

    E --> F{Validate structure}
    F -->|Invalid| G[Log error & delete file]
    F -->|Valid| H[Extract observations array]

    H --> I{Parse filename}
    I -->|SensorKit pattern| J[Map to SensorKitObservations_dataType]
    I -->|HealthKit pattern| K[Map to HealthObservations_identifier]

    J --> L[Batch write 500 docs at a time]
    K --> L
    L -->|Store in| M[users/USER-ID/collection/observationId]

    M --> N[Delete processed file from Storage]
    N --> O[Log observation count]
    G --> O
    O --> P[End]

    style A fill:#e1f5ff
    style C fill:#fff4e1
    style D fill:#fff4e1
    style E fill:#ffe1f5
    style L fill:#f5e1ff
    style M fill:#ffe1e1
    style N fill:#ffe1f5
    style P fill:#d3d3d3
Loading

Send Nudges Function

flowchart TD
    A[Scheduled: Every 15 minutes] --> B[Fetch all users]
    B --> C[For each user: Read notificationBacklog]
    C --> D{Backlog items exist?}
    D -->|No| E[Skip user]
    D -->|Yes| F[Check each item timestamp]

    F --> G{timestamp <= now?}
    G -->|No| H[Keep in backlog]
    G -->|Yes| I[Get user fcmToken]

    I --> J{fcmToken exists?}
    J -->|No| K[Create failed history entry]
    J -->|Yes| L[Send via admin.messaging]

    L --> M{Send successful?}
    M -->|Yes| N[Create sent history entry]
    M -->|No| K

    N -->|Write to| O[users/USER-ID/notificationHistory/UUID]
    K -->|Write to| O
    O -->|Fields| P[title, body, status, processedTimestamp, errorMessage, isLLMGenerated]

    P --> Q[Delete from notificationBacklog]
    Q --> R[Log sent count]
    E --> R
    H --> R
    R --> S[End]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#fff4e1
    style L fill:#ffe1f5
    style N fill:#f5e1ff
    style O fill:#ffe1e1
    style Q fill:#ffe1f5
    style S fill:#d3d3d3
Loading

Delete Account Function

flowchart TD
    A[User calls markAccountForDeletion] --> B{User authenticated?}
    B -->|No| C[Throw unauthenticated error]
    B -->|Yes| D[Extract userId from auth.uid]

    D --> E[userService.getUser]
    E --> F{User document exists?}
    F -->|No| G[Throw not-found error]
    F -->|Yes| H{User already disabled?}

    H -->|Yes| I[Throw failed-precondition error]
    H -->|No| J[Update user document]
    J -->|Set fields| K[toBeDeleted: true, markedForDeletionAt: timestamp]

    K -->|Write to| L[users/USER-ID]
    L --> M[Return success response]
    M -->|Fields| N[success: true, markedAt: ISO timestamp]

    C --> O[End - Error thrown]
    G --> O
    I --> O
    N --> P[End - Account marked]

    style A fill:#e1f5ff
    style D fill:#fff4e1
    style E fill:#ffe1f5
    style J fill:#f5e1ff
    style L fill:#ffe1e1
    style P fill:#d3d3d3
Loading

Bulk Deletion of Samples Function

flowchart TD
    A[User calls deleteHealthSamples] --> B{User authenticated?}
    B -->|No| C[Throw unauthenticated error]
    B -->|Yes| D[Validate input schema]

    D --> E{userId, collection, documentIds present?}
    E -->|No| F[Throw invalid-argument error]
    E -->|Yes| G{documentIds.length <= 50000?}
    G -->|No| F
    G -->|Yes| H{User has permission for userId?}

    H -->|No| I[Throw permission-denied error]
    H -->|Yes| J[Generate jobId]
    J --> K[Return immediate response]
    K -->|Fields| L[status: accepted, jobId, totalSamples, estimatedDurationMinutes]

    L --> M[Start async background processing]
    M --> N[Batch documentIds into groups of 500]
    N --> O[For each batch: Retrieve documents]
    O --> P[Update document status]
    P -->|Set field| Q[status: entered-in-error]

    Q -->|Update in| R[users/USER-ID/collection/documentId]
    R --> S[100ms delay between batches]
    S --> T{More batches?}
    T -->|Yes| O
    T -->|No| U[Log completion & stats]

    C --> V[End - Error thrown]
    F --> V
    I --> V
    U --> W[End - Samples marked]

    style A fill:#e1f5ff
    style D fill:#fff4e1
    style J fill:#ffe1f5
    style M fill:#ffe1f5
    style P fill:#f5e1ff
    style R fill:#ffe1e1
    style W fill:#d3d3d3
Loading

Contributing

Contributions to this project are welcome. Please make sure to read the contribution guidelines and the contributor covenant code of conduct first.

License

This project is licensed under the MIT License. See Licenses for more information.

Stanford Biodesign Footer Stanford Biodesign Footer

About

MyHeart Counts Firebase Project Repository

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published

Contributors 6