EN | PL
Advanced cycling training analysis application powered by Strava data. Self-hosted, dark mode UI, metrics: CTL/ATL/TSB, power curve, zones, EF, monotony.
- System Requirements
- Strava API Setup
- Environment Configuration (.env)
- Running — Docker Compose (production)
- Running — Developer Mode
- First Use — Connecting to Strava
- Data Import (Sync)
- Navigating the App
- API Reference — Key Endpoints
- Running Tests
- Troubleshooting
| Component | Minimum Version |
|---|---|
| Docker | 24+ |
| Docker Compose | 2.20+ |
| Java (dev) | 21 LTS |
| Node.js (dev) | 20+ |
| PostgreSQL | 16 + PostGIS 3.4 |
Note: Docker Compose starts the database automatically — no local PostgreSQL installation required.
Before running the application, you need to create an app in the Strava developer portal:
- Go to https://developers.strava.com and log in with your Strava account
- Click "Create & Manage Your App" in the top menu
- If you don't see the option, go directly to: https://www.strava.com/settings/api (requires login)
- Fill in the form:
- Application Name:
Strava Training Analyzer(any name) - Category:
Training Analysis - Club: (optional)
- Website:
http://localhost:5173 - Authorization Callback Domain:
localhost
- Application Name:
- After creation, copy:
- Client ID → paste into
.envasSTRAVA_CLIENT_ID - Client Secret → paste into
.envasSTRAVA_CLIENT_SECRET
- Client ID → paste into
Important: Callback Domain must be set to
localhost— the app uses redirect URI:http://localhost:8080/api/auth/strava/callback
Copy .env.example to .env and fill in the values:
cp .env.example .env.env contents:
# === Database ===
DB_PASSWORD=your-secure-password
# === Strava OAuth2 ===
STRAVA_CLIENT_ID=12345 # From Strava API portal
STRAVA_CLIENT_SECRET=abc123def456... # From Strava API portal
STRAVA_WEBHOOK_TOKEN=random-webhook-token # Any random string
# === Security ===
JWT_SECRET=minimum-32-char-random-secret
ENCRYPTION_KEY=64-char-hex-aes-256-key# JWT Secret (32+ chars)
openssl rand -base64 32
# Encryption Key (32 bytes = 64 hex chars)
openssl rand -hex 32The easiest way to run the entire application:
# 1. Clone the repository
cd strava-training-analyzer
# 2. Configure .env (see section 3)
cp .env.example .env
# edit .env with your values
# 3. Start everything
docker compose up -d
# 4. Check status
docker compose psAfter startup:
| Service | URL |
|---|---|
| Frontend | http://localhost |
| Backend | http://localhost:8080 |
| Swagger | http://localhost:8080/swagger-ui.html |
| Database | localhost:5432 |
docker compose down # stop containers
docker compose down -v # stop + remove data (WARNING: deletes database!)docker compose logs -f # all services
docker compose logs -f backend # backend only
docker compose logs -f frontend # frontend only
docker compose logs -f db # database onlyIn developer mode you run each component separately with hot-reload:
docker compose up -d dbWait for the healthcheck (database ready after ~10s):
docker compose ps # status: healthycd backend
# Set environment variables or use .env
export DB_PASSWORD=your-password
export STRAVA_CLIENT_ID=12345
export STRAVA_CLIENT_SECRET=abc123
# Start Spring Boot
./gradlew bootRunBackend starts at http://localhost:8080.
Flyway automatically runs database migrations on startup.
cd frontend
# Install dependencies (first time)
npm install
# Start dev server
npm run devFrontend starts at http://localhost:5173 with hot-reload.
Vite proxy automatically forwards /api/* requests to the backend on port 8080.
After starting the application you need to connect it to your Strava account:
Go to http://localhost:5173 (dev) or http://localhost (Docker).
Call the connect endpoint (from browser or cURL):
GET http://localhost:8080/api/auth/strava/connect
The response contains the authorization URL:
{
"url": "https://www.strava.com/oauth/authorize?client_id=12345&redirect_uri=http://localhost:8080/api/auth/strava/callback&response_type=code&scope=read,activity:read_all,profile:read_all"
}Open that URL in your browser → Strava will ask for authorization → After approval you'll be redirected back to the app.
Check that the profile was saved:
GET http://localhost:8080/api/profile
Should return your data (name, FTP, etc.).
After connecting to Strava you can import your activities.
POST http://localhost:8080/api/sync/strava/fullExample with Swagger UI or any HTTP client:
POST http://localhost:8080/api/sync/strava/full
Content-Type: application/json
Response (202 Accepted):
{
"synced": 150,
"failed": 0,
"status": "COMPLETED"
}Note: The first full import may take a few minutes depending on the number of activities in your Strava account. Strava API limits: 100 req/15min, 1000 req/day.
POST http://localhost:8080/api/sync/strava/recentFetches only new activities since the last sync. Use this for daily updates.
GET http://localhost:8080/api/sync/statusAfter importing activities, metrics (NP, TSS, IF, zones, power curve, CTL/ATL/TSB) are calculated automatically by the metric engine (MetricRegistry).
The application has three main sections accessible from the sidebar:
- Weekly summary — distance, time, TSS, elevation
- Training load (30d) — PMC chart (CTL/ATL/TSB)
- Recent activities — last 5 with links to details
- Readiness — placeholder (future Garmin integration)
- Today's recommendation — placeholder (future advisor)
- Activity list — table with sport type filtering
- Click a row → activity detail page
- Activity details (
/activities/:id):- Route map (Leaflet + CARTO dark tiles)
- Metrics: power, heart rate, cadence, distance, time, elevation
- Stream charts (power, HR, cadence, altitude)
- Power and HR zones (horizontal bars)
6 tabs:
| Tab | Description |
|---|---|
| PMC | Performance Management Chart — CTL (fitness), ATL (fatigue), TSB (form) |
| Power Curve | Best efforts 1s–120min, logarithmic scale |
| Zones | Time distribution across power zones (bar chart) |
| Load | Weekly TSS (bar chart) |
| Trends | FTP over time + Efficiency Factor (scatter plot) |
| Compare | Side-by-side comparison of two arbitrary periods (distance, time, elevation) |
Each tab has a date range picker for selecting the date range.
Full interactive documentation: http://localhost:8080/swagger-ui.html
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/auth/strava/connect |
Returns Strava authorization URL |
GET |
/api/auth/strava/callback |
OAuth2 callback (automatic redirect) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/profile |
Get athlete profile |
PUT |
/api/profile |
Update profile (e.g. FTP) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/sync/strava/full |
Full sync of all activities |
POST |
/api/sync/strava/recent |
Sync new activities only |
GET |
/api/sync/status |
Status of last sync |
| Method | Endpoint | Parameters | Description |
|---|---|---|---|
GET |
/api/activities |
?sportType, ?from, ?to |
Activity list with filters |
GET |
/api/activities/{id} |
— | Activity details + metrics + streams |
GET |
/api/activities/{id}/map |
— | Route as GeoJSON |
| Method | Endpoint | Parameters | Description |
|---|---|---|---|
GET |
/api/analytics/pmc |
from, to |
CTL/ATL/TSB series |
GET |
/api/analytics/power-curve |
from, to |
Power curve (best efforts) |
GET |
/api/analytics/zones |
zoneType, from, to |
Zone distribution (power/hr) |
GET |
/api/analytics/weekly |
?weeks=8 |
Weekly summaries |
GET |
/api/analytics/summary |
?period=month |
Aggregated statistics |
GET |
/api/analytics/trends |
metric, from, to |
Metric trend over time |
GET |
/api/analytics/compare |
period1From, period1To, period2From, period2To |
Two-period comparison |
cd backend
./gradlew testCovers:
- Unit tests for metric calculators (NP, TSS, IF, EF, zones, PMC, power curve, monotony)
- ArchUnit tests (hexagonal architecture)
- MockMvc controller tests (Activity, Analytics, Auth, Sync, Profile)
cd frontend
npm test # single run
npm run test:watch # watch mode (auto-rerun)Covers:
- Component tests (Dashboard, Activities, Analytics, charts)
- Utility tests (formatters, theme)
# From the root directory
cd backend && ./gradlew test && cd ../frontend && npm test# Check logs
docker compose logs db
# Check if port 5432 is already in use
ss -tlnp | grep 5432# If schema is out of sync — wipe and recreate (DEV only!)
docker compose down -v
docker compose up -d dbToken expired. The app refreshes tokens automatically, but if the refresh token has expired:
- Delete the profile from the database
- Re-authorize:
GET /api/auth/strava/connect
Strava limits:
- 100 requests / 15 minutes
- 1000 requests / day
Wait 15 minutes and try again. For large activity counts, a full sync may require multiple attempts.
Make sure:
- Backend is running on port 8080
- Frontend (Vite) is configured with proxy
/api→http://localhost:8080 - Check the browser console (F12 → Network)
In developer mode, Vite proxy handles CORS automatically. If you call the API directly (e.g. Postman), CORS is disabled — SecurityConfig allows all requests.
# 1. Set up Strava API (https://developers.strava.com → Create & Manage Your App)
# 2. Copy .env.example → .env and fill in your values
cp .env.example .env
# edit .env
# 3. Start database
docker compose up -d db
# 4. Start backend
cd backend && ./gradlew bootRun &
# 5. Start frontend
cd frontend && npm install && npm run dev &
# 6. Connect to Strava
# Open: http://localhost:8080/api/auth/strava/connect
# Copy the URL from the response and open it in your browser
# 7. Import data
# POST http://localhost:8080/api/sync/strava/full
# 8. Done!
# Open http://localhost:5173This project is licensed under the GNU General Public License v3.0. You are free to use, study, and modify this software. Any derivative works must also be distributed under the GPL v3 license. Commercial use is not prohibited, but proprietary closed-source forks are not allowed.
See LICENSE for the full license text.