Architecture Overview¶
Tempo is a self-hosted running tracker built as a full-stack application with a clear separation between frontend and backend.
System Components¶
Frontend¶
- Framework: Next.js 16 with React 19
- Language: TypeScript
- Styling: Tailwind CSS
- State Management: TanStack Query for server state
- Maps: Leaflet/React-Leaflet for route visualization
- Charts: Recharts for analytics
Backend¶
- Framework: ASP.NET Core 9 Minimal APIs
- Language: C#
- Database: PostgreSQL 16 with JSONB fields for raw workout data
- ORM: Entity Framework Core
- Logging: Serilog
Database¶
- PostgreSQL 16 with JSONB support
- Hybrid storage: Core stats in dedicated columns, raw data in JSONB
- Automatic migrations on startup
Key Architectural Patterns¶
1. Minimal APIs¶
Endpoints are organized in static extension methods that extend WebApplication:
MapWorkoutsEndpoints- Workout management endpointsMapSettingsEndpoints- Settings and configuration endpointsMapShoesEndpoints- Shoe management endpointsMapAuthEndpoints- Authentication endpointsMapVersionEndpoints- Version information endpoints
Each extension method:
- Creates a MapGroup for route organization (e.g., /workouts/*, /settings/*, /auth/*)
- Uses WithTags() for Swagger documentation grouping
- Defines endpoints as private static methods with XML documentation comments
- Uses dependency injection from the service container
- Maps private methods to HTTP verbs using MapGet, MapPost, MapPut, MapPatch, MapDelete
- All endpoint methods return Task<IResult> and use Results.* helper methods
2. Service Layer¶
Parser services handle file format conversion:
- GpxParserService - Parses GPX XML files
- FitParserService - Parses binary FIT files (uses FIT SDK)
- StravaCsvParserService - Parses Strava export CSV files
The FIT SDK is included as source files in api/Libraries/FitSDK/ and compiled directly into the project (not a NuGet package).
All services are registered as Scoped in Program.cs except for configuration objects (MediaStorageConfig, ElevationCalculationConfig) which are Singleton.
3. Hybrid Data Storage¶
- Core Stats: Stored in dedicated database columns for efficient querying
- Raw Data: Stored as JSONB in PostgreSQL for flexibility
- Allows querying both structured and unstructured data efficiently
4. Media Storage¶
- Files stored on filesystem in
media/directory - Organized by workout GUID:
media/{workoutId}/filename.ext - Metadata stored in database for quick access
5. Automatic Migrations¶
Database migrations run automatically on API startup. The DatabaseMigrationHelper implements idempotent migrations that:
- Create the __EFMigrationsHistory table if missing
- Detect existing tables and columns by querying information_schema
- Mark migrations as applied if their corresponding tables/columns already exist
- Use hardcoded mappings to associate database objects with migration IDs
This ensures migrations can be safely applied even when database state doesn't match migration history.
6. Logging¶
- Serilog configured for structured logging
- Console output in development
- Request logging enabled via
UseSerilogRequestLogging()
Data Model¶
Core Entities¶
- Workout: Core entity with stats (distance, pace, elevation, heart rate, etc.) and JSONB fields for raw GPX/FIT/Strava data
- WorkoutRoute: One-to-one relationship storing GeoJSON LineString coordinates
- WorkoutSplit: One-to-many relationship for distance-based splits (km or mile)
- WorkoutTimeSeries: One-to-many relationship for time-series data (heart rate, pace, elevation over time)
- WorkoutMedia: One-to-many relationship for photos/videos attached to workouts
- Shoe: Running shoe entity for tracking shoe mileage and assignments
- User: User accounts for authentication
- UserSettings: Single-row table for user preferences (heart rate zones, unit preferences, default shoe)
Data Flow¶
Workout Import Flow¶
- File uploaded to
POST /workouts/import - File type detected (GPX, FIT, or CSV)
- Appropriate parser service extracts data
- Weather data fetched from Open-Meteo API based on workout location/time
- Elevation data smoothed using configurable thresholds
- Splits calculated based on unit preference (1km for metric, 1 mile for imperial)
- Default shoe assigned (if configured in UserSettings)
- Workout saved to database with raw data in JSONB fields
- Route stored as GeoJSON LineString in
WorkoutRoutetable
Bulk Import Flow¶
- ZIP file uploaded to
POST /workouts/import/bulk - ZIP extracted and validated
activities.csvparsed for metadata- Workout files processed from
activities/folder - Only "Run" activities imported
- Duplicate detection using
StartedAt,DistanceM, andDurationS - Default shoe assigned to each workout (if configured in UserSettings)
- All workouts saved to database
Authentication¶
- JWT-based authentication with httpOnly cookies
- Registration only available when no users exist (single-user deployment)
- Password hashing using BCrypt
- All workout and settings endpoints require authentication (except
/healthand/version)
Database Indexing¶
The TempoDbContext configures several important indexes:
- Workout indexes: StartedAt, composite index on (StartedAt, DistanceM, DurationS) for duplicate detection
- JSONB GIN indexes: On RawGpxData, RawFitData, RawStravaData, and Weather fields
- WorkoutSplit: Composite index on (WorkoutId, Idx)
- WorkoutTimeSeries: Composite index on (WorkoutId, ElapsedSeconds)
- User: Unique index on Username
Key File Locations¶
- API Endpoints:
api/Endpoints/*.cs - Models:
api/Models/*.cs - Services:
api/Services/*.cs - Database Context:
api/Data/TempoDbContext.cs - Frontend API Client:
frontend/lib/api.ts - Frontend Pages:
frontend/app/*/page.tsx - Frontend Components:
frontend/components/*.tsx