Collect, store, and visualize route travel times between locations using the Google Maps Directions API. Schedule automated data collection and view historical trends for driving, walking, or transit.
https://routewatch-jn559.ondigitalocean.app
The hosted deployment runs with DEMO_MODE=true: unauthenticated visitors get read-only
access to a seeded dataset — three Toronto routes with a week of travel times showing
weekday rush-hour peaks and quieter weekends. Writes still require a login.
It runs on SQLite over ephemeral container storage, so the demo data is regenerated on
every boot rather than persisted. Set DATABASE_URL to a Postgres instance for real
persistence; the app picks Postgres automatically when that variable is present.
- Create collection jobs — Start/destination (address autocomplete), cycle interval (minutes or seconds), duration, navigation type (driving, walking, transit)
- Route preferences — Avoid highways, avoid tolls (driving only)
- Live monitoring — Current cycle data, countdown to next collection, status badges
- Timeline chart — Travel time over the collected period (Recharts)
- Per-snapshot details — Expand any row to see route map + turn-by-turn directions
- Export — CSV or JSON download of collected data
- Job control — Start, pause, resume, stop collection
- Current location — One-tap geolocation for start address
| Layer | Technology |
|---|---|
| Backend | Node.js, Express, SQLite (better-sqlite3) |
| Scheduler | setInterval (in-process, survives restart) |
| Frontend | React, Vite, Recharts, Leaflet |
| Mobile | Flutter (see mobile/README.md) |
| Maps | Google Directions API, Places Autocomplete |
npm install
cd frontend && npm install && cd ..- Create a Google Cloud project
- Enable: Directions API, Maps JavaScript API, Places API
- Create an API key (or two: one for backend, one for frontend with referrer restrictions)
cp .env.example .env
# Edit .env and set:
# GOOGLE_MAPS_API_KEY - backend (Directions API)
# VITE_GOOGLE_MAPS_API_KEY - frontend (Places + Maps), if using a separate key
# AUTH_PASSWORD - optional; set to require a password to use the app (see Authentication below)npm run db:initnpm run dev- Backend: http://localhost:3001
- Frontend (Vite): http://localhost:5173 (or next free port, e.g. 5174); proxies
/apito backend
Why does 3001 show an old UI? The backend serves the built frontend from frontend/dist. That folder is only updated when you run npm run build in the frontend. While developing, use the Vite URL (e.g. 5174) to see live changes. To see your latest UI on 3001, run npm run build from the repo root (or cd frontend && npm run build), then reload http://localhost:3001.
Google Maps APIs (Directions, Geocoding, Places) are billed per request. This app reduces cost by:
- Geocoding & reverse geocode — Cached 24h in memory (same address = one API call per day).
- Route preview (map polyline) — Cached 5 min so opening the same route map doesn’t re-call Directions.
- Collection interval — Server enforces a minimum interval (default 5 minutes). Set
MIN_CYCLE_SECONDS=300in.env(or higher, e.g. 600 for 10 min). Jobs set to 1 min will still run at most every 5 min. - Place autocomplete — Requests only after 3 characters and 400 ms debounce to limit Places API calls.
Set billing alerts and quotas in Google Cloud Console to avoid surprises.
Auth is optional. By default the app has no login; anyone with the URL can use it.
You can enable one or both:
- Set
AUTH_PASSWORDin.env(e.g.AUTH_PASSWORD=my_secret). - Restart the server. The sign-in page will show a password field.
- (Optional) Set
AUTH_SECRETfor session signing; if unset,AUTH_PASSWORDis used.
- In Google Cloud Console, create an OAuth 2.0 Client ID (Application type: Web application).
- Under Authorized redirect URIs, add:
- Production:
https://your-domain.com/api/auth/google/callback - Local:
http://localhost:3001/api/auth/google/callback
- Production:
- In
.envsetGOOGLE_OAUTH_CLIENT_IDandGOOGLE_OAUTH_CLIENT_SECRET(from the OAuth client). - Restart the server. The sign-in page will show Sign in with Google; after consent, users are signed in with their Google account (email shown in the header).
If frontend and backend are on different hosts (e.g. DigitalOcean with separate frontend/backend apps), set BACKEND_URL and FRONTEND_URL on the backend so the OAuth callback and session cookie work; set VITE_API_URL when building the frontend so API calls go to your backend. → Full checklist: DEPLOYMENT-GOOGLE-LOGIN.md
Local testing with Google Sign-In: Use the built app on one origin so the session cookie works: run npm run build then node backend/server.js, open http://localhost:3001, and add http://localhost:3001/api/auth/google/callback as a redirect URI in Google Console.
Sessions last 7 days (HTTP-only cookie). Use Sign out in the header to log out. If neither AUTH_PASSWORD nor Google OAuth is configured, the app has no login.
Per-user routes: When auth is enabled, each user only sees and manages their own routes. Google Sign-In users are scoped by email; password users share one account. Existing jobs (from before user_id was added) are treated as belonging to the anonymous/default account.
npm run build
# Serves from backend; frontend static files go to frontend/distTo make the app available on the internet so others can use it:
npm run build
PORT=3001 node backend/server.jsVisit http://localhost:3001. The backend serves the built frontend and the API from the same origin (/api).
The app runs as one Node process: it serves the API and the static frontend. Use a host that supports Node and persistent disk (for the SQLite database).
| Option | Notes |
|---|---|
| Railway | Add a volume for backend/data, set env vars, deploy from GitHub. |
| Render | Web Service, add persistent disk for backend/data so the DB survives restarts. |
| Fly.io | Use a volume for backend/data and run node backend/server.js. |
| DigitalOcean App Platform | Node app; add a volume or use a managed DB later if you outgrow SQLite. Use repo root as source; Build Command: npm run build; Run Command: node backend/server.js. |
| VPS (e.g. DigitalOcean Droplet, Linode) | Full control: install Node, run with node backend/server.js or use PM2; put Nginx in front for HTTPS if you want. |
Configure these in your host’s dashboard (or .env on a VPS):
GOOGLE_MAPS_API_KEY— Backend (Directions API). Required.VITE_GOOGLE_MAPS_API_KEY— Frontend (Places + Maps). Must be set at build time (see below).PORT— Optional; many hosts set this automatically (e.g.PORT=3001).MIN_CYCLE_SECONDS— Optional; e.g.300to limit API cost.
Important: VITE_* variables are baked into the frontend at build time. So either:
- Build on the host after setting
VITE_GOOGLE_MAPS_API_KEYin the build environment, or - Build locally with the right env, then deploy the built
frontend/distand run only the backend on the server.
Example (build with frontend key):
export VITE_GOOGLE_MAPS_API_KEY=your_key_here
npm run build
# Deploy the whole repo (including frontend/dist) and run: node backend/server.js- DigitalOcean App Platform: App Platform does not support volumes. To keep user data you must add a database (Add components → Create or attach database → PostgreSQL). The app does not yet use PostgreSQL; see docs/DIGITALOCEAN-APP-PLATFORM.md for steps and options.
- Railway / Render / Fly.io: Attach a volume and set
DATA_DIRto the mount path (e.g.DATA_DIR=/data). - VPS: The app writes to
backend/data/by default; just don’t delete that folder.
- In Google Cloud Console, restrict the key:
- Backend key: Application restriction “None” or “IP addresses” (your server IPs); API restriction “Directions API” (and Geocoding if you use reverse geocode).
- Frontend key: HTTP referrer restriction to your public URL(s), e.g.
https://your-app.up.railway.app/*; APIs: “Maps JavaScript API”, “Places API”.
- Set billing alerts and optional quotas so you don’t get unexpected charges.
- With auth: Set
AUTH_PASSWORD(and optionallyAUTH_SECRET) on the host. Only users who sign in with that password can use the app. - Without auth: Leave
AUTH_PASSWORDunset for open access (fine for personal or trusted use). If you expose the URL broadly, enable auth and consider rate limiting to protect your Google API key.
-
npm run buildand runnode backend/server.js; test at the host’s URL. - Set
GOOGLE_MAPS_API_KEYandVITE_GOOGLE_MAPS_API_KEY(and build with the latter if needed). - Attach a persistent volume and set
DATA_DIRto its mount path (e.g.DATA_DIR=/data) so users and routes are not lost on redeploy. - Restrict Google API keys (referrer for frontend, IP/API for backend) and set billing alerts.
- (Optional) Set
AUTH_PASSWORDon the host to require login.
├── backend/
│ ├── db/init.js # SQLite schema + init
│ ├── services/
│ │ ├── googleMaps.js # Directions API client
│ │ └── scheduler.js # Collection cycles
│ └── server.js # Express API + static
├── frontend/
│ └── src/
│ ├── components/ # CreateJob, EditJob, JobDetail, JobsList, etc.
│ └── utils/
└── data/ # SQLite DB (created on first run)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/jobs | List all jobs |
| POST | /api/jobs | Create job |
| GET | /api/jobs/:id | Get job |
| PATCH | /api/jobs/:id | Update job |
| DELETE | /api/jobs/:id | Delete job |
| POST | /api/jobs/:id/start | Start collection |
| POST | /api/jobs/:id/stop | Stop collection |
| POST | /api/jobs/:id/pause | Pause collection |
| POST | /api/jobs/:id/resume | Resume collection |
| GET | /api/jobs/:id/snapshots | Get route snapshots |
| GET | /api/jobs/:id/export | Export CSV/JSON |
| GET | /api/route-preview | Route polyline for map |