Last verified against the codebase: April 8, 2026
This guide is for contributors working on Trajectory's desktop app codebase. It is intentionally practical: what exists today, how it fits together, and how to extend it safely.
- Node.js 22+
- Rust stable toolchain
- macOS (primary supported platform for local dev)
- GitHub Actions for preview/release builds on macOS, Windows, and Linux
npm ci
npm run tauri devnpm run checkThis runs:
- TypeScript typecheck (
npm run typecheck) - Rust check (
cargo check --manifest-path src-tauri/Cargo.toml)
npm run tauri buildArtifacts are created under:
src-tauri/target/release/bundle/macos/*.appsrc-tauri/target/release/bundle/dmg/*.dmgsrc-tauri/target/release/bundle/nsis/*.exesrc-tauri/target/release/bundle/msi/*.msisrc-tauri/target/release/bundle/deb/*.debsrc-tauri/target/release/bundle/appimage/*.AppImage
Trajectory is a local-first desktop app for exploring activity files.
Current stack:
- Shell/runtime: Tauri v2
- Backend: Rust
- Frontend: React + TypeScript + Vite + Tailwind
- Storage: SQLite + JSON settings
Supported activity files:
.tcx.txc(accepted alias).fit
Important behavior:
- The selected import folder is treated as read-only input.
- App data is stored in app-owned directories (
activities.sqlite,settings.json). - No cloud backend. No telemetry service.
- Map views still fetch external map tiles (OSM/CARTO) at runtime.
| Path | Purpose |
|---|---|
src/ |
React frontend |
src-tauri/ |
Tauri shell + Rust backend |
docs/DEVELOPER_GUIDE.md |
This document |
.github/workflows/ci.yml |
Typecheck + Rust quality gate |
.github/workflows/preview-bundles.yml |
Manual preview bundle pipeline for artifact testing |
.github/workflows/release.yml |
Tag-based macOS/Windows/Linux release pipeline |
.
├── src/
│ ├── components/ # shared UI building blocks
│ ├── lib/ # Tauri bridge wrappers and frontend utilities
│ ├── pages/ # route-level page components
│ ├── store/ # Zustand app/UI/analytics state
│ ├── types.ts # frontend contract mirror for Rust DTOs
│ └── App.tsx # app bootstrap, routing, startup scan flow
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Tauri command registration and app wiring
│ │ ├── models.rs # Rust DTOs shared across commands/modules
│ │ ├── scanner.rs # file discovery + incremental/full scan logic
│ │ ├── parser.rs # TCX/FIT parsing into normalized activities
│ │ ├── db.rs # SQLite schema, migrations, and query layer
│ │ ├── analytics.rs # advanced analytics computation
│ │ └── settings.rs # settings load/save and defaults
│ ├── Cargo.toml
│ └── tauri.conf.json
├── docs/
│ └── DEVELOPER_GUIDE.md
└── .github/
└── workflows/
├── ci.yml
├── preview-bundles.yml
└── release.yml
flowchart LR
UI[React Pages + Zustand Stores] --> Bridge[src/lib/tauri.ts]
Bridge --> TauriInvoke[Tauri invoke commands]
TauriInvoke --> Main[src-tauri/src/main.rs]
Main --> Scanner[scanner.rs]
Main --> DB[db.rs]
Main --> Analytics[analytics.rs]
Main --> Settings[settings.rs]
Scanner --> Parser[parser.rs]
Scanner --> DB
DB --> SQLite[(activities.sqlite)]
Settings --> SettingsFile[(settings.json)]
Scanner --> Events[scan:progress / scan:done]
Events --> UI
src/App.tsx handles:
- app bootstrap (
useAppStore.init()) - dark/light theme application
- accent theme CSS variable application
- automatic startup scan (once per selected import folder path)
- startup advanced analytics cache warm-up after a completed scan
- route rendering via
HashRouter
Current routes:
| Route | Page |
|---|---|
/ |
DashboardPage |
/activities |
ActivitiesPage |
/activities/:id |
ActivityDetailPage |
/heatmap |
HeatmapPage |
/analytics |
AdvancedAnalyticsPage |
/settings |
SettingsPage |
src/lib/tauri.ts defines command wrappers and event listeners.
Current wrappers:
getSettingssetImportFoldersetDarkModesetAccentThemesetHeatmapFullOpacitysetChartMaxSamplessetChartOutlierRemovalsetHeartRateZoneUpperBoundsBpmscanImportFolderlistActivitiesgetActivitygetActivitySamplesgetHeatmapDatarunAdvancedAnalyticsexportAnalyticsJsononScanProgress
| Store | File | Responsibility |
|---|---|---|
| App/runtime state | src/store/useAppStore.ts |
settings, scan lifecycle, in-memory activity cache, in-memory analytics cache |
| Persisted UI state | src/store/useUiStateStore.ts |
filters, tabs, navigation memory, dashboard mode, heatmap controls |
| Persisted analytics definitions | src/store/useAdvancedAnalyticsStore.ts |
metrics/streaks/charts definitions, selection, auto-run |
- Onboarding: directory picker + recursive toggle.
- Settings: tabs for Import, Appearance, Athlete Metrics.
- Import actions: incremental rescan and full cache clear + rescan.
- Appearance includes chart sample cap slider, chart outlier-removal toggle, and heatmap opacity preference.
- Athlete Metrics manages heart-rate zone cutoffs.
- Dashboard: year/month calendar views with aggregate metrics and drilldowns.
- Activities: filter + sort table, navigation into details.
- Activity Detail:
- fetches summary/track via
getActivity - fetches chart samples via
getActivitySamples - re-queries samples when zoom window, pause visibility, or
chartMaxSampleschanges - GPS activities can switch between distance and time charts; time charts can collapse explicit paused segments into moving time
- fetches summary/track via
- Heatmap: map overlay rendering with date/category/sport filters.
- Advanced Analytics: custom metrics/streaks/charts builder + preview, selective JSON import/export.
Responsibilities:
- initialize app state and storage paths
- initialize SQLite schema
- ensure default settings file exists
- register Tauri command handlers
- validate input settings (accent theme, chart sample limits, HR zones)
activities.sqlitestores normalized activity summaries plus per-sample data.- Activity rows also track a parser version and serialized pause segments.
- Incremental scans reparse unchanged source files automatically when the parser version changes, so importer fixes can roll forward without requiring a manual full rescan.
Registered Tauri commands:
| Command | Purpose |
|---|---|
get_settings |
Read settings JSON |
set_import_folder |
Set folder + recursive scan option |
set_dark_mode |
Update theme mode |
set_accent_theme |
Update accent palette |
set_heatmap_full_opacity |
Toggle heatmap opacity mode |
set_chart_max_samples |
Persist chart sample cap |
set_chart_outlier_removal |
Toggle robust outlier suppression in Activity Detail charts |
set_heart_rate_zone_upper_bounds_bpm |
Save Z1-Z4 upper bpm limits (Z5 is everything above Z4) |
scan_import_folder |
Run incremental/full scan |
list_activities |
Query activity list |
get_activity |
Get activity summary + route track |
get_activity_samples |
Query/downsample chart samples |
get_heatmap_data |
Return heatmap tracks |
run_advanced_analytics |
Compute analytics payload |
export_analytics_json |
Write exported analytics JSON file |
What it does:
- discovers activity files (
.tcx,.txc,.fit) - supports recursive/non-recursive scan modes
- canonicalizes paths when possible
- compares
(source_path, source_mtime, source_size)for incremental detection - prunes deleted source files from DB on incremental scans
- supports full rebuild (
full_rescan = true) - emits progress/done events:
scan:progress{ parsed, total, currentFile }scan:done{ added, updated, skipped, errors }
Parses TCX and FIT into normalized activity models.
Notable behavior:
- derives category/title fallbacks
- computes duration, moving time, distance, elevation, speed, HR stats
- supports summary-only FIT import if no point records are present
- parses cadence and power when available
- downsamples stored route track to
MAX_UI_POINTS = 2000 - preserves full sample rows for DB insert (sampling happens at query time)
Responsibilities:
- schema creation and migrations (
DB_SCHEMA_VERSION = 2) - upsert activity + sample rows
- query list/detail/heatmap/sample windows
- query-side downsampling
Tables:
activitiesactivity_samples
Recent schema/migration coverage includes:
category,title,min_hr,moving_duration_seconds- sample-level
cadence,power_watts
Sampling notes:
get_activityreturns summary + track (no samples)get_activity_samplesreturns filtered/downsampled sample windows- default chart sample cap is
2000, clamped in Rust (50..=20000) - settings validation in
main.rsaccepts100..=20000
load_settings(path)reads JSON, falling back to defaults when missing.save_settings(path, settings)writes formatted JSON.
Default settings (current):
scanRecursive: truedarkMode: falseaccentTheme: "citrus-orange"heatmapFullOpacity: falsechartMaxSamples: 2000chartOutlierRemoval: trueheartRateZoneUpperBoundsBpm: [120, 140, 160, 180]
Mirror files:
- Rust:
src-tauri/src/models.rs - TypeScript:
src/types.ts
When changing any command payload or DTO:
- Update Rust model(s) in
src-tauri/src/models.rs. - Update backend behavior (
main.rs,db.rs,analytics.rs, etc.). - Update TypeScript interfaces in
src/types.ts. - Update bridge wrappers in
src/lib/tauri.ts. - Update UI/store consumers.
Serialization uses camelCase mapping (serde(rename_all = "camelCase")).
App-managed files are created using Tauri path APIs:
- DB: app data directory, file
activities.sqlite - settings: app config directory, file
settings.json
Design choices:
- tracks are stored as JSON in
activities.track_json(map-friendly payload) - detailed samples are stored in
activity_samples - UI-heavy views fetch sampled windows rather than always loading all points
- App loads settings.
- If import folder exists,
App.tsxtriggers a scan once for that folder path. - UI receives scan progress events.
- On completion, caches are invalidated and
lastScanTimestampupdates. - App optionally pre-warms advanced analytics cache for active definitions.
- Triggered in Settings -> Import ->
Rescan. - Uses incremental behavior.
- Triggered in Settings -> Import ->
Clear Cache + Full Rescan. - Clears
activities+activity_samples, then reimports all files.
npm cinpm run tauri devfor developmentnpm run checkfor quality gatenpm run tauri buildfor local production bundles
- frontend quality job on Ubuntu:
npm cinpm run typecheck
- rust quality job on Ubuntu, macOS, and Windows:
- Linux runner installs Tauri system dependencies (
libwebkit2gtk-4.1-dev,libappindicator3-dev,librsvg2-dev,patchelf) cargo check --manifest-path src-tauri/Cargo.tomlcargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check
- Linux runner installs Tauri system dependencies (
Triggered manually from GitHub Actions.
Builds downloadable artifacts without creating a GitHub release:
- macOS:
.app+.dmg(optional) - Windows:
.exe(NSIS) +.msi - Linux:
.deb+.AppImage
Use this workflow to validate that a branch is release-ready and hand the generated bundles to testers before tagging a real release.
Triggered by tags matching v*.
Pipeline verifies version alignment across:
- git tag (without
vprefix) package.jsonsrc-tauri/tauri.conf.jsonsrc-tauri/Cargo.toml
Then builds and publishes platform bundles via tauri-apps/tauri-action:
- macOS:
.app+.dmgwith ad-hoc signing - Windows:
.exe(NSIS) +.msi - Linux:
.deb+.AppImage
- Define/extend DTOs in
src-tauri/src/models.rs. - Implement behavior in backend modules.
- Add
#[tauri::command]inmain.rsand register it. - Add typed wrapper in
src/lib/tauri.ts. - Mirror types in
src/types.ts. - Consume in store/page/component.
- Add default + field in Rust
Settings. - Add setter command in
main.rs. - Mirror in TypeScript
Settingsinterface. - Add UI controls and app-store update action.
- Use the setting in rendering/query logic.
- Update Rust + TS filter models.
- Extend SQL predicates in
db::list_activitiesand/ordb::get_heatmap_data. - Pass through bridge wrapper.
- Wire into UI state and page controls.