Parking the Vélib is the hard part

A phone compass for the last few minutes of the trip — Paris

Jonathan Jayes

The last few minutes are the problem

When I unlock a Vélib, I already know where I’m going — I don’t route from my front door. The riding is easy.

The part no app answers is the last few minutes: near my destination, which dock has a free spot right now — and once I’ve locked the bike, which way do I walk?

…me, circling the block for an open dock.

Every app plans the whole trip up front

Google Maps: walk → ride → walk, routed from your front door.

The Vélib app: 1,400 stations — but which one near you is free now?

Both assume you’re on foot from the start and plan the route before you move. My app skips the plan: by default the compass points to the nearest free dock, and once you park, it turns to point at the door.

Two feeds — that’s the whole backend

Framed as coordinates, it shrinks: I need to know where the open docks are, and where the person is standing.

🚲 Vélib live feed · opendata.paris.fr

{
  "name": "Vieille du Temple – Francs Bourgeois",
  "numdocksavailable": 14,
  "capacity": 35,
  "coordonnees_geo": { "lat": 48.858, "lon": 2.358 }
}

1,500+ stations, refreshed live.

📍 Google Geocoding API

{
  "formatted_address": "21 Rue des Gravilliers, 75003 Paris",
  "geometry": { "location": { "lat": 48.8637, "lng": 2.3569 } },
  "status": "OK"
}

Turns a spoken or typed address into lat / lng.

Everything else — nearest dock, distance, bearing — is arithmetic on two lists of coordinates.

The phone does the arithmetic; the server only geocodes

flowchart LR
  A["📱 iPhone (React SPA)"] -->|"address string"| B["FastAPI /api/location/geocode"]
  B -->|"lat, lng"| A
  A -->|"haversine + bearing<br/>vs. live docks"| A
  A -->|"webkitCompassHeading"| N["🧭 needle = bearing − heading"]
  subgraph Client [runs entirely on device]
    A
    N
  end
  style Client fill:#DEEBF7,stroke:#2b8cbe

The only thing that ever leaves the phone is the address. Choosing the nearest dock and turning the compass both happen on the device — which keeps it fast and, almost by accident, private.

Finding the nearest free dock, live

Destination 21 rue des Gravilliers. Green is the nearest open dock, blue the rest — fetched live from opendata.paris.fr. This is the same haversine that runs on the phone.

Getting a true-north compass out of an iPhone

// Must be called *inside* a user gesture (a button tap)
await DeviceOrientationEvent.requestPermission();

function onOrient(e) {
  // iOS: this is TRUE north, clockwise.
  // NOT e.alpha (relative + inverted).
  const heading = e.webkitCompassHeading
                ?? (360 - e.alpha);   // Android
}

This quietly cost me an afternoon. Three ways to get nothing: without HTTPS the sensors are blocked; ask for permission on page load and iOS ignores it, because it needs a real tap; and e.alpha gives an arbitrary origin, not north.

The fix: an “Enable Compass” button that calls requestPermission() on the tap, then reads webkitCompassHeading.

One container on Cloud Run, and HTTPS comes free

flowchart LR
  S["git push"] --> CB["Cloud Build"]
  CB --> IMG["Artifact Registry<br/>single image"]
  IMG --> CR["Cloud Run<br/>europe-west9 · HTTPS"]
  CR --> P["📱 *.run.app"]
  SM["Secret Manager<br/>Mistral · Geocoding"] -.-> CR
  style CR fill:#DEEBF7,stroke:#2b8cbe

One image: FastAPI serves both the built React app and the API from a single origin, so there’s no CORS to fight, and every *.run.app URL is HTTPS by default — exactly what the iPhone compass insists on. Keys stay in Secret Manager.

Three ways in, then you just follow the needle

Reach a destination three ways: speak it — Mistral Vox transcribes and a Mistral model pulls the address out — type it into the sidebar, or hit demo.

From there the compass points at the nearest open dock. Tap “Parked” and the needle flips toward your actual destination. Your position updates live through watchPosition.

Try it now

It’s live today — one container on Cloud Run, HTTPS, real geocoding, a working iOS compass, and voice, typed, and demo input all in place.

Now on the live Vélib feed. Next: rank docks by walking time instead of straight-line distance, and make it an installable app with offline map tiles.

Merci — questions?

velib-app…run.app
Point your camera here to try it on your phone.