Profile
Back to NewsBack
GitHub Trending 14 min
Reader Mode
jimmykane/quantified-self: Analyze your data from Garmin, Suunto, Coros, Wahoo and more to one centralized app

jimmykane/quantified-self: Analyze your data from Garmin, Suunto, Coros, Wahoo and more to one centralized app

4 hours ago

Quantified Self

!Testing

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.
For Firebase emulators and Rules tests, also install:
  • Firebase CLI, installed at the repository's pinned version below.
  • Java 21, matching the CI environment.
[!NOTE]
The root .nvmrc still selects Node 20 for local frontend development. Select Node 22 explicitly with nvm use 22 for frontend tests and Functions development; the CI test jobs use Node 22 and functions/package.json declares 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:

The --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.
Audit all project-specific identifiers and domains before deploying a fork. Deployment, publishing, and cloud configuration changes should always be deliberate, separate operations.

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

Contributing

Contributions are welcome. Start with a good first issue, or ask a question in GitHub Discussions. Before opening a pull request:

  1. Keep changes focused and add or update the narrowest relevant tests.
  2. Run the applicable checks from the table above.
  3. Use a prefixed commit subject: feat:, fix:, chore:, refactor:, test:, or docs:.
  4. Follow the Code of Conduct.
Security-related guidance is available in SECURITY.md.

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.

Chat with me