Live: https://calcpace.app
Track your runs and rides. Understand your fitness.
Log activities, upload GPX routes, analyse splits — and use free public tools to calculate pace, predict race times and estimate VO2max. No ads, no tracking, totally free. Your data is yours — export it anytime.
Built to practice modern Rails infrastructure and deployment. This app serves as a production dogfooding environment for the open-source calcpace gem.
- Master VPS deployment with Kamal + Docker on DigitalOcean (vs. Heroku/PaaS)
- Practice TDD with Minitest and CI/CD with GitHub Actions
- Follow 37signals/Basecamp design principles — thin controllers, rich models, Current attributes
| Layer | Technology |
|---|---|
| Backend | Ruby on Rails 8 |
| Frontend | ERB + Tailwind CSS |
| Database | PostgreSQL 17 |
| Cache / Jobs | Redis + Sidekiq |
| Resend API | |
| Monitoring | AppSignal (APM + error tracking) |
| Deploy | Kamal + Docker |
| Hosting | DigitalOcean (VPS) |
| File Storage | Cloudflare R2 (Active Storage — S3-compatible) |
| Maps | Leaflet.js + OpenStreetMap (no API key required) |
| Bot Protection | Cloudflare Turnstile + Rack::Attack |
- Ruby 3.4.4 (via asdf)
- Docker + Docker Compose
- PostgreSQL client (
libpq)
# 1. Clone and install dependencies
git clone https://github.com/0jonjo/calcpace_web.git
cd calcpace_web
bundle install
# 2. Start PostgreSQL and Redis via Docker
docker compose up -d
# 3. Create and migrate the database
bin/rails db:create db:migrate
# 4. Start the development server (Rails + Tailwind watcher)
bin/devOpen http://localhost:3000.
Note: A system Redis on port 6379 will conflict. The compose file maps Redis to port 6380 to avoid it.
bin/rails testThe calculator and converter are publicly accessible at calcpace.app — no login required:
Compute running and cycling metrics:
- Pace — given distance and total time
- Total Time — given distance and pace
- Distance — given total time and pace
- Race Times — finish time for standard distances at a given pace
- Race Predictor — estimate finish time for standard race distances (5K, 10K, half marathon, marathon) from a known result, using Riegel or Cameron formula
- VO2max — estimate aerobic capacity from a race result (Daniels & Gilbert formula)
Supports both metric (km) and imperial (mi) unit systems.
Convert between common sports units:
- Distance — km ↔ mi
- Speed — km/h ↔ mph
- Pace — min/km ↔ min/mi
All calculations are powered by the calcpace gem.
A guest account with sample activities (a run and a bike ride) is available to explore the app:
- Email:
guest@calcpace.app - Password: set via
GUEST_PASSWORDenv var in production
The guest profile is automatically reset every Monday at 3am by the GuestResetJob.
Registration is open. Visit /registration/new or click "Sign up" in the nav.
After registering, a verification email is sent via Resend. The account is locked until the email is confirmed (link expires in 48 hours). Unverified users are redirected to the verification page on login.
Registration is protected by Cloudflare Turnstile (bot challenge) and Rack::Attack (rate limiting).
Handled by Resend (free tier: 3,000 emails/month). All emails are queued via Sidekiq.
| Event | |
|---|---|
| Account created | Email verification link (expires in 48h) + welcome email |
| Password reset requested | Reset link (expires in 15 min) |
| Password changed | Security notification |
Activities support an optional .gpx file upload (stored in Cloudflare R2). When a GPX file is attached:
GpxParseJobruns asynchronously via Sidekiq- The job parses trackpoints from the file using the
gpxgem - Trackpoints are bulk-inserted into the
trackpointstable - Distance and elevation gain are calculated via the
calcpacegem (TrackCalculatormodule — Haversine formula) and stored on the activity
The activity show page (both logged-in and public) renders an interactive OpenStreetMap map via Leaflet.js and a per-km splits table, calculated on-the-fly from the stored trackpoints.
Users can opt in to a public profile at /:username. Each activity can individually be set to public or private. Public activities are listed on the profile page and have a dedicated public detail page at /:username/activities/:id.
Profile avatars and activity photos are stored in Cloudflare R2 via Active Storage. Supported formats: JPEG, PNG, WebP (max 5 MB). GPX files: XML format (max 20 MB).
Sidekiq processes the job queue using the Redis accessory. A separate worker container runs alongside the web container in production (see config/deploy.yml).
| Job | Trigger | Description |
|---|---|---|
GuestResetJob |
Every Monday at 3am (sidekiq-cron) | Wipes guest user activities and recreates the profile |
GpxParseJob |
On GPX file upload | Parses trackpoints, calculates distance/elevation, bulk-inserts into DB |
GitHub Actions runs the full test suite on every push to main. Deploys are triggered by GitHub Releases. See .github/workflows/.
Deployment is handled by Kamal targeting a DigitalOcean VPS. See config/deploy.yml.