How far has your finger travelled scrolling today?
Fingermile is a Chrome extension that measures how far your finger travels while scrolling across websites, exposed as personal analytics. Think: "Your finger travelled 1.4km on YouTube today."
- Per-site scroll tracking — see which websites you scroll the most
- Distance + time metrics — scroll distance in meters, time spent per site
- Privacy-first — raw pixel data never leaves your browser; only final centimeter values are sent
- Personal dashboard — web app with date range filters and site breakdown
- Chrome extension popup — quick glance at your stats without leaving the browser
- Multi-user — sign in with Google or email via Clerk
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Chrome Extension│────▶│ Web Dashboard │────▶│ FastAPI Backend │
│ (content script │ │ (Vite + React) │ │ (PostgreSQL) │
│ + popup UI) │ │ Clerk sync host │ │ Railway deploy │
└─────────────────┘ └──────────────────┘ └──────────────────┘
| Component | Stack | Path |
|---|---|---|
| Extension | React, Chrome APIs, Clerk Chrome SDK | extension/ |
| Web App | Vite, React, Tailwind CSS, Clerk React | web/ |
| Backend | FastAPI, SQLModel, asyncpg, PostgreSQL | backend/ |
- The extension listens for
wheelevents on every page deltaY(pixels scrolled) is normalized acrossdeltaModevalues (line, page, pixel)- Pixels are converted to centimeters using your screen DPI and device pixel ratio
- Distance accumulates in the content script, flushes to the background worker every 2s
- Every 5 minutes, the session syncs to the backend via authenticated API call
- The web dashboard fetches aggregated data for display
What we collect: scroll distance (cm), duration (s), site hostname, date What we never collect: page content, keystrokes, URLs, screenshots, browsing history
- Node.js 18+
- Python 3.11+
- PostgreSQL (or a Railway Postgres instance)
- A Clerk account
cd backend
cp .env.dist .env # fill in DATABASE_URL and CLERK_JWKS_URL
pip install -r requirements.txt
uvicorn app.main:app --reloadThe API runs at http://localhost:8000. Tables are auto-created on startup via init_db().
cd web
cp .env.dist .env # fill in VITE_CLERK_PUBLISHABLE_KEY and VITE_BACKEND_URL
npm install
npm run devThe dashboard runs at http://localhost:5173.
cd extension
cp .env.dist .env # fill in all required vars
npm install
npm run buildLoad the extension/dist/ folder as an unpacked extension in chrome://extensions.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string (postgresql+asyncpg://...) |
CLERK_JWKS_URL |
Yes | Clerk JWKS endpoint for JWT verification |
BYPASS_AUTH |
No | Set to true to skip auth (local dev only) |
| Variable | Required | Description |
|---|---|---|
VITE_CLERK_PUBLISHABLE_KEY |
Yes | Clerk publishable key |
VITE_BACKEND_URL |
Yes | Backend API URL |
| Variable | Required | Description |
|---|---|---|
VITE_CLERK_PUBLISHABLE_KEY |
Yes | Clerk publishable key |
VITE_CLERK_SYNC_HOST |
Yes | Web app URL (Clerk session sync host) |
VITE_BACKEND_URL |
Yes | Backend API URL |
VITE_WEB_DASHBOARD_URL |
Yes | Web app URL (for "View Dashboard" link) |
VITE_CLERK_FRONTEND_API |
Yes | Clerk Frontend API URL |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/v1/scroll |
Bearer JWT | Upsert a scroll session (called every 5 min) |
GET |
/v1/metrics?from=&to=&site= |
Bearer JWT | Get aggregated daily scroll data |
DELETE |
/v1/data |
Bearer JWT | Hard-delete all user data |
POST |
/v1/contact |
None | Submit a contact form message |
GET |
/health |
None | Health check |
The backend deploys automatically on Railway via railway.json:
# Set these in Railway → Variables
DATABASE_URL=${{Postgres.DATABASE_URL}}
CLERK_JWKS_URL=https://your-instance.clerk.accounts.dev/.well-known/jwks.jsonThe web app can be deployed to any static host (Vercel, Netlify, Railway).
# Production (external Postgres)
cd backend
docker build -t fingermile-api .
# Local dev (embedded Postgres)
docker compose upfingermile/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app + lifespan
│ │ ├── routes.py # API endpoints
│ │ ├── models.py # SQLModel tables + Pydantic schemas
│ │ ├── db.py # Async engine + session factory
│ │ ├── auth.py # Clerk JWT verification
│ │ └── limiter.py # Rate limiting
│ ├── Dockerfile # Production build
│ ├── Dockerfile.compose # Local dev (embedded Postgres)
│ ├── requirements.txt
│ └── railway.json
├── web/
│ ├── src/
│ │ ├── pages/ # HomePage, SignIn, SignUp, Privacy, Contact
│ │ ├── components/ # Logo, Header, Footer
│ │ └── index.css # Tailwind + design tokens
│ ├── index.html
│ └── package.json
├── extension/
│ ├── src/
│ │ ├── popup/ # React popup UI
│ │ ├── content.ts # Scroll event listener
│ │ └── background.ts # Session management + sync
│ ├── public/ # Icons, manifest, fonts
│ └── package.json
└── docker-compose.yml
MIT