Quantified Self
Quantified Self is an open-source platform for collecting, analyzing, and visualizing fitness and health data. It combines activity imports and connected fitness services with dashboards, training insights, route tools, sleep trends, and detailed workout analysis.
Try the hosted app at quantified-self.io.
Highlights
- Import activity files in FIT, GPX, TCX, JSON, and SML formats.
- Connect Garmin, Suunto, COROS, and Wahoo for supported activity, route, sleep, and history workflows.
- Explore configurable dashboards, training readiness, load trends, power curves, intensity zones, laps, and durability metrics.
- View activities and saved routes with Mapbox-powered maps and route tools.
- Compare recordings from multiple devices, share selected activities, and export your data.
- Ask grounded fitness-data questions through the built-in Assistant.
Technology and repository layout
| Area | Technology |
| --- | --- |
| Frontend | Angular 20, Angular Material, RxJS |
| Backend | Firebase Auth, Firestore, Functions, Storage, Hosting, App Check, and Remote Config |
| Visualization | ECharts and Mapbox GL JS |
| Activity parsing | @sports-alliance/sports-lib |
| Testing | Vitest and Firebase Rules Unit Testing |
The main repository areas are:
| Path | Purpose |
| --- | --- |
| src/app/ | Angular application, routes, components, and browser services |
| functions/src/ | Firebase Functions, queues, integrations, and scheduled jobs |
| shared/ | Contracts and helpers shared by the browser and Functions runtimes |
| extensions/ | Firebase Extension configuration |
| docs/ | Architecture, product rules, and operational documentation |
| Repository root | Firebase rules, indexes, Hosting configuration, and build tooling |
Prerequisites
For frontend tests and the repository's full development workflow, install:
- Git.
- Node.js 22, matching the CI test jobs and Cloud Functions runtime.
- npm, which is included with Node.js.
- Gitleaks, used by the pre-commit credential scan.
- A Mapbox public access token for maps and geocoding.
- Firebase CLI, installed at the repository's pinned version below.
- Java 21, matching the CI environment.
[!NOTE]
The root.nvmrcstill selects Node 20 for local frontend development. Select Node 22 explicitly withnvm use 22for frontend tests and Functions development; the CI test jobs use Node 22 andfunctions/package.jsondeclares the same runtime.
Quick start
1. Clone and install
git clone https://github.com/jimmykane/quantified-self.git
cd quantified-self
nvm install 22
nvm use 22
npm ci
npm --prefix functions ci
npm run firebase:tools
The root application and Functions use separate lockfiles, so both installs are required for the full development workflow.
2. Add a local Mapbox token
The local Angular build expects a file that is intentionally excluded from Git. Create src/environments/mapbox-token.local.ts with your own public token:
export const mapboxAccessToken = 'YOUR_PUBLIC_MAPBOX_TOKEN';
Do not copy a maintainer token or commit this file. A valid token is required for map and geocoding features.
3. Understand the local Firebase boundary
[!WARNING]
The current development configuration is hybrid, not fully isolated. Callable Functions are routed to the Functions emulator, but browser Auth, Firestore, Storage, Analytics, App Check, and Remote Config still use the configured hosted Firebase project. Starting additional emulators does not connect those browser SDKs automatically.
Use a dedicated development Firebase project and test account for authenticated work. Do not perform writes until you have confirmed which project the browser is using. Development credentials placed in functions/.secret.local can also call real provider APIs, and Cloud Tasks uses its configured external API unless a task emulator host is supplied.
The Functions emulator does not require a service-account JSON or Google Application Default Credentials when Admin SDK traffic stays on the Auth, Firestore, and Storage emulators. The shared Admin bootstrap uses the emulator hosts locally and the assigned runtime identity in Cloud Functions. Do not add a service-account key to the Functions source tree.
4. Build Functions and start the emulators
In the first terminal:
npm --prefix functions run build
npm run firebase -- emulators:start --only auth,functions,firestore,storage
The Functions emulator loads compiled output from functions/lib, so the build must finish before the emulators start.
Keep the TypeScript compiler running in another terminal while editing Functions or shared backend code:
npm --prefix functions run build:watch
The emulator reloads the emitted JavaScript; it does not compile TypeScript source changes itself. After switching branches or changing templates, rerun the full build. A new callable missing from a stale build can appear as a browser CORS/preflight failure while existing callables still work. Check the emulator's initialized endpoint list and rebuild before changing CORS policy. Angular's hot reload does not refresh the Functions build.
For the default browser configuration, which keeps hosted Auth, Firestore, and Storage, run just the Functions emulator:
npm run firebase -- emulators:start --only functions
Admin impersonation in this mode signs tokens with an existing, Git-ignored quantified-self-io-firebase-adminsdk.json at the repository root, outside functions/. The key must match the emulator's project and is used only for custom-token signing; the default Admin app keeps ADC. Alternatively, set GOOGLE_APPLICATION_CREDENTIALS to an external service-account JSON file before starting the emulator. See Functions-only emulation with hosted Auth for credential precedence and isolation.
5. Start Angular
In a second terminal:
npm run start:functions:emu -- --ssl=false
Open:
- Application: http://localhost:4200
- Firebase Emulator UI: http://localhost:4000
--ssl=false override provides a predictable fresh-clone path without relying on a local trusted certificate. If you need HTTPS for an integration flow, generate and trust your own localhost certificate rather than reusing or sharing private key material. The certs/ directory is ignored and must never be committed:
mkdir -p certs
umask 077
openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 825 \
-keyout certs/localhost.key -out certs/localhost.crt \
-subj '/CN=localhost' \
-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1'
Public pages such as /, /help, /integrations, and /tools/compare are useful first smoke tests. Authenticated flows additionally require correctly configured Firebase Auth providers, authorized domains, and App Check settings.
npm start and npm run start:functions:emu start Angular only; neither command starts Firebase emulators. Avoid npm run start:functions:prod during normal contributor work because it routes callable requests to the configured hosted Functions.
Optional backend and provider configuration
Copy functions/.secret.local.example to the ignored functions/.secret.local only when an emulator or operational script needs provider credentials. Add development-only values for the integration you are testing; builds and unit tests do not require the file. See Firebase Function secret management for binding, deployment, and rotation rules.
Operational scripts that intentionally access managed Google Cloud or Firebase resources use Application Default Credentials. Authenticate your local user with gcloud auth application-default login, or use an approved least-privilege service-account impersonation flow, and pass the target project explicitly to any write-capable script. Emulator-only development does not require this login.
| Feature | Configuration names |
| --- | --- |
| Garmin | GARMINAPI_CLIENT_ID, GARMINAPI_CLIENT_SECRET |
| Suunto | SUUNTOAPP_CLIENT_ID, SUUNTOAPP_CLIENT_SECRET, SUUNTOAPP_SUBSCRIPTION_KEY, SUUNTOAPP_NOTIFICATION_SECRET |
| Suunto Training Guides | Reuses the existing Suunto OAuth credentials and SUUNTOAPP_SUBSCRIPTION_KEY with Guides access; SUUNTOAPP_GUIDE_OWNER stores the exact existing OAuth application name through Secret Manager (or .secret.local for emulators). See Training delivery. |
| COROS | COROSAPI_CLIENT_ID, COROSAPI_CLIENT_SECRET |
| Wahoo | WAHOOAPI_CLIENT_ID, WAHOOAPI_CLIENT_SECRET, WAHOOAPI_WEBHOOK_TOKEN, WAHOOAPI_ALLOWED_FILE_HOSTS |
| Stripe | STRIPE_SECRET_KEY, STRIPE_ADMIN_BILLING_KEY, STRIPE_BILLING_KEY, STRIPE_BILLING_WEBHOOK_SECRET |
| Built-in Assistant | GEMINI_API_KEY |
| Backend Mapbox access | MAPBOX_ACCESS_TOKEN |
| Optional task emulator | CLOUD_TASKS_EMULATOR_HOST |
| Release source maps | SENTRY_AUTH_TOKEN |
Never commit environment files, service-account JSON, API tokens, private keys, decrypted credentials, personal data, or production exports. Emulator code can still reach external services when real credentials are configured.
Development commands
| Purpose | Command | Notes |
| --- | --- | --- |
| Frontend tests once | npm test -- --run | Deterministic command used by CI |
| Frontend tests in watch mode | npm test | Keeps Vitest running |
| Frontend coverage | npm run test-coverage | Writes the coverage report locally |
| Frontend lint | npm run lint | Angular ESLint |
| Firestore and Storage Rules tests | npm run test:rules | Uses the isolated demo-test emulator project |
| Frontend build | npm run build | Development build |
| Production build | npm run build-production | Builds locally; does not deploy |
| Functions tests | npm --prefix functions test | One-shot unit suite; registered emulator files run separately |
| Functions coverage | npm --prefix functions run test:coverage | Writes the Functions coverage report |
| Functions build | npm --prefix functions run build | Compiles TypeScript to functions/lib |
| Functions emulator tests | npm run test:functions-emulators -- lifecycle | Builds first; use no argument for all four isolated demo groups |
| Emulator CI coverage guard | npm run test:emulator-coverage | Rejects missing/duplicate suites, unsafe environments and skipped coverage |
| CI workflow regression tests | npm run test:workflows | Fork/internal trigger routing and the required aggregate test result |
| Training monitoring configuration | npm run test:training-monitoring | Offline metric/dashboard/alert definitions and safe, idempotent provisioning |
| Activity import monitoring configuration | npm run test:import-monitoring | Offline four-provider dashboard/policies; no production changes by default |
| Health/Sleep monitoring configuration | npm run test:health-sleep-monitoring | Offline ordinary-ingestion/serialized-backfill dashboard and policies; no production changes by default |
| Recorded-activity delivery monitoring configuration | npm run test:activity-delivery-monitoring | Offline outbound delivery dashboard/policies, including pending and manual-reconciliation outcomes; no production changes by default |
| Route import/delivery monitoring configuration | npm run test:route-monitoring | Offline route dashboard, bounded labels, eight policies and safe provisioning; no cloud changes by default |
| Connection-history monitoring configuration | npm run test:connection-history-monitoring | Offline coordinator dashboard/six policies, bounded observations and safe provisioning; activation needs separate approval |
| Email delivery monitoring configuration | npm run test:email-monitoring | Offline Firestore-mail/extension/marketing dashboard and seven policies; no extra queue or monitoring collection; activation needs separate approval |
| Derived-metrics monitoring configuration | npm run test:derived-metrics-monitoring | Offline ingress/calculation dashboard and six policies; bounded pending-generation observation; activation needs separate approval |
| Sports Lib reprocessing monitoring | npm run test:reparse-monitoring | Offline normal/heavy/route dashboard and seven policies; existing scanners only; activation needs separate approval |
| Account cleanup monitoring | npm run test:cleanup-monitoring | Offline cleanup/disconnect dashboard and seven policies, including failed partner-revocation email; no added Firestore reads, indexes or Function; activation needs separate approval |
| Functions lint | npm --prefix functions run lint | Runs ESLint with --fix and may edit files |
| Install Git hooks | npm run hooks:install | Reinstalls the repository Lefthook hooks; npm ci normally installs them automatically |
| Test the local credential guard | npm run credentials:test | Checks the staged-file rejection policy without reading credential values |
| MCP pre-push checks | npm run hooks:mcp:pre-push | Runs the registered-contract gate and focused MCP output/server tests |
| Local plugin tooling | npm run plugin:tools | Installs the isolated pinned Codex CLI dependency |
| Local plugin setup | npm run plugin:setup | Explicitly builds, validates, registers, and installs the configured plugin |
| Local plugin validation | npm run plugin:validate | Uses an isolated temporary Codex profile |
| Local plugin refresh | npm run plugin:sync | Explicitly rebuilds and reinstalls; normal app builds never do this |
The repository-local Quantified Self plugin bundles the registered permission-scoped MCP app with seven discoverable workflows
for cross-domain analysis, all-day Health, training, sleep, body measurements, activities, and saved routes. Manual Health
entry lookup/logging/editing/permanent deletion needs independent measurements:write authorization and client approval;
historical read permissions never grant writes and imports remain immutable. Configure its
account-specific ChatGPT technical app ID once by setting QS_CHATGPT_APP_ID and running
npm run plugin:configure, then run npm run plugin:setup. Generated app mappings and cache-busted manifests are
ignored. Restart the ChatGPT desktop app after setup or sync, then test in a new conversation. See the
MCP server documentation for the complete lifecycle and update matrix.
Deployment and self-hosting
The deployment scripts and Firebase aliases in this repository target maintainer-managed environments. Do not run deployment commands against them as part of ordinary contributor setup.
Self-hosting is an advanced workflow rather than a turnkey installation. A fork must provide and review its own:
- Firebase project, client configuration, CLI aliases, Hosting and Storage targets, and indexes.
- Auth providers, authorized domains, redirect URIs, App Check, and Remote Config.
- Function regions, Cloud Tasks queues, buckets, allowed origins, and email URLs.
- Firebase Extension instances and Secret Manager bindings.
- Garmin, Suunto, COROS, Wahoo, Stripe, Gemini, Mapbox, email, and observability credentials used by enabled features.
Data retention and policies
The hosted project uses Firestore TTL policies for short-lived operational data:
| Collection | Retention | TTL field | Purpose |
| --- | --- | --- | --- |
| mail | About 90 days | expireAt | Transactional email records |
| aiInsightsPromptRepairs | Until the retired AI Insights purge completes | expireAt | Temporary TTL protection for historical prompt-repair data; no active writer remains |
| failed_jobs | 7 days | expireAt | Failed background-job records |
| *Queue | 7 days | expireAt | Temporary queue items |
| adminStats | About 1 hour | expireAt | Admin aggregate cache |
| adminDashboardSnapshots | 730 days | expireAt | Aggregate-only daily user, authentication-activity, plan, and subscription-cadence history for the Admin Dashboard |
| userDeletionTombstones | Account-deletion retention window | expireAt | Deletion guards with TTL fallback cleanup |
| mcpOAuthAuthorizationRequests / mcpOAuthAuthorizationCodes | 10 / 5 minutes | expireAt | MCP OAuth consent and single-use codes |
| mcpOAuthAccessTokens / mcpOAuthRefreshTokens | 1 hour / 30 days | expireAt | Hashed MCP bearer and refresh credentials |
| mcpOAuthRateLimits | About 5 minutes | expireAt | Distributed MCP request counters |
| users/*/mcpConnections | 5 minutes while pending | expireAt | Abandoned MCP approvals; successful exchanges remove the TTL field |
| users/*/assistantConversations | 7 days after the latest completed turn or reset; an active turn is protected for at most 4 extra minutes | expireAt | One bounded server-owned active Assistant conversation plus private, unindexed replay fingerprints per user |
| users/*/eventMergeOperations | 7 days after the latest state transition | expireAt | Event-merge idempotency and reconciliation ledger |
| users/*/activitySyncOutboundFingerprints | About 120 days | expireAt | Server-only exact/semantic FIT receipts that suppress provider-returned activity echoes |
These policies are infrastructure configuration; starting local emulators does not create or deploy production TTL policies.
Training delivery jobs/ledgers are an exception to short-lived queues: unfinished delivery/withdrawal evidence has no TTL. Account deletion cleans them recursively, including UID-associated top-level jobs. Delivery receipts carry a 30-day expiry; their production TTL setup and rollout remain separate approval-gated work. See the Training delivery foundation.
Unified health history under users/{uid}/healthSourceRecords and healthSampleChunks has no time-based TTL. It is bounded per document, atomic replacement, and query; provider disconnect retains imported history, while recursive account deletion removes the complete user-scoped health subtree. See Unified health data foundation.
Architecture documentation
- Analytics collection, attribution boundaries, and rollout checks
- Firestore stats index allowlist, query audit and two-phase rollout
- Benchmark comparison generation and reparse preservation
- CI test coverage and emulator isolation
- Helper test environment benchmark
- Component fixture benchmark
- Reusable event tag catalog and backfill
- Firebase Functions target-aware entrypoint loading
- Temporary Garmin dispatch cost measurements
- Optional connection history import and shared header/Dashboard progress
- Admin dashboard aggregate user history
- Unified health data foundation
- Private Timeline notes
- Existing-user Health backfill operations
- Garmin Health integration
- Provider integration implementation guide
- Stripe billing architecture, local testing and cutover
- Stripe-backed first payments, trial conversion and paid retention
- Suunto 24/7 Health integration and rollout
- COROS integration architecture and release checklist
- Wahoo integration architecture and release checklist
- Training workspace architecture and maintenance
- Derived-metrics queue monitoring and activation
- Sports Lib activity and route reprocessing monitoring
- Account cleanup and disconnect recovery monitoring
- Recorded activity import monitoring and activation
- Activity ascent/descent corrections and source preservation
- Activity sending workspace and recovery
- Recorded activity delivery monitoring and activation
- Route import and delivery monitoring
- Connection-history imports and coordinator monitoring
- Frontend UI composition and shared route headers
- Event summary readers and performance
- Event chart sport defaults and visibility persistence
- Supported activities and metrics catalog
- MCP-backed built-in Assistant
- Activity Calendar architecture and maintenance
- MCP server
- Firebase Function secret management
- Pinned Firebase CLI and deployment commands
- Queue processing architecture
- Sleep sync operations
- Email lifecycle
- Firebase Auth link-domain routing
- Connected-provider attribution audit
- Event merge idempotency and recovery
- Pricing and usage limits
- User deletion workflow
- Delete User Data extension replacement and recovery
Contributing
Contributions are welcome. Start with a good first issue, or ask a question in GitHub Discussions. Before opening a pull request:
- Keep changes focused and add or update the narrowest relevant tests.
- Run the applicable checks from the table above.
- Use a prefixed commit subject:
feat:,fix:,chore:,refactor:,test:, ordocs:. - Follow the Code of Conduct.
License
Unless a file or directory states otherwise, original Quantified Self source code—including the Firebase Functions
backend—is licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). Third-party
packages and assets remain under their respective licenses. Release tags retain versioned source, and the hosted
interfaces link directly to the source repository and license.
Original frontend code whose copyright holders have authorized it also carries a narrow AGPL section 7 exception for linking or combining with Mapbox GL JS. The exception does not license Mapbox materials: users must obtain any required Mapbox account and access token and follow Mapbox's terms.
Icons by Alessandro.