A modern web application for discovering, creating, and viewing cycling routes stored as GPX files. Built with Vue.js frontend, FastAPI backend, and PostgreSQL for data persistence.
Gravly helps cyclists discover their next gravel adventure by providing:
- 🗺️ Interactive map discovery with real-time segment streaming
- ✏️ Advanced GPX editor with surface classification and difficulty ratings
- 🚴 Strava integration for seamless activity import
- ⚙️ Wahoo Cloud integration for route upload and management
- 📍 Route planner with interactive waypoint management
- 📊 Elevation profiles and detailed route analytics
- 🔐 Secure authorization system for editor access control
📸 Screenshots coming soon - showing the interactive map, route editor, and segment details
# 1. Install Pixi
curl -fsSL https://pixi.sh/install.sh | bash
# 2. Install dependencies
pixi run frontend-install
# 3. Setup database
pixi run pg-setup
# 4. Configure environment (see Environment Configuration section)
mkdir -p .env
# Create .env/database, .env/storage, .env/strava, .env/auth_users, .env/server files
# 5. Start the application (in separate terminals)
pixi run start-backend # Terminal 1: http://localhost:8000 (or configured port)
pixi run start-frontend # Terminal 2: http://localhost:3000 (or configured port)
# 6. (Optional) Seed test data
pixi run python scripts/test_seeding.py- Screenshots
- Quick Start
- Features
- Tech Stack
- Prerequisites
- Setup Instructions
- Environment Configuration
- Database Seeding
- Project Structure
- API Endpoints
- Development
- Architecture & Performance
- Testing
- Continuous Integration
- Key Features Implemented
- Future Enhancements
- Troubleshooting
- Contributing
- License
- Support
- Acknowledgments
- Interactive Map Discovery: Real-time streaming of cycling segments using Server-Sent Events (SSE) with Leaflet maps
- GPX Route Editor: Upload and edit GPX files with visual segment selection, surface type classification, and difficulty rating
- Segment Detail View: Comprehensive segment information with interactive maps, elevation charts, and detailed metadata
- Advanced Segment List: Filterable and sortable segment lists with track type filtering (segments vs routes)
- Real-time Segment Streaming: Efficient bounds-based search with client-side GPX parsing for optimal performance
- Map State Persistence: Automatic saving and restoration of map position and zoom level
- Multi-language Support: Internationalization with English and French locales
- Responsive Design: Works seamlessly on desktop and mobile devices
- Advanced Route Analysis: Elevation profiles, distance calculations, and trail condition metadata
- Media Management: Upload and manage images and videos for cycling segments
- Flexible Storage: Support for both local filesystem and AWS S3 storage backends
- Database Seeding: Automated generation of realistic test data for development and testing
- Strava Integration: Import GPX files directly from Strava activities with OAuth authentication
- Wahoo Cloud Integration: Upload routes to Wahoo Cloud devices with OAuth authentication and GPX to FIT conversion
- Route Planner: Interactive route planning tool with waypoint management, elevation profiles, and route generation
- Frontend: Vue.js 3, TypeScript, Leaflet, Chart.js, Vitest
- Backend: FastAPI, Python, SQLAlchemy, Pydantic
- Database: PostgreSQL (async with asyncpg)
- Storage: Local filesystem (development) or AWS S3 (production)
- Testing: pytest (backend), Vitest + Vue Test Utils (frontend)
- Environment: Pixi (conda-based environment management)
- Code Quality: Ruff (Python), ESLint (TypeScript), TypeScript strict mode
- Pixi (for environment management)
- PostgreSQL (installed via Pixi - see setup instructions)
-
Install Pixi (if not already installed):
curl -fsSL https://pixi.sh/install.sh | bash -
Install dependencies and setup:
# Install frontend dependencies pixi run frontend-install # Setup PostgreSQL database pixi run pg-setup
-
Start the application:
# Terminal 1: Start backend pixi run start-backend # Terminal 2: Start frontend pixi run start-frontend
-
Access the application:
- Frontend: http://localhost:3000 (or configured FRONTEND_PORT)
- Backend API: http://localhost:8000 (or configured BACKEND_PORT)
- API Documentation: http://localhost:8000/docs (or your backend URL)
-
Seed the database with test data (optional):
# Generate 1,000 realistic cycling segments across France pixi run python scripts/database_seeding.py # Or generate just 5 segments for quick testing pixi run python scripts/test_seeding.py
This project uses separate .env files for database, storage, server, and API
configuration. The backend automatically loads environment variables from
.env/storage, .env/database, and .env/server files.
-
Create the environment files in the
.env/directory:mkdir -p .env
-
Create the database configuration file (
.env/database):# Database configuration DB_HOST=localhost DB_PORT=5432 DB_NAME=cycling DB_USER=postgres DB_PASSWORD=your_secure_password_here -
Create the storage configuration file (
.env/storage):# For local storage (development) STORAGE_TYPE=local LOCAL_STORAGE_ROOT=../scratch/local_storage LOCAL_STORAGE_BASE_URL=http://localhost:8000/storageOr for S3 storage:
# For S3 storage (production/testing) STORAGE_TYPE=s3 AWS_S3_BUCKET=your-bucket-name AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key AWS_REGION=us-east-1 -
Create the Strava configuration file (
.env/strava):# Strava API Configuration STRAVA_CLIENT_ID=your_client_id_here STRAVA_CLIENT_SECRET=your_client_secret_here STRAVA_TOKENS_FILE_PATH=/secure/path/to/strava_tokens.json -
Create the authorization configuration file (
.env/auth_users):# Editor Authorization - Authorized Strava Users AUTHORIZED_STRAVA_USERS=820773,123456,789012 -
Create the Wahoo configuration file (
.env/wahoo):# Wahoo API Configuration WAHOO_CLIENT_ID=your_client_id_here WAHOO_CLIENT_SECRET=your_client_secret_here WAHOO_CALLBACK_URL=http://localhost:3000/wahoo/callback WAHOO_SCOPES=user_read routes_write -
Create the Thunderforest map configuration file (
.env/thunderforest):# Thunderforest Map API Key THUNDERFOREST_API_KEY=your_api_key_here -
Create the server configuration file (
.env/server):# Server Configuration BACKEND_PORT=8000 BACKEND_HOST=0.0.0.0 FRONTEND_PORT=3000 FRONTEND_URL=http://localhost:3000 BACKEND_URL=http://localhost:8000 -
Never commit actual
.envfiles - they contain sensitive information and are already in.gitignore.
The storage backend is controlled by the STORAGE_TYPE variable in .env/storage:
STORAGE_TYPE=local- Use local filesystem storageSTORAGE_TYPE=s3- Use AWS S3 storage
The application uses Thunderforest for map tiles. Configuration is set in
.env/thunderforest:
THUNDERFOREST_API_KEY- API key for Thunderforest map tiles (required)
To get an API key:
- Visit Thunderforest
- Sign up for an account
- Create a new API key
- Add the key to your
.env/thunderforestfile
Note: The backend proxies map tile requests to hide the API key from the frontend for security.
The application's server ports and URLs are configured through environment variables in
.env/server:
BACKEND_PORT- Backend server port (default: 8000)BACKEND_HOST- Backend server host (default: 0.0.0.0)FRONTEND_PORT- Frontend development server port (default: 3000)FRONTEND_URL- Frontend URL for CORS and OAuth redirects (default: http://localhost:3000)BACKEND_URL- Backend URL for frontend API calls (default: http://localhost:8000)
Production Deployment: When deploying to production, update these values:
# Example production configuration
BACKEND_PORT=8000
BACKEND_HOST=0.0.0.0
FRONTEND_PORT=80
FRONTEND_URL=https://yourdomain.com
BACKEND_URL=https://api.yourdomain.comThe application uses PostgreSQL for persistent storage. Database connection is
configured through environment variables in .env/database:
DB_HOST- Database host (required)DB_PORT- Database port (required)DB_NAME- Database name (required)DB_USER- Database username (required)DB_PASSWORD- Database password (required)
The application automatically constructs the PostgreSQL connection string from these individual components.
-
Initialize and start PostgreSQL (first time only):
pixi run pg-setup
-
Start PostgreSQL (for subsequent sessions):
pixi run pg-start
-
Stop PostgreSQL:
pixi run pg-stop
-
Check PostgreSQL status:
pixi run pg-status
Note: The database will be initialized in a postgres_data directory in your
project root. This directory should be added to .gitignore (which it already is).
When using local storage, configure these variables in .env/storage:
LOCAL_STORAGE_ROOT- Root directory for storing filesLOCAL_STORAGE_BASE_URL- Base URL for serving files
When using S3 storage, configure these variables in .env/storage:
AWS_S3_BUCKET- S3 bucket nameAWS_ACCESS_KEY_ID- AWS access keyAWS_SECRET_ACCESS_KEY- AWS secret keyAWS_REGION- AWS region (optional, defaults tous-east-1)
The application includes comprehensive Wahoo Cloud integration for uploading routes to Wahoo devices. The integration provides OAuth 2.0 authentication, route upload with GPX to FIT conversion, route management, and seamless integration with the route editor.
-
Create a Wahoo Application:
- Go to Wahoo Developer Portal
- Register your application
- Get your client ID and client secret
-
Configure Environment Variables: Add your Wahoo credentials to the
.env/wahoofile:WAHOO_CLIENT_ID=your_client_id_here WAHOO_CLIENT_SECRET=your_client_secret_here WAHOO_CALLBACK_URL=http://localhost:3000/wahoo/callback WAHOO_SCOPES=user_read routes_write
- Authentication: Users authenticate with Wahoo using OAuth 2.0
- Route Upload: Routes can be uploaded directly from the segment detail view
- GPX to FIT Conversion: GPX files are automatically converted to FIT format for Wahoo devices
- Route Management: Routes are synced between Gravly and Wahoo Cloud
- Automatic Updates: If a route already exists in Wahoo, it's updated instead of creating duplicates
- OAuth 2.0 Authentication: Secure login with Wahoo Cloud
- Database-backed Token Storage: OAuth tokens stored securely in PostgreSQL
- Automatic Token Refresh: Seamless token renewal when access tokens expire
- Route Upload: Direct upload of routes to Wahoo Cloud from segment detail view
- GPX to FIT Conversion: Automatic conversion of GPX files to FIT format
- Route Synchronization: Routes are tracked with external IDs for update support
- Route Deletion: Delete routes from Wahoo Cloud
- Activity Retrieval: Access to Wahoo activities and route data
GET /api/wahoo/auth-url- Get OAuth authorization URLPOST /api/wahoo/exchange-code- Exchange authorization code for access tokenPOST /api/wahoo/refresh-token- Refresh expired access tokenPOST /api/wahoo/deauthorize- Deauthorize and delete tokensGET /api/wahoo/user- Get authenticated user informationGET /api/wahoo/callback- Handle OAuth callbackPOST /api/wahoo/routes/{route_id}/upload- Upload route to Wahoo CloudDELETE /api/wahoo/routes/{route_id}- Delete route from Wahoo Cloud
- OAuth tokens are stored securely in PostgreSQL database
- Client secret is handled securely on the backend only
- All Wahoo API calls are proxied through the backend for security
- Tokens are automatically refreshed before expiration
The application includes comprehensive Strava integration with full OAuth 2.0 authentication, activity retrieval, and GPX data import capabilities. The backend provides a complete Strava API service with secure token management and comprehensive error handling.
-
Create a Strava Application:
- Go to Strava API Settings
- Click "Create App" or "Register Your Application"
- Fill in the application details:
- Application Name: Your cycling website name
- Category: Choose "Web"
- Club: Leave empty or select a club
- Website: Your website URL (e.g.,
http://localhost:5173) - Authorization Callback Domain:
localhost(for development) or your production domain
-
Get Your API Credentials: After creating the app, you'll get:
- Client ID: A number (e.g.,
12345) - Client Secret: A string (e.g.,
abcdef1234567890abcdef1234567890abcdef12)
- Client ID: A number (e.g.,
-
Configure Environment Variables: Add your Strava credentials to the
.env/stravafile (see Environment Configuration section above). Important: TheSTRAVA_TOKENS_FILE_PATHmust be set to a secure location for storing OAuth tokens. This path should be outside your web root and accessible only by your application.
- Authentication: Users click "Login with Strava" in the navbar to authenticate with Strava using OAuth 2.0
- Route Protection: The editor requires Strava authentication - users are automatically redirected to login when accessing protected routes
- Activity Import: Once authenticated, users can import Strava activities directly from the editor's sidebar menu
- Activity List: Users see a list of their cycling activities with previews including distance, time, elevation, and mini-maps
- Import: Users can select any activity to import its GPX data directly into the editor
- Processing: The GPX data is processed the same way as local file uploads, maintaining full compatibility with existing editor functionality
The Strava integration uses a sophisticated authentication system:
- Global Authentication: Login/logout buttons are always visible in the navbar
- Route Protection: Protected routes (like editor) automatically check authentication status
- Smart Redirects: Users are returned to their original page after authentication
- Token Storage: Access and refresh tokens are stored securely on the backend
- Proactive Refresh: Tokens are automatically refreshed 5 minutes before expiration
- Error Handling: If authentication fails, users are automatically redirected back to Strava login
- Full Page Reload: After authentication, the page reloads to ensure navbar updates correctly
- Seamless Experience: Users rarely need to manually re-authenticate
- Global Authentication: Login/logout buttons in the navbar for easy access
- OAuth Authentication: Secure login with Strava using industry-standard OAuth 2.0
- Route Protection: Automatic authentication checks for protected routes
- Smart Redirects: Users return to their original page after authentication
- Automatic Token Refresh: Seamless token renewal when access tokens expire
- Smart Authentication Handling: Automatic redirect to login when authentication fails
- Activity Preview: Visual previews showing distance, time, elevation, and GPS trace for each activity
- GPX Import: Direct import of Strava activity GPX data with elevation and time information
- Seamless Integration: Works with all existing editor functionality including charts, segment selection, and metadata editing
- Backend Processing: GPX data is processed on the backend using the same pipeline as local file uploads
- Editor Authorization: Restrictive access control for editor features based on authorized Strava user IDs
The Strava integration provides a modern, seamless user experience:
- Not Authenticated: Orange "Login with Strava" button in navbar
- Authenticated: User avatar, name, and dropdown menu in navbar
- Token Expired: Automatic refresh or redirect to login
- Logout: Clean return to home page with login button restored
- Context Preservation: Users return to their original page after authentication
- Route Protection: Editor automatically redirects unauthenticated users to login
- Full Page Reload: Ensures navbar updates correctly after authentication
- Seamless Flow: No manual navigation required
- Desktop: Full login button with text and Strava branding
- Tablet: Icon-only login button to save space
- Mobile: Compact authentication interface
- "Invalid redirect_uri": Check that your callback domain in Strava settings matches your development/production domain
- "Invalid client_id": Verify your
STRAVA_CLIENT_IDis correct in.env/strava - "Invalid client_secret": Verify your
STRAVA_CLIENT_SECRETis correct in.env/strava - CORS errors: Make sure your callback domain is properly configured in Strava
- Development: Use
localhostas the callback domain in Strava settings - Production: Use your actual domain (e.g.,
mycyclingapp.com) withouthttp://orhttps://
Strava has rate limits for API calls:
- Default: 1,000 requests per 15 minutes
- Premium: 1,000 requests per 15 minutes (same as default)
The integration is designed to be efficient and should not hit these limits under normal usage.
The backend includes a comprehensive Strava service (backend/src/services/strava.py)
that provides:
- OAuth 2.0 Flow: Complete authentication flow with authorization URL generation and token exchange
- Token Management: Automatic token refresh with secure filesystem storage
- Activity Retrieval: Paginated access to user activities with comprehensive metadata
- GPX Processing: Direct GPX data retrieval and processing from Strava streams
- Athlete Information: Access to authenticated user profile data
- HTTPException Propagation: Proper handling of Strava API errors with correct HTTP status codes
- Authentication Failures: Comprehensive handling of token expiration and refresh scenarios
- Rate Limiting: Proper handling of Strava API rate limits
- Network Errors: Robust error handling for network failures and API unavailability
- Secure Configuration: Environment-based configuration with mandatory token file path
- Token Encryption: Secure storage of OAuth tokens with proper file permissions
- Client Secret Protection: Backend-only handling of sensitive credentials
- Request Validation: Proper validation of all API requests and responses
- Client secret is handled securely on the backend
- OAuth tokens are stored in localStorage (consider using httpOnly cookies for production)
- All Strava API calls are proxied through the backend for security
- Secure Token Storage: OAuth tokens are stored securely in the location specified
by
STRAVA_TOKENS_FILE_PATHenvironment variable for persistence across sessions - Never commit tokens: The token file is automatically ignored by git and should never be committed to version control
- Token Security: The token file path must be set to a secure location outside your web root and accessible only by your application
The project includes comprehensive database seeding scripts to generate realistic test data for development and testing purposes.
scripts/database_seeding.py: Generates 1,000 realistic 5km cycling segments across 13 French regionsscripts/test_seeding.py: Generates 5 segments for quick testingscripts/seed_auth_users.py: Seeds authorized users from.env/auth_usersfile
- Geographic Distribution: Segments distributed across 13 French regions with realistic GPS coordinates
- Realistic Routes: 5km cycling routes with elevation changes and proper GPS point distribution
- Varied Surface Types: Different surface types (paved roads, trails, etc.) with realistic probabilities
- Tire Recommendations: Appropriate tire types for dry and wet conditions
- Difficulty Levels: 1-5 difficulty scale with realistic distribution
- Batch Processing: Processes segments in batches for memory efficiency
- Storage Integration: Uploads GPX files to configured storage (local or S3)
- Database Integration: Stores segment metadata in PostgreSQL
# Full seeding (1,000 segments) - takes several minutes
pixi run python scripts/database_seeding.py
# Quick test seeding (5 segments) - takes seconds
pixi run python scripts/test_seeding.py
# Auth users seeding (if authorization enabled)
pixi run python scripts/seed_auth_users.pyYou can modify the parameters in the main() function of database_seeding.py:
await seed_database(
num_segments=1000, # Number of segments to generate
target_distance_km=5.0, # Distance of each segment in km
batch_size=50 # Number of segments per batch
)The application includes a sophisticated authorization system that controls access to the route editor based on authorized Strava user IDs. This enables selective access to premium editing features while maintaining security.
The authorization system provides fine-grained control over who can access the GPX route editor:
- Strava Authentication: Users must be authenticated with Strava
- Authorization Check: Backend validates authorized user IDs against database
- Frontend UI Control: Editor button only visible to authenticated and authorized users
- Environment Configuration: Authorized users configured via
.env/auth_usersfile
-
Create the authorization configuration file:
# Create from example template cp .env/auth_users.example .env/auth_users -
Configure authorized users in
.env/auth_users:# List of Strava user IDs with editor access # Format: comma-separated Strava IDs (no spaces) AUTHORIZED_STRAVA_USERS=820773,123456,789012
-
Seed the database with authorized users:
# Run the authorization seeder pixi run python scripts/seed_auth_users.py
- Database-driven Control: Authorized users stored in PostgreSQL
auth_userstable - Automatic Checking: Frontend automatically validates authorization on Strava authentication
- UI Hiding: Editor button hidden from unauthorized users (not shown at all)
- Error Prevention: Unauthorized users cannot access editor routes
- Secure Backend: All authorization checks validated server-side
- Environment Configuration: Easy management via
.env/auth_usersfile
- User Authentication: User logs in with Strava OAuth
- Authorization Request: Frontend calls
/api/auth/check-authorization?strava_id=<ID> - Database Check: Backend validates Strava ID against
auth_userstable - Response: Returns authorization status and user information
- UI Update: Editor button appears/disappears based on authorization
- Real-time Updates: Authorization checked on any authentication state changes
GET /api/auth/check-authorization?strava_id=820773
Response:
{
"authorized": true,
"user": {
"strava_id": 820773,
"firstname": "Test",
"lastname": "User"
}
}GET /api/auth/users
Response:
[
{
"id": 1,
"strava_id": 820773,
"firstname": "Test",
"lastname": "User",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]- Database Validation: Server-side authorization checks against PostgreSQL
- Environment Security: No sensitive data hardcoded in repository
- Token Protection: OAuth tokens managed securely by backend
- UI Protection: Frontend UI prevents unauthorized access to editor
- Route Protection: Backend validates authorization on editor access
- Error Handling: Proper HTTP status codes and error messages
- Cache Management: Frontend caching with automatic refresh on auth changes
The .env/auth_users.example template shows:
# Default authorization setup with test user
AUTHORIZED_STRAVA_USERS=820773
# For additional users, use comma separation:
# AUTHORIZED_STRAVA_USERS=820773,123456,789012You can also manually add authorized users directly to the database:
INSERT INTO auth_users (strava_id, firstname, lastname)
VALUES (123456, 'Test', 'User');- Editor button not showing: Verify user's Strava ID exists in
auth_userstable - Database errors: Ensure
auth_userstable has been created (run seeder) - Environment not loaded: Check
.env/auth_usersfile exists and has valid Strava IDs - No authorization response: Check backend authorization endpoints are working
- Authentication failures: Ensure Strava OAuth integration is properly configured
# Check if authorization endpoint is working
curl "http://localhost:8000/api/auth/check-authorization?strava_id=820773"
# Verify authorized users in database
curl "http://localhost:8000/api/auth/users"
# Check if environment is loaded correctly
pixi run python -c "
import os
print('AUTHORIZED_STRAVA_USERS:', os.getenv('AUTHORIZED_STRAVA_USERS', 'Not found'))
"| Feature | Authentication | Authorization |
|---|---|---|
| Purpose | Verify Strava login | Control feature access |
| Scope | Global login state | Specific feature (editor) |
| Storage | OAuth tokens (secure) | Authorized users (database) |
| UI Impact | Login/logout buttons | Editor button visibility |
| Validation | Backend Strava API | Backend authorization DB |
| Security | OAuth 2.0 tokens | Database-driven user list |
# Using pixi (loads from .env/storage and .env/database)
pixi run start-backend
# Or manually
source ../.env/strava && uvicorn backend.src.main:app --reload --host 0.0.0.0 --port 8000# Using pixi
pixi run start-frontend
# Or manually
cd frontend && npm run devFiles are served via the /storage/{file_path} endpoint when using local storage.
Example: http://localhost:8000/storage/gpx-segments/12345.gpx
Files are accessed via presigned URLs when using S3 storage.
- Local Development: Use
STORAGE_TYPE=localfor development without needing AWS credentials - Testing: Tests use moto to mock S3, so they work regardless of storage configuration
- Production: Use
STORAGE_TYPE=s3with proper AWS credentials
scratch/local_storage/
├── gpx-segments/
│ ├── file1.gpx
│ ├── file1.gpx.metadata
│ ├── file2.gpx
│ └── file2.gpx.metadata
└── other-prefix/
└── ...
Each GPX file has a corresponding .metadata file containing:
- file-id
- file-type
- content-type
- original-path
gravly/
├── backend/ # FastAPI backend
│ ├── src/
│ │ ├── main.py # Main API server with streaming endpoints
│ │ ├── models/ # SQLAlchemy models and Pydantic schemas
│ │ │ ├── track.py # Track model and response schemas
│ │ │ ├── image.py # Track image model and schemas
│ │ │ ├── video.py # Track video model and schemas
│ │ │ ├── auth_user.py # Authorized user model and schemas
│ │ │ ├── wahoo_token.py # Wahoo OAuth token model and schemas
│ │ │ └── base.py # Base model configuration
│ │ ├── services/ # Service modules
│ │ │ ├── strava.py # Strava API integration service with OAuth and GPX processing
│ │ │ └── wahoo/ # Wahoo API integration service
│ │ │ ├── client.py # Wahoo API client
│ │ │ ├── service.py # Wahoo service with token management
│ │ │ ├── protocol.py # API protocol definitions
│ │ │ └── exceptions.py # Wahoo-specific exceptions
│ │ └── utils/ # Utility modules
│ │ ├── config.py # Environment configuration
│ │ ├── storage.py # Storage managers (S3/Local)
│ │ ├── gpx.py # GPX parsing utilities
│ │ └── postgres.py # Database configuration
│ └── tests/ # Backend tests with comprehensive coverage
├── frontend/ # Vue.js frontend
│ ├── src/
│ │ ├── components/ # Vue components
│ │ │ ├── Explorer.vue # Interactive map with streaming segments
│ │ │ ├── Editor.vue # GPX route editor with chart visualization
│ │ │ ├── SegmentDetail.vue # Detailed segment view with map and charts
│ │ │ ├── SegmentList.vue # Filterable segment list component
│ │ │ ├── Navbar.vue # Navigation component with Strava authentication
│ │ │ ├── RoutePlanner.vue # Route planning component with waypoint management
│ │ │ ├── RoutePlannerSidebar.vue # Route planner sidebar with filters and controls
│ │ │ ├── StravaCallback.vue # Strava OAuth callback handler
│ │ │ ├── StravaActivityList.vue # Strava activities list component
│ │ │ ├── StravaActivityDetailsModal.vue # Strava activity details modal
│ │ │ └── WahooCallback.vue # Wahoo OAuth callback handler
│ │ ├── composables/ # Vue composables
│ │ │ ├── useMapState.ts # Map state persistence management
│ │ │ ├── useStravaApi.ts # Strava API integration and authentication composable
│ │ │ ├── useStravaActivities.ts # Strava activities management composable
│ │ │ ├── useWahooApi.ts # Wahoo API integration and authentication composable
│ │ │ └── useAuthorization.ts # Editor authorization management
│ │ ├── utils/ # Frontend utilities
│ │ │ ├── gpxParser.ts # Client-side GPX parsing
│ │ │ └── distance.ts # Distance calculation utilities
│ │ ├── types/ # TypeScript type definitions
│ │ ├── i18n/ # Internationalization
│ │ └── assets/ # Images and static assets
│ ├── package.json # Frontend dependencies
│ └── vite.config.js # Vite configuration
├── .env/ # Environment configuration files
│ ├── database # Database configuration
│ ├── storage # Storage configuration
│ ├── strava # Strava API configuration
│ ├── wahoo # Wahoo API configuration
│ ├── thunderforest # Thunderforest map API key
│ ├── auth_users # Editor authorization configuration
│ ├── server # Server port and URL configuration
│ └── strava_tokens.json # Strava OAuth tokens (auto-generated, git-ignored)
├── scripts/ # Database seeding and utility scripts
│ ├── database_seeding.py # Generate 1,000 realistic cycling segments
│ ├── test_seeding.py # Generate 5 test segments
│ ├── seed_auth_users.py # Authorized users for editor access
│ └── README_auth_users.md # Authorization documentation
├── backend/tests/data/ # Test GPX files
├── postgres_data/ # PostgreSQL data directory (created by pg-setup)
├── scratch/ # Local storage directory
├── pixi.toml # Pixi environment configuration
└── README.md
GET /- Root endpoint with API informationGET /storage/{file_path}- Serve GPX files from storage
POST /api/upload-gpx- Upload and parse GPX filesPOST /api/segments- Create new cycling segments from uploaded GPX data
GET /api/strava/auth-url?state={route}- Get Strava OAuth authorization URL with optional state parameter for redirectPOST /api/strava/exchange-code- Exchange Strava authorization code for access tokenPOST /api/strava/refresh-token- Refresh expired Strava access tokenGET /api/strava/activities?page={page}&per_page={per_page}- Get paginated list of Strava activitiesGET /api/strava/activities/{activity_id}/gpx- Get GPX data for a specific Strava activity
GET /api/wahoo/auth-url?state={state}- Get Wahoo OAuth authorization URLPOST /api/wahoo/exchange-code- Exchange Wahoo authorization code for access tokenPOST /api/wahoo/refresh-token- Refresh expired Wahoo access tokenPOST /api/wahoo/deauthorize- Deauthorize and delete tokensGET /api/wahoo/user?wahoo_id={wahoo_id}- Get authenticated Wahoo user informationGET /api/wahoo/callback?code={code}- Handle Wahoo OAuth callbackPOST /api/wahoo/routes/{route_id}/upload?wahoo_id={wahoo_id}- Upload route to Wahoo CloudDELETE /api/wahoo/routes/{route_id}?wahoo_id={wahoo_id}- Delete route from Wahoo Cloud
GET /api/auth/check-authorization?strava_id={strava_id}- Check if Strava user is authorized for editor accessGET /api/auth/users- List all authorized users (admin endpoint)
GET /api/tracks/{track_id}/images- Get images for a specific trackPOST /api/tracks/{track_id}/images- Upload images for a trackDELETE /api/tracks/{track_id}/images/{image_id}- Delete a specific track imageGET /api/tracks/{track_id}/videos- Get videos for a specific trackPOST /api/tracks/{track_id}/videos- Upload videos for a trackDELETE /api/tracks/{track_id}/videos/{video_id}- Delete a specific track video
GET /api/utils/map-tiles/{z}/{x}/{y}.png- Proxy endpoint for Thunderforest map tiles (hides API key from frontend)
Features:
- OAuth 2.0 Authentication: Complete OAuth flow with secure token management (Strava and Wahoo)
- Automatic Token Refresh: Seamless token renewal when access tokens expire
- Activity Retrieval: Paginated access to user's Strava activities
- GPX Data Processing: Direct import and processing of Strava activity GPX files
- Wahoo Route Upload: Upload routes to Wahoo Cloud with GPX to FIT conversion
- Comprehensive Error Handling: Proper HTTP status codes and error messages
- Secure Token Storage: OAuth tokens stored securely in PostgreSQL database
GET /api/segments/search- Stream segments within geographic bounds using Server-Sent EventsOPTIONS /api/segments/search- CORS preflight for streaming endpoint
- Real-time Streaming: Uses Server-Sent Events (SSE) for efficient data delivery
- Bounds-based Search: Geographic filtering with PostgreSQL spatial queries
- Client-side GPX Parsing: Raw XML data streamed to frontend for optimal performance
- CORS Support: Full CORS configuration for cross-origin requests
The application uses Pixi for environment management, which provides:
- Isolated Python environments for backend
- Node.js environment for frontend
- Easy dependency management
- Consistent development environment
# Start backend API server (http://localhost:8000)
pixi run start-backend
# Start frontend dev server (http://localhost:5173)
pixi run start-frontend
# Install frontend dependencies
pixi run frontend-install
# Build frontend for production
pixi run frontend-build# First-time setup (init + start + create)
pixi run pg-setup
# Regular operations
pixi run pg-start # Start PostgreSQL server
pixi run pg-stop # Stop PostgreSQL server
pixi run pg-status # Check PostgreSQL server status
# Database operations
pixi run pg-create-db # Create database
pixi run pg-drop-db # Drop database
pixi run pg-cleanup # Stop, drop, and erase database
pixi run pg-erase # Erase database files# Linting (with auto-fix)
pixi run -e dev lint-backend # Python (Ruff)
pixi run -e dev lint-frontend # TypeScript (ESLint)
pixi run -e dev lint-all # Both backend & frontend
# Formatting
pixi run -e dev format-backend # Python (Ruff)
pixi run -e dev format-frontend # TypeScript (Prettier)
pixi run -e dev format-all # Both backend & frontend
# Type checking
pixi run -e dev type-check-frontend # TypeScript type checking
# Pre-commit hooks
pixi run -e lint install-pre-commit # Install git hooks
pixi run -e lint pre-commit # Run pre-commit checks
pixi run -e lint pre-commit-update # Update pre-commit hooks# Run tests with coverage
pixi run -e dev test-backend # Backend pytest
pixi run -e dev test-frontend # Frontend Vitest
# Watch mode (for development)
cd frontend && npm run test # Frontend watch mode# Quick test seeding (5 segments)
pixi run python scripts/test_seeding.py
# Full seeding (1,000 segments)
pixi run python scripts/database_seeding.py
# Seed authorized users
pixi run python scripts/seed_auth_users.pyNote: Task definitions use Pixi's cwd and depends-on fields for clarity. Use the
-e dev flag to access development environment features (testing, linting).
Gravly is built with a modern, scalable architecture:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Vue.js 3 + │◄────────┤ FastAPI + │◄────────┤ PostgreSQL │
│ TypeScript │ HTTP │ Python 3.10+ │ Async │ Database │
│ Frontend │ SSE │ Backend │ SQL │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Leaflet Maps │ │ Storage Layer │
│ Chart.js │ │ (S3 / Local) │
└─────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ Strava API │
│ OAuth 2.0 │
└──────────────────┘
- GPX Parsing: Moved from backend to frontend for reduced server load
- Streaming Architecture: Server-Sent Events for real-time data delivery
- Bounds-based Filtering: Efficient geographic queries with PostgreSQL
- Client-side Caching: Layer tracking to prevent redundant map redraws
- Distance Calculations: Real-time distance calculations from map center to segments
- Async Database: PostgreSQL with asyncpg for concurrent operations
- Streaming Responses: Real-time segment delivery without blocking
- Storage Abstraction: Unified API for S3 and local storage
- Error Handling: Comprehensive error recovery and logging
- Debounced Search: Prevents excessive API calls during map interactions
- Incremental Updates: Only updates map with new segments
- Zoom Optimization: Avoids unnecessary searches when zooming in
- Map State Persistence: Automatic saving and restoration of map state using localStorage
- Composable Architecture: Reusable Vue composables for state management
- TypeScript: Strict typing for better performance and reliability
- Test Coverage: 82.96% frontend, 100% backend core modules
- Backend Response Time: < 50ms for segment queries (typical)
- Frontend Load Time: < 2s initial page load
- Streaming Latency: Real-time SSE with < 100ms delay
- Database Performance: Spatial queries optimized with PostgreSQL GIS
- Framework: pytest with comprehensive coverage
- Coverage: 100% coverage for core modules including:
backend/src/main.py- API endpoints and streaming functionality (99% coverage)backend/src/services/strava.py- Strava API integration service (100% coverage)backend/src/utils/config.py- Environment configuration management (100% coverage)backend/src/utils/storage.py- S3 and local storage managers (100% coverage)backend/src/models/track.py- Data models and schemas (100% coverage)backend/src/utils/- All utility modules (100% coverage)
- Run:
pixi run test-backend - Features:
- Comprehensive Strava Testing: 22 tests covering all Strava API endpoints
- Error Handling Coverage: HTTPException propagation, authentication failures, API errors
- Mock Integration: stravalib mock fixture integration for realistic API testing
- Async Database Testing: PostgreSQL testing with async operations
- S3 Mocking: moto-based S3 testing for storage operations
- Security Testing: Token management, OAuth flow validation
- Framework: Vitest + Vue Test Utils + Testing Library with jsdom
- Coverage: 82.96% overall coverage with:
Explorer.vue: 58.68% (map interactions, EventSource streaming)SegmentList.vue: Comprehensive segment list functionalitySegmentDetail.vue: Detailed segment view and interactionsEditor.vue: 73.42% (route editing functionality)Navbar.vue: 88.78% (navigation and language switching)gpxParser.ts: 92.8% (comprehensive GPX parsing tests)useMapState.ts: Map state persistence composable
- Run:
pixi run test-frontend - Features: Component testing, Leaflet mocking, EventSource simulation, composable testing
- Comprehensive Coverage: Both backend and frontend have extensive test suites
- Real-world Scenarios: Tests cover actual GPX data processing and map interactions
- Error Handling: Robust testing of edge cases and error conditions
- Performance Testing: Coverage of streaming functionality and client-side parsing
GitHub Actions run tests on push/PR to main using prefix-dev/setup-pixi:
- Backend:
.github/workflows/backend-tests.yml - Frontend:
.github/workflows/frontend-tests.yml
Workflows activate the Pixi environment and run the Pixi tasks, posting coverage summaries and uploading coverage reports to Codecov.
Test coverage is automatically uploaded to Codecov on each push/PR. The coverage badge in the README shows the current coverage percentage for the entire project.
Setup for Repository Maintainers:
- Sign up at Codecov with your GitHub account
- Add the repository to Codecov
- Copy the repository upload token from Codecov settings
- Add the token as a GitHub secret:
- Go to repository Settings > Secrets and variables > Actions
- Create a new secret named
CODECOV_TOKEN - Paste the token from Codecov
The workflows will automatically upload coverage reports:
- Backend: XML coverage report from pytest (
backend/coverage.xml) - Frontend: LCOV coverage report from Vitest (
frontend/coverage/lcov.info)
- Interactive Map: Leaflet-based map with real-time segment streaming
- Server-Sent Events: Efficient streaming of GPX data to frontend
- Geographic Search: Bounds-based filtering with PostgreSQL spatial queries
- Client-side Parsing: Frontend GPX parsing for optimal performance
- Map State Persistence: Automatic saving and restoration of map position and zoom level
- Segment List: Filterable and sortable segment lists with track type filtering
- Segment Detail View: Comprehensive segment information with interactive maps and elevation charts
- Track Type Filtering: Separate views for segments and routes
- Distance Calculations: Real-time distance calculations from map center
- File Upload: Drag-and-drop GPX file upload with validation
- Strava Integration: Import GPX files directly from Strava activities with OAuth authentication
- Wahoo Integration: Upload routes to Wahoo Cloud devices with automatic GPX to FIT conversion
- Route Protection: Editor requires Strava authentication for access
- Visual Editing: Interactive segment selection with map and elevation chart
- Surface Classification: Trail condition metadata (surface type, difficulty, tire recommendations)
- Chart Visualization: Real-time elevation profile with Chart.js
- Commentary Support: Text, video links, and image attachments for segments
- Media Management: Upload and manage images and videos for cycling segments
- Interactive Waypoint Management: Add, move, and remove waypoints on the map
- Multiple Routing Modes: Standard routing, start/end mode, and free mode
- Elevation Profiles: Real-time elevation visualization with gain/loss statistics
- Route Generation: Automatic route generation from selected waypoints
- Segment Integration: Add segments from the explorer to your route
- Filterable Segment List: Filter segments by difficulty, surface type, and tire recommendations
- State Persistence: Automatically saves route state to localStorage
- Undo/Redo Support: History management for route editing
- Route Saving: Save planned routes to the database with Strava authentication
- Global Authentication: Navbar-based login/logout with Strava OAuth 2.0
- Route Protection: Automatic authentication checks for protected routes
- Token Management: Secure backend token storage with automatic refresh
- Smart Redirects: Users return to original page after authentication
- Full Page Reload: Ensures proper navbar state updates
- Mobile Responsive: Adaptive authentication UI for all screen sizes
- Editor Access Control: Restrictive editor access based on authorized Strava user IDs
- Database-driven Control: PostgreSQL-stored authorized user list
- Environment Configuration: Easy management via
.env/auth_usersfile - Frontend UI Control: Editor button shown/hidden based on authorization
- Backend Validation: Server-side authorization checks for security
- Seamless Integration: Works with existing Strava authentication system
- OAuth 2.0 Authentication: Secure login with Wahoo Cloud
- Database-backed Token Storage: OAuth tokens stored securely in PostgreSQL
- Automatic Token Refresh: Seamless token renewal when access tokens expire
- Route Upload: Direct upload of routes to Wahoo Cloud from segment detail view
- GPX to FIT Conversion: Automatic conversion of GPX files to FIT format for Wahoo devices
- Route Synchronization: Routes tracked with external IDs for update support
- Route Management: Delete routes from Wahoo Cloud
- Dual Storage: Support for both local filesystem and AWS S3
- PostgreSQL: Async database with SQLAlchemy ORM
- Metadata Management: Comprehensive track metadata storage
- File Serving: Efficient GPX file serving with proper MIME types
- Media Storage: Support for images and videos with metadata tracking
- Database Seeding: Automated generation of realistic test data
- TypeScript: Strict typing throughout frontend
- Testing: Comprehensive test coverage (82.96% frontend, 100% backend core)
- Code Quality: Automated linting with Ruff (Python) and ESLint (TypeScript)
- Environment Management: Pixi-based development environment
- Component Architecture: Modular Vue components with clear separation of concerns
- Composable Pattern: Reusable state management with Vue composables
- Database Seeding: Automated test data generation for development
- Personal segment collections and favorites
- Social features (sharing, comments, ratings)
- Advanced filtering and sorting options (difficulty, surface type, region)
- Offline map support with progressive web app (PWA)
- Real-time route tracking and live activity sharing
- Native mobile app integration
- Advanced analytics and route statistics dashboard
- Route planning with multi-segment itineraries
- Weather integration for route conditions
- Community-contributed route reviews and photos
Problem: pg_ctl: command not found
# Solution: Ensure Pixi is properly activated
pixi shellProblem: Database connection refused
# Solution: Check if PostgreSQL is running
pixi run pg-status
# If not running, start it
pixi run pg-startProblem: Port 5432 already in use
# Solution: Stop existing PostgreSQL or change port in .env/database
# To stop system PostgreSQL (macOS)
brew services stop postgresql
# Or find process using port 5432
lsof -i :5432
kill <PID>Problem: npm install fails or missing dependencies
# Solution: Clean install
cd frontend
rm -rf node_modules package-lock.json
npm installProblem: Port 5173 already in use
# Solution: Kill process or change port
# Change port in vite.config.js or kill existing process
lsof -i :5173
kill <PID>Problem: TypeScript errors after update
# Solution: Clear cache and rebuild
cd frontend
npm run type-check
rm -rf dist
npm run buildProblem: ModuleNotFoundError when running backend
# Solution: Ensure you're in the pixi environment
pixi shell
# Or run with pixi
pixi run start-backendProblem: .env files not loaded
# Solution: Check .env directory structure
ls -la .env/
# Ensure files exist: database, storage, strava, auth_users
# Check file permissions
chmod 644 .env/*Problem: Strava integration not working
# Solution: Verify Strava credentials
cat .env/strava
# Check callback URL matches Strava app settings
# Ensure STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET are correctProblem: GPX files not uploading (local storage)
# Solution: Check directory permissions
mkdir -p scratch/local_storage
chmod -R 755 scratch/local_storageProblem: S3 storage errors
# Solution: Verify AWS credentials
aws s3 ls s3://your-bucket-name
# Check .env/storage has correct AWS credentialsIf you encounter issues not covered here:
- Check the API documentation for backend errors
- Review browser console for frontend errors
- Check
postgres.logfor database errors - Open an issue on GitHub with error details
Contributions are welcome! Please follow these guidelines:
- Fork the repository and create a feature branch
- Follow the code style:
- Python: Ruff (configured in
pyproject.toml) - TypeScript: ESLint + Prettier (configured in
frontend/eslint.config.js)
- Python: Ruff (configured in
- Write tests for new features:
- Backend: pytest in
backend/tests/ - Frontend: Vitest in
frontend/src/__tests__/
- Backend: pytest in
- Run the test suite before submitting:
pixi run -e dev test-backend pixi run -e dev test-frontend pixi run -e dev lint-all pixi run -e dev format-all pixi run -e dev type-check-frontend
- Submit a pull request with a clear description of changes
- Make your changes in a feature branch
- Ensure all tests pass locally
- Run linters and formatters
- Commit with clear, descriptive messages
- Push and create a pull request
This project is licensed under the MIT License. See the LICENSE file for details.
For issues, questions, or contributions:
- 🐛 Report bugs: Open an issue on GitHub
- 💡 Feature requests: Open an issue with the "enhancement" label
- 📧 Contact: [Your contact information]
- Leaflet: Interactive mapping library
- FastAPI: Modern Python web framework
- Vue.js: Progressive JavaScript framework
- Strava API: Activity data integration
- Pixi: Cross-platform package management