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.
My Heart Counts Firebase makes extensive usage of both the Firestore Database (NoSQL cloud database) and Firebase Cloud Storage (object storage service).
| 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 |
| 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 |
| 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. |
This section contains developer information to kickstart local- and cloud development using the ressources from this repository.
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.
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 upThis 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.
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.
For this study, we choose to have three environments to test, stage and then run the code in production:
- My Heart Counts Development serves as the internal testing playground for iterating rapidly. Deployed to manually via CLI, not via a pipeline.
- tds/development is the staging environment hosted by Stanford Technology and Digital Solutions of the School of Medicine and Stanford Health Care. We publish to this environment via the CI pipeline on push to main and make sure that every setting matches the production environment 1:1 (Service Account Rules, Notification Settings, Tokens, API Keys).
- tds/production is the production environment of the My Heart Counts Study in the US. It is also hosted by Stanford Technology and Digital Solutions of the School of Medicine and Stanford Health Care. We publish here via the CI pipeline on release, in sync if needed with the iOS deployment.
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
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
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
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
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
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
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
Contributions to this project are welcome. Please make sure to read the contribution guidelines and the contributor covenant code of conduct first.
This project is licensed under the MIT License. See Licenses for more information.

