Deployment and setup guide
Production-ready setup for self-hosted fitness tracking.
Prerequisites
- Node.js 18+.
- PostgreSQL database (local or cloud).
- Strava API credentials (client ID + secret).
- Domain or ngrok (for webhook development).
Quick start (development)
1. Clone and install
git clone <your-repo>
cd fit
npm install
2. Environment configuration
cp .env.example .env
# Required Configuration:
STRAVA_CLIENT_ID=your_client_id
STRAVA_CLIENT_SECRET=your_client_secret
NEXT_PUBLIC_BASE_URL=https://your-domain.com
DATABASE_URL="postgresql://user:password@localhost:5432/fitdb"
# Webhooks (required for real-time sync):
STRAVA_VERIFY_TOKEN=your_random_verify_token # Random string for webhook verification
# Optional Configuration:
STRAVA_WORKER_INTERVAL_MS=14400000 # 4 hours (polling fallback)
STRAVA_AUTO_SYNC_ENABLED=true # Enable automatic polling (backup to webhooks)
STRAVA_AUTO_SYNC_STOP_AFTER=20 # Stop after N consecutive existing activities
THUMB_SIZE=256 # Thumbnail size in pixels
THUMB_DEBUG=false # Enable debug logging
THUMB_NO_BASEMAP=false # Disable map backgrounds
3. Database setup
npx prisma migrate dev --name init
npx prisma generate
4. Start development
# Start all workers (web + thumbnails + strava + webhook processing)
npm run dev
5. Configure strava OAuth
- Set Authorization Callback Domain in Strava API settings to your domain
- Visit
/api/strava/authto authenticate
6. Set up real-time webhooks
# Create webhook subscription for instant activity sync
npm run webhook:create
# Verify webhook is active
npm run webhook:list
Alternative: Use the built-in Subscription menu in the app for GUI-based webhook management.
Webhook development with ngrok
ngrok configuration requirements
ngrok's Pay-as-you-go plan requires domain registration:
- Visit ngrok Dashboard: https://dashboard.ngrok.com/domains
- Register your domain: Add your custom domain (e.g.,
fit.muppit.coach). - Configure IP policies (if using access control):
- Create IP policy with Strava webhook IPs (see list below).
- Add your home/office IP for development access.
- Attach policy to your domain endpoint.
- Update ngrok command: Use
--domainflag:ngrok http 3000 --domain=fit.muppit.coach --host-header=fit.muppit.coach
Strava webhook IP addresses
If you use IP-based access control (ngrok policies, server firewalls, etc.), you must whitelist these Strava webhook IPs:
52.1.196.92 # AWS US-East region
52.4.243.43 # AWS US-East region
52.70.212.225 # AWS US-East region
54.209.86.30 # AWS US-East region
3.209.55.129 # AWS US-East region
44.194.7.173 # AWS US-East region
54.157.3.203 # AWS US-East region
54.160.181.190 # AWS US-East region
18.206.20.56 # AWS US-East region
3.208.213.46 # AWS US-East region
34.194.140.119 # AWS US-East region
34.203.235.59 # AWS US-East region
Source: Strava community Hub - webhook IP addresses
Webhook troubleshooting
# Test webhook endpoint manually
curl "https://your-domain.com/api/strava/webhook"
# Should return: {"status":"webhook endpoint active"}
# Create test activity fetch job
curl -X POST "http://localhost:3000/api/admin/activity-jobs" \
-H "Content-Type: application/json" \
-d '{"activityId": "12345", "athleteId": "67890", "reason": "manual"}'
# Clean up completed jobs
curl -X DELETE "http://localhost:3000/api/admin/activity-jobs?status=done&olderThan=24"
Background workers
Architecture overview
The application runs 4 concurrent processes:
- WEB - Next.js server (port 3000).
- THUMB - Thumbnail generation worker.
- STRAVA - Token refresh and periodic sync worker.
- ACTIVITY - Webhook-triggered activity fetch worker.
Worker configuration
Strava worker
- Purpose: Automatic activity sync every 4 hours.
- Environment:
STRAVA_WORKER_INTERVAL_MS,STRAVA_AUTO_SYNC_STOP_AFTER,STRAVA_AUTO_SYNC_ENABLED. - Features: Database-first token management, configurable sync thresholds, intelligent early termination.
Activity fetch worker
- Purpose: Process webhook-triggered activity fetches.
- Features: Job queue processing (every 2 seconds), rate limiting & backoff, concurrent job management (max 3), retry logic with exponential backoff.
Thumbnail worker
- Purpose: Generate activity thumbnails in background.
- Environment:
THUMB_WORKER_INTERVAL_MS,THUMB_SIZE,THUMB_STALE_MS - Features: OpenStreetMap integration, automatic polyline processing, configurable thumbnail size, stale job cleanup.
Individual worker commands
npm run thumb:worker # Run thumbnail worker only
npm run strava:worker # Run strava sync worker only
npm run activity:worker # Run activity fetch worker only
Security and authentication
Database-first authentication architecture
- Database persistence ensures authentication works across all devices and browsers.
- No file dependencies eliminates cross-device synchronisation issues.
- Centralized token management with proper audit trails.
- Enterprise-grade audit trail with
isActivefield and soft deletion. - Multi-user ready with unique athlete IDs for future expansion.
Security features
- Database-first authentication - tokens stored securely in PostgreSQL with proper access controls.
- Automatic token rotation - database-managed refresh with audit trail and timestamp tracking.
- Multi-user isolation - unique athlete IDs prevent cross-user data access.
- HTTP-only cookies for session management with Safari compatibility (fallback only).
- Environment variable protection for sensitive API credentials and database connections.
- CORS and CSRF protection via Next.js defaults.
- Large field exclusion prevents accidental data exposure in API responses.
- Backup integration - authentication data included in database backup strategies.
Backup and recovery
Creating a project backup
The fitness app includes a backup script that creates timestamped backups of the entire project, including configurations and environment files.
- Make the backup script executable:
chmod +x backup.sh
- Run the backup:
# Basic backup
./backup.sh
# Backup with a comment
./backup.sh -m "Added comprehensive testing framework and GitLab CI/CD"
# or
./backup.sh --message "Fixed Strava webhook processing issues"
This will create a zip file in ~/Projects/backups/fit/ with format: fit_BRANCH_YYYYMMDD_HHMMSS.zip
For example:
~/Projects/backups/
└── fit/ # Fitness app backups
├── fit_main_20250922_143000.zip
├── fit_feature_20250922_154500.zip
└── fit_bugfix_20250922_162300.zip
Each backup includes:
- All source code and components.
- Configuration files (.prettierrc, eslint.config.mjs, jest.config.mjs).
- .env files with Strava credentials (as .env.backup).
- Database schema and migrations (prisma/).
- Documentation (memory_bank/, README.md).
- GitLab CI/CD configuration (.gitlab-ci.yml).
- Git hooks and quality tooling (.husky/).
- VS Code configuration (.vscode/).
- BACKUP_MANIFEST.txt with backup details.
- comment.txt with your backup message (if provided).
Excludes:
- node_modules (can be reinstalled with npm install).
- .git directory (use Git for version control).
- .next build directory (regenerated on build).
- coverage/ directory (regenerated on test).
- Log files and temporary files.
Recovering from a backup
To restore files from a fitness app backup:
- List backup contents without extracting:
unzip -l ~/Projects/backups/fit/fit_main_20250922_143000.zip
- View backup details:
# Extract and view the manifest
unzip -p backup_file.zip "*/BACKUP_MANIFEST.txt"
# View the backup comment (if exists)
unzip -p backup_file.zip "*/comment.txt"
- Restore specific files:
# Extract specific file(s)
unzip backup_file.zip "*/path/to/your/file"
# Or extract to temporary location
unzip backup_file.zip -d ~/temp_restore
Common fitness app restore scenarios:
- Restore Strava credentials:
unzip backup.zip "*/.env.backup" - Restore specific component:
unzip backup.zip "*/src/components/MapOverlay/*" - Restore database schema:
unzip backup.zip "*/prisma/*" - Restore GitLab CI config:
unzip backup.zip "*/.gitlab-ci.yml" - Restore testing configuration:
unzip backup.zip "*/jest.config.mjs" "*/jest.setup.js" - Restore memory bank docs:
unzip backup.zip "*/memory_bank/*" - Restore quality tooling:
unzip backup.zip "*/.husky/*" "*/.prettierrc" "*/eslint.config.mjs"
Note: Always verify the contents of restored files before replacing existing ones. Pay special attention to .env files containing Strava API credentials.
Database backup strategy
For the fitness app's PostgreSQL database:
# Create database backup
pg_dump $DATABASE_URL > ~/Projects/backups/fit/fitness_db_$(date +%Y%m%d_%H%M%S).sql
# Restore database backup
psql $DATABASE_URL < ~/Projects/backups/fit/fitness_db_TIMESTAMP.sql
# Backup with compression
pg_dump $DATABASE_URL | gzip > ~/Projects/backups/fit/fitness_db_$(date +%Y%m%d_%H%M%S).sql.gz
# Restore compressed backup
gunzip -c ~/Projects/backups/fit/fitness_db_TIMESTAMP.sql.gz | psql $DATABASE_URL
Critical data protection
Essential backup items for fitness app:
-
Strava API Credentials (.env file)
- Client ID and secret for OAuth integration.
- Webhook verification tokens.
- Database connection strings.
-
Database Content
- Activity data and splits.
- User authentication tokens.
- Webhook subscription cache.
- Best efforts calculations.
-
Configuration Files
- Testing framework setup (Jest, coverage).
- GitLab CI/CD pipeline configuration.
- Code quality tooling (Prettier, ESLint, Husky).
- TailwindCSS and PostCSS configuration.
-
Documentation
- Memory bank with all decisions and context.
- API documentation and deployment guides.
- Progress tracking and decision logs.
Production deployment
Production considerations
- Set
NODE_ENV=production(enables secure cookies automatically). - Configure proper PostgreSQL connection pooling.
- Set up reverse proxy (nginx) for static assets.
- Enable gzip compression for API responses.
- Monitor background worker health.
- Ensure database persistence and backup strategies for authentication tokens.
Docker deployment
# Include all three processes in production
CMD ["npm", "run", "start"]
# Ensure database connection and proper environment variables
ENV DATABASE_URL="postgresql://user:password@db:5432/fitdb"
ENV STRAVA_CLIENT_ID="your_client_id"
ENV STRAVA_CLIENT_SECRET="your_client_secret"
ENV NODE_ENV=production
Environment variables (production)
# Required
NODE_ENV=production
DATABASE_URL="postgresql://user:password@db:5432/fitdb"
STRAVA_CLIENT_ID="your_client_id"
STRAVA_CLIENT_SECRET="your_client_secret"
NEXT_PUBLIC_BASE_URL="https://your-production-domain.com"
# Webhooks
STRAVA_VERIFY_TOKEN="your_secure_random_token"
# Performance Tuning
STRAVA_WORKER_INTERVAL_MS=14400000 # 4 hours
STRAVA_AUTO_SYNC_STOP_AFTER=20
THUMB_SIZE=256
Performance monitoring
Expected performance benchmarks
- Initial Page Load: ~411ms DOM ready, ~998ms full load (optimized).
- Activity Details API: ~20ms response time, less than 1KB response size.
- Activity Details Page: ~40ms response time, ~14KB response size.
- Activities List API: ~15ms response time, ~2KB for 20 activities.
- Thumbnail Requests: ~113-134kB per 96x96 PNG thumbnail (lazy loaded).
- Best Efforts API: ~30ms response time, less than 5KB for multiple distances.
- Total Page Transfer: ~3MB for 50 activities (down from 10MB+ pre-optimization).
Performance testing
# Test API response times and sizes
time curl -s "http://localhost:3000/api/activities/[id]" | wc -c
time curl -s "http://localhost:3000/activities/[id]" | wc -c
time curl -s "http://localhost:3000/api/activities?sport=Run&limit=20" | wc -c
# Check for large fields in responses
curl -s "http://localhost:3000/api/activities/[id]" | jq 'keys'
curl -s "http://localhost:3000/api/activities?limit=1" | jq '.[0] | keys'
# Verify large fields are excluded
curl -s "http://localhost:3000/api/activities/[id]" | jq 'has("raw") or has("thumbPng")'
# Should return: false
Warning signs of performance issues
- API responses >10KB for single activities.
- Response times >100ms for activity lists.
- Browser network tab showing MB transfers.
- High memory usage in browser dev tools.
Admin and monitoring
Admin API examples
# Check system health
curl -s "http://localhost:3000/api/admin/webhooks" | jq '.subscriptions[]'
curl -s "http://localhost:3000/api/admin/activity-jobs" | jq '.stats'
curl -s "http://localhost:3000/api/admin/thumbnails" | jq '.pending'
# Queue management
curl -X POST "http://localhost:3000/api/admin/activity-jobs" \
-H "Content-Type: application/json" \
-d '{"activityId": "12345", "athleteId": "67890", "reason": "manual"}'
curl -X DELETE "http://localhost:3000/api/admin/activity-jobs?status=done&olderThan=24"
# Force thumbnail regeneration for all activities
curl -X POST "http://localhost:3000/api/admin/thumbnails" \
-H "Content-Type: application/json" \
-d '{"force": true}'
# Performance analytics
curl -X POST "http://localhost:3000/api/admin/calculate-best-efforts"
curl -X POST "http://localhost:3000/api/admin/clear-best-efforts"
curl -X POST "http://localhost:3000/api/admin/sync-streams"
Rate limiting status
The activity fetch worker includes intelligent rate limiting:
- 600 requests per 15 minutes (Strava's limit).
- Token bucket algorithm with automatic refill.
- Per-athlete cooldown (15 seconds between fetches).
- Exponential backoff on errors (30s → 1m → 2m → 4m → 8m).
Configuration recommendations
By usage pattern
- Daily runner:
STRAVA_AUTO_SYNC_STOP_AFTER=5(very fast). - Regular runner:
STRAVA_AUTO_SYNC_STOP_AFTER=20(balanced). - Weekend warrior:
STRAVA_AUTO_SYNC_STOP_AFTER=50(comprehensive).
Sync filter examples
# Date range sync (2024 activities only)
after=1704067200&before=1735689599
# Quick sync with early termination after 5 existing
stopAfter=5
# Large historical sync
per_page=200&stopAfter=999&force=true
Expected logs
[strava-worker] started. interval: 240min checking every 4h | auto-sync enabled (stopAfter=20)
[strava-worker] connection verified for athlete: John Doe
[strava-worker] starting auto-sync of new activities...
[strava-worker] activity 15841223672: created successfully
[strava-worker] stopping auto-sync - found 20 consecutive existing activities
[strava-worker] auto-sync completed: 1 new activities imported!
Sync process deep dive
What happens when you press "Sync Activities"
When you click the "Sync Activities" button on the main page, it opens an advanced sync interface. Here's what happens when you perform a sync:
Initial setup
- Filter Selection: Choose sync options from the advanced filter modal.
- Button State Changes: Button text changes to "Syncing…" during process.
- API Call: Makes a POST request to
/api/strava/syncwith selected filters. - Authentication Check: Verifies and refreshes Strava access token if needed.
Data fetching from strava
- Paginated Fetch: Requests activities from Strava API in batches of 50
- Starts with page 1:
https://www.strava.com/api/v3/athlete/activities?page=1&per_page=50. - Continues to next pages until no more activities found.
- For each activity summary, fetches detailed data:
https://www.strava.com/api/v3/activities/{id}.
- Starts with page 1:
Data processing for each activity
-
Pace Calculations:
- Average Pace: Calculated from
average_speed(1000 / speed in m/s). - Fastest Pace: Analyzed from 1km+ splits to find the fastest kilometer.
- Average Pace: Calculated from
-
Comprehensive Data Extraction:
- Basic metrics: Name, date, distance, time, elevation.
- Performance data: HR (avg/max), cadence (avg/max), power (avg/max).
- Environmental: Temperature, gear used.
- Social: Achievement count, photo count, description.
- Elevation details: High/low elevation points.
Database operations
-
Activity Upsert: For each activity:
- Check if exists: Looks up activity by Strava ID.
- Insert or Update: Creates new record or updates existing one.
- Full data storage: Stores all processed metrics + raw Strava JSON.
-
Splits Processing:
- Per-kilometer splits: Extracted from
splits_metric. - Pace calculation: Converts speed to pace for each split.
- Elevation & HR: Stores elevation gain and average HR per split.
- Database upsert: Updates existing splits or creates new ones
- Per-kilometer splits: Extracted from
Thumbnail generation
- Thumbnail Queue:
- Map check: Only for activities with GPS routes.
- Job creation: Adds to
ThumbnailJobtable for background processing. - Conditional: Only if forced sync or no existing thumbnail.
Token management
- Cookie Updates: If tokens were refreshed during sync:
- Updates browser cookies with new access token.
- Updates refresh token and expiration time.
- Ensures seamless future requests.
Response & UI update
-
Sync Summary: Returns statistics:
{
"total": 150, // Total activities processed
"created": 5, // New activities added
"updated": 145 // Existing activities updated
} -
Page Refresh:
- Reload data: Calls
load()function to refresh activity list. - Modal close: Advanced sync filter modal closes automatically.
- UI update: New activities appear in the list with updated counts.
- Reload data: Calls
What you will see
- New activities appear at the top of your list.
- Updated data for existing activities (if Strava data changed).
- Thumbnails will generate in the background within minutes.
- Performance metrics calculated and stored for analytics.
Performance notes
- Intelligent skipping: Only processes new activities, skips existing ones automatically.
- Early termination: Stops when 20+ consecutive existing activities found (huge time savings).
- Force mode available: Use
?force=trueto update all activities if needed. - Background processing: Thumbnail generation doesn't block the sync.
- Error handling: Continues syncing even if individual activities fail
- Token refresh: Automatically handles expired tokens.
Sync filter parameters
The advanced sync interface supports these filter options:
- Date range:
after=1704067200&before=1735689599- Unix timestamps for date filtering. - Force sync:
force=true- Updates all activities regardless of existing status. - Stop threshold:
stopAfter=50- Stop after N consecutive existing activities. - Activity limit:
per_page=200- Max activities per page (up to 200). - Combined filters: Multiple parameters can be combined for precise control.
This comprehensive sync process ensures the local database stays perfectly synchronized with the Strava data while calculating additional performance metrics not available in the standard Strava interface.
This deployment guide covers everything needed to run the fitness app in development and production environments with optimal performance and security.