Bikesy is a bike route planner that helps riders find directions based on their hill tolerance and preference for bike lanes, bike routes, or a more direct trip. Three hill settings and three bike-facility preferences provide nine routing combinations.
Routes use OpenStreetMap (OSM) data and are calculated by the Valhalla backend at api.bikesy.com. This repository contains the Next.js frontend, with Mapbox maps and Google Places address search and geocoding. The routing backend currently covers the San Francisco Bay Area.
The browser requests https://api.bikesy.com/route directly, passing the start and
end coordinates and a routing scenario (1–9; the web controls default to 5):
curl 'https://api.bikesy.com/route?lat1=37.77201&lng1=-122.45189&lat2=37.76131&lng2=-122.42157&scenario=5'Clients must inspect the JSON error field even on HTTP 200, which the backend uses
for validation and no-route failures. Direct browser requests require the backend's
CORS permission.
The frontend's /api/route and legacy /api.php endpoints return a 307 temporary
redirect to the backend, preserving query parameters. An omitted or empty scenario
uses the backend default of 1. These compatibility endpoints require a Next.js
server deployment.
Use Node.js 24 and pnpm 12.8.1, the version pinned in package.json.
pnpm install
cp .env.example .envIf you already have a .env file, update it using .env.example as a reference.
Configure these values:
NEXT_PUBLIC_REGION: usesffor the Bay Area. Thetahoeconfiguration is retained, but the current routing backend does not include Tahoe coverage. See regional configuration for other configurations.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN: Mapbox token for displaying maps.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: Google Maps key for address search and geocoding. Enable Maps JavaScript API, Places API (New), and Geocoding API. Allow your deployed site andhttp://localhost:3000/*in the key's HTTP referrer restrictions, and include these APIs in its API restrictions.OPEN_WEATHER_MAP_API_KEYandAIRNOW_API_KEY: server-side keys for weather and air-quality data.
Leave analytics disabled for local development; see Analytics below.
pnpm run devOpen http://localhost:3000 in your browser.
pnpm exec next build
PORT=3000 pnpm run startThe start script requires PORT. Deploy with a Next.js server to support the API
routes, including weather, air quality, and routing compatibility redirects.
pnpm exec node --test src/lib/*.test.mjs
pnpm exec prettier --check README.mdpnpm run prettier formats the entire repository in place.
Navigation is on in every region, for everyone: there is no region setting or URL parameter to turn it on. Tapping Start ride loads the navigation engine (Ferrostar) on demand, so visitors who never navigate don't download it.
Every region's backend must support /route?nav=1, which the frontend sends only for rides
and reroutes. Planning requests don't send it.
For development, add ?sim=1 to ride a simulated rider (&warp=8&noise=6 speeds it up and
adds GPS noise) and ?debug=1 for a heading and GPS readout. Set
NEXT_PUBLIC_BIKESY_API_URL to point at a different backend.
The overlay generator infers display classes
from an OSM snapshot. These are not official agency designations or safety
ratings. The table describes the implemented rules (version 1.2.1), using the
default --path-policy practical. Classification does not change backend routing
costs or automatically update the hosted map style. All matches below must also
pass the access, lifecycle, surface and geometry filters described below.
| Inferred layer | OSM tags/values that contribute |
|---|---|
Class I — paths (class-I.geojson) |
highway=cycleway, or highway=path/footway with bicycle=designated, normally becomes a practical bicycle-path candidate without requiring is_sidepath=no. Explicit independence (is_sidepath=no) also supports I on cycleways or designated path/footway/track/bridleway/pedestrian ways. bicycle=yes alone is access evidence, not Class I evidence. |
Class II — lanes (class-II.geojson) |
On road-centerline ways, cycleway=lane or cycleway:left/right/both=lane. Deprecated opposite_lane also matches, with a warning. cycleway:*:lane=exclusive/advisory is subtype metadata; advisory lanes remain II with lower confidence. |
Class III — shared lanes / bike routes (class-III.geojson) |
cycleway=shared_lane or cycleway:left/right/both=shared_lane. On eligible road types without an existing facility or separate-mapping/contraflow reference, bicycle_road=yes or cyclestreet=yes supplies a bicycle-priority-street fallback. Otherwise, lcn/rcn/ncn/icn=yes or active bicycle-route relation membership supplies a lower-confidence route fallback. |
Class IV — protected tracks (class-IV.geojson) |
On road-centerline ways, cycleway=track or cycleway:left/right/both=track; deprecated opposite_track also matches with a warning. A separately mapped highway=cycleway is IV with cycleway=track, or with both is_sidepath=yes and foot=no, subject to the path precedence below. |
For separately mapped paths, ordinary sidewalk classification takes precedence,
then explicit independence (is_sidepath=no), then protected-track evidence. Thus
a conflicting highway=cycleway + is_sidepath=no + cycleway=track currently matches
I. Practical path promotion excludes crossing, traffic-island, link and
separate-mapping contexts; see the exact rules
for the tag-specific checks. --path-policy strict disables the practical
promotion and requires independence evidence for I.
highway=cycleway takes precedence over footway=sidewalk. Such a way can
still appear as an inferred path, with sidewalk context retained. This does not
promote ordinary highway=footway + footway=sidewalk ways or override access
restrictions. A shared roadside cycleway (is_sidepath=yes + foot=yes) can be I in
practical mode; the class does not establish independence from roads.
cycleway:left/rightoverridescycleway:both, which overridescycleway. Explicitno/nonesuppresses that side;separaterecords an independently mapped facility reference instead of drawing another facility on the road. Unsuffixedcycleway=lanealone does not invent two lanes.- Example:
cycleway:both=lane + cycleway:left=trackyields II on the right and IV on the left. Addingcycleway:right=noremoves the right-side match. A mixed way may therefore occur in multiple class files, once per file by OSM way ID. - Left/right is relative to OSM way direction. Side-specific
:onewayis honored; otherwise the generator uses road oneway and right-hand-traffic conventions.oneway:bicycle=nodoes not invent a contraflow lane. Plaincycleway=oppositeis direction/access evidence only. Deprecatedopposite_*values are warned and classified by their recognized suffix. share_buswayis an other facility, not II or IV;shared_buswayis accepted as a warned alias. Buffer metadata,segregated=yes, orcycleway:*:separationalone does not establish IV. A lane with physical-separation metadata stays II and is flagged for review;segregatedconcerns separation from pedestrians.cycleway:lanes/bicycle:lanesarrays andcycleway:forward/backwardfacility values are retained and warned, not parsed into facilities. Values such asbuffered_lane,sharedandyesare not direct class matches. Width, smoothness, lighting, speed, parking and crossing improvements are useful review evidence, not additional implemented class-selection rules.- Relations with
route=bicycleorroute_master=bicycleretain network, reference, name and roles, including nested memberships. Inactive or conditional memberships cannot supply III fallback; mountain-bike-only relations are not used. Membership does not prove signage or grant access, and a reference such aslcn_refalone does not trigger III.route-network.geojsonis review metadata and can include restricted ways.
Access precedence is bicycle → vehicle → access, with directional tags taking
precedence within each mode. Effective yes/designated/official, unspecified
access and qualified permissive can be retained; no/private/dismount/use_sidepath
and limited or uncertain access are withheld in the affected direction. A feature
can remain if at least one relevant direction is eligible. Bicycle/access/vehicle,
oneway or cycleway :conditional tags are kept unevaluated and withhold the
facility; motorcar-only conditions do not. Inactive lifecycle tags, steps (even
with bicycle ramps), area=yes, motorway/motorway-link and motorroad=yes candidates
are withheld. Missing or degenerate geometry is not drawn.
The default --surfaces all includes paved, unpaved and unknown surfaces.
--surfaces paved keeps only recognized pavement: paved, asphalt, concrete,
concrete:lanes, concrete:plates, paving_stones, sett, cobblestone and
unhewn_cobblestone, plus wood, metal, metal_grid, bricks and chipseal.
Hard decking stays visible without admitting gravel/dirt trails; no bridge or
short-length exception overrides surface or access rules. Compacted/fine gravel
remain unpaved; missing/unrecognized surfaces are unknown. Side surface overrides both-side, then generic cycleway
surface, then the way surface. Buffer, separation and lane-subtype attributes
also use side → both → generic precedence. Review outputs retain filtered records.
For commands, all output files and review metadata, see the
generator instructions.
Weather and AQI responses are cached in browser memory for five minutes after a successful response, separately for each exact latitude/longitude pair and API endpoint. Concurrent requests for the same location share one pending request. Network, HTTP and JSON errors are not cached; a later call can retry. Expired entries are removed on the next lookup. Location changes clear the displayed data, and late responses for a previous location or an unmounted view are ignored. There is no polling timer or shared server cache. Provider URLs and payloads remain unchanged; this browser cache does not replace server/API rate limits.
On the planner's initial mount, the existing URL parser checks for a trip. If one
is present, the automatic route request warms the backend. Otherwise, the page
fires a nonblocking GET /health on the origin of appConfig.BIKESY_API_URL
(normally https://api.bikesy.com/health), with cache: 'no-store' and no credentials.
Failures are ignored. Trip detection uses the existing URL field-presence check;
it adds no separate coordinate or scenario validation.
There is no periodic keepalive, retry, timer, or shared lifecycle registry. The check runs on each planner mount (development effect replay can repeat it). It only runs in the browser and does not delay routing or change route cancellation. Deploy the frontend for this behavior to take effect.
Google Analytics 4 is optional and disabled by default. To enable it in a production
build, first disable Enhanced Measurement in the GA4 web stream, then set
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID (the example uses G-S9DP8E6902) and
NEXT_PUBLIC_GOOGLE_ANALYTICS_ENABLED=true in .env and rebuild.
The app tracks page views and basic routing interactions with sanitized URLs; custom events exclude addresses, coordinates, and search text. Analytics failures do not interrupt routing.