Giving recognition for efforts where effort was given.
A client-side cycling awards app that connects to Strava and surfaces achievements Strava doesn't offer: year bests, season firsts, consistency streaks, comeback tracking, power curve milestones, and more. All data stays in your browser — no backend stores user data.
🚴 Live at aeyu.io · Try the demo
- Connect your Strava account via OAuth
- Activities and segment efforts sync into your browser's IndexedDB
- An awards engine analyzes your history and computes 26+ award types
- Awards appear on your dashboard and per-activity detail views, with shareable cards
The only server component is a small Cloudflare Worker that proxies OAuth token exchange (Strava requires a server-side client_secret). Everything else — data storage, award computation, rendering — runs entirely in the browser as a PWA.
Want to run your own instance? Here's everything you need.
- A Strava API application (free)
- A Cloudflare account (free tier is fine)
- A static hosting provider (GitHub Pages, Netlify, Vercel, or any static host)
- Wrangler CLI for deploying the worker
Go to strava.com/settings/api and create an application:
- Application Name: Whatever you'd like
- Category: Training
- Website: Your hosting URL (e.g.,
https://yourdomain.com) - Authorization Callback Domain: Your domain (e.g.,
yourdomain.com)
This gives you a Client ID (public) and a Client Secret (private — never expose this in client code).
Note: New Strava apps start in "single player mode" — only one connected athlete. This is fine for personal use. To support multiple users, email
developers@strava.comwith your app ID and Strava branding compliance screenshots.
Fork this repository, then edit src/config.js with your values:
export const STRAVA_CLIENT_ID = "YOUR_CLIENT_ID";
export const WORKER_URL = "https://your-worker-name.your-subdomain.workers.dev";
export const OAUTH_REDIRECT_URI = "https://yourdomain.com/callback.html";The other constants (STRAVA_AUTH_URL, STRAVA_API_BASE, OAUTH_SCOPE) don't need changes.
The worker proxies token exchange so your client_secret stays server-side:
cd worker
wrangler login
wrangler secret put STRAVA_CLIENT_ID # paste your Client ID
wrangler secret put STRAVA_CLIENT_SECRET # paste your Client Secret
wrangler deployNote the deployed URL — it should match what you put in config.js.
If you're hosting on a different domain than aeyu.io, update the ALLOWED_ORIGINS array in worker/worker.js to include your domain and any local development origins.
GitHub Pages: Push to main with GitHub Pages enabled on the repo. Add a CNAME file with your domain if using a custom domain.
Any static host: Just deploy the root directory. There's no build step — all files serve as-is. The app uses ESM imports from CDN (Preact, HTM, Signals, Tailwind).
Local development: Serve the root directory with any static server:
python3 -m http.server 5500
# or
npx serve .Then visit http://localhost:5500. The worker CORS config already allows localhost origins on ports 3000 and 5500.
Visit your deployed URL, click "Connect with Strava," and authorize. Your first sync will backfill your activity history — this may take several sessions due to Strava API rate limits (100 requests per 15 minutes, 1000 per day). The sync is resumable and picks up where it left off.
Frontend (100% client-side, no build step)
├── index.html App shell
├── callback.html OAuth redirect handler
├── src/
│ ├── app.js Hash-based router, init
│ ├── auth.js OAuth flow, token management
│ ├── config.js Public configuration
│ ├── db.js IndexedDB wrapper
│ ├── sync.js Strava API sync with rate limiting
│ ├── awards.js Award computation engine (26+ types)
│ ├── award-config.js Award labels, colors, tiers
│ ├── power-curve.js Power curve analysis
│ ├── units.js Metric/imperial formatting
│ ├── demo.js Demo mode with canned data
│ ├── icons.js SVG icons as Preact components
│ └── components/
│ ├── Landing.js Pre-auth landing page
│ ├── Dashboard.js Main screen
│ └── ActivityDetail.js Per-activity view + share cards
├── sw.js Service worker (PWA offline)
└── manifest.json PWA manifest
Backend (Cloudflare Worker — OAuth proxy only)
└── worker/
├── worker.js Token exchange + refresh endpoints
└── wrangler.toml Worker configuration
Testing
└── test/
├── harness.py Playwright-based test harness
├── setup.sh Vendor bundle bootstrap
└── fixture-real.json Real data fixture (not for redistribution)
- Preact + HTM + Signals — reactive UI with no build step, ESM imports from CDN
- IndexedDB — all user data stored locally in the browser
- Tailwind CSS — via CDN
- Cloudflare Workers — OAuth proxy (free tier)
- GitHub Pages — static hosting (or any static host)
No backend stores user data. Privacy by architecture. Your Strava data never leaves your browser. The worker only touches OAuth tokens in transit.
No build step. Edit a JS file, push, done. Preact/HTM/Signals are vendored ESM bundles in vendor/. Tailwind CSS is pre-built via npx @tailwindcss/cli -i vendor/tailwind-input.css -o vendor/tailwind.css --minify (regenerate when adding new utility classes).
Resumable sync. Strava rate limits are real. Sync state persists in IndexedDB so backfill can span multiple sessions without re-fetching.
CV filter. Segments with high time variance (coefficient of variation > 0.5) suppress most awards, preventing noisy segments like traffic-light intersections from generating meaningless achievements.
The engine computes segment-level and ride-level awards including: year bests, season firsts, recent bests (30/60/90-day), beat-median, top quartile and decile performances, consistency streaks, monthly bests, improvement streaks, comebacks, milestones, best month ever, closing-in-on-PR, anniversary rides, YTD bests for both time and power, and reference bests. Comeback mode adds additional tracking for returning from breaks.
See src/award-config.js for the full catalog with labels, colors, and tier rankings.
The test harness uses Playwright to inject mock data and verify award computation:
bash test/setup.sh # one-time: build vendor bundles
python3 test/harness.py # run test scenariosRequires Node.js (for esbuild/Tailwind bundling) and Python 3 with Playwright installed.
The codebase has _MAP.md files in each directory that document all exports with signatures and line numbers — start there rather than reading source files top to bottom.
For development context, see CLAUDE.md which contains architecture notes, patterns, and gotchas.
MIT License — free to use, modify, and distribute with attribution.
Copyright (c) 2025-2026 Oskar Austegard
If you build something with this, a link back to aeyu.io or this repo is appreciated.
Built with Claude by Anthropic. The entire codebase — from spec to implementation to the awards engine to this README — was developed through human-AI collaboration.
Strava and the Strava API are trademarks of Strava, Inc. This project is not affiliated with or endorsed by Strava.
