Skip to main content

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/auth to 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

warning

ngrok's Pay-as-you-go plan requires domain registration:

  1. Visit ngrok Dashboard: https://dashboard.ngrok.com/domains
  2. Register your domain: Add your custom domain (e.g., fit.muppit.coach).
  3. 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.
  4. Update ngrok command: Use --domain flag:
    ngrok http 3000 --domain=fit.muppit.coach --host-header=fit.muppit.coach

Strava webhook IP addresses

warning

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 isActive field 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.

  1. Make the backup script executable:
chmod +x backup.sh
  1. 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:

  1. List backup contents without extracting:
unzip -l ~/Projects/backups/fit/fit_main_20250922_143000.zip
  1. 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"
  1. 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 &lt; ~/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:

  1. Strava API Credentials (.env file)

    • Client ID and secret for OAuth integration.
    • Webhook verification tokens.
    • Database connection strings.
  2. Database Content

    • Activity data and splits.
    • User authentication tokens.
    • Webhook subscription cache.
    • Best efforts calculations.
  3. Configuration Files

    • Testing framework setup (Jest, coverage).
    • GitLab CI/CD pipeline configuration.
    • Code quality tooling (Prettier, ESLint, Husky).
    • TailwindCSS and PostCSS configuration.
  4. 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

  1. Filter Selection: Choose sync options from the advanced filter modal.
  2. Button State Changes: Button text changes to "Syncing…" during process.
  3. API Call: Makes a POST request to /api/strava/sync with selected filters.
  4. Authentication Check: Verifies and refreshes Strava access token if needed.

Data fetching from strava

  1. 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}.

Data processing for each activity

  1. Pace Calculations:

    • Average Pace: Calculated from average_speed (1000 / speed in m/s).
    • Fastest Pace: Analyzed from 1km+ splits to find the fastest kilometer.
  2. 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

  1. 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.
  2. 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

Thumbnail generation

  1. Thumbnail Queue:
  • Map check: Only for activities with GPS routes.
  • Job creation: Adds to ThumbnailJob table for background processing.
  • Conditional: Only if forced sync or no existing thumbnail.

Token management

  1. 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

  1. Sync Summary: Returns statistics:

    {
    "total": 150, // Total activities processed
    "created": 5, // New activities added
    "updated": 145 // Existing activities updated
    }
  2. 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.

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=true to 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.