backend / groundfix

Groundfix

A small Next.js backend-for-frontend that validates coordinate queries and fans them out to OpenStreetMap, Overpass, Wikipedia and Cloudflare, then scores nearby public places by likely connectivity.

2026 · active · local repository

web → routes: HTTPS GET /api/*web → cf: speed test __down/__uproutes → gate: search / reversegate → nominatim: HTTPS jsonv2routes → scorer: nearbyscorer → overpass: POST Overpass QLroutes → wiki: parallel summariesroutes → cf: server traceInstrument view(browser)Instrument view (browser) (client) GPS fix, map, speed testNext.js route handlersNext.js route handlers (service) 5 GET endpoints, Zod-validatedNominatim gateNominatim gate (service) 1.1 s spacing, UA headerClassify + scoreClassify + score (service) 11 kinds, distance falloffNominatimNominatim (third party) search / reverseWikipedia APIWikipedia API (third party) geosearch + summariesCloudflare edgeCloudflare edge (third party) cdn-cgi/trace, speed.cloudflare.comOverpass mirrors x2Overpass mirrors x2 (third party) failover in orderHTTPS GET /api/*speed test __down/__upsearch / reverseHTTPS jsonv2nearbyPOST Overpass QLparallel summariesserver trace web → routes: HTTPS GET /api/*web → cf: speed test __down/__uproutes → gate: search / reversegate → nominatim: HTTPS jsonv2routes → scorer: nearbyscorer → overpass: POST Overpass QLroutes → wiki: parallel summariesroutes → cf: server traceInstrument view(browser)Instrument view (browser) (client) GPS fix, map, speed testNext.js routehandlersNext.js route handlers (service) 5 GET endpoints, Zod-validatedNominatim gateNominatim gate (service) 1.1 s spacing, UA headerClassify + scoreClassify + score (service) 11 kinds, distance falloffNominatimNominatim (third party) search / reverseWikipedia APIWikipedia API (third party) geosearch + summariesCloudflare edgeCloudflare edge (third party) cdn-cgi/trace, speed.cloudflare.comOverpass mirrors x2Overpass mirrors x2 (third party) failover in orderHTTPS GET /api/*speed test __down/__upsearch / reverseHTTPS jsonv2nearbyPOST Overpass QLparallel summariesserver trace
clientservicethird partyasync
  • [*]LanguagesTypeScript
  • [*]FrameworksNext.js App Router route handlers · Zod
  • [*]InfraBun · Next.js (local)
  • [*]Third partiesNominatim (OSM) · Overpass API (2 mirrors) · Wikipedia geosearch + REST summary · Cloudflare 1.1.1.1 cdn-cgi/trace · Cloudflare speed test (client-side)

Groundfix is a night-radio-style map of your own position, your current connection and the public places around you. The backend is five GET route handlers under src/app/api (ip, nearby, place, reverse, search), each a thin shell over a service in src/server/services. There is no database: every request is validated, sent to a public upstream, normalised and returned.

The work is in how it treats those public upstreams. Nominatim calls go through a process-wide gate that spaces requests at least 1,100 ms apart, as Nominatim's usage policy requires, and every outbound request carries an identifying User-Agent. Overpass queries are POSTed to two mirrors in order, falling back to the second if the first fails. Wikipedia summaries are fetched in parallel for the top four geosearch hits, and if a summary fails the hit is still returned without its extract.

Nearby places come from one Overpass QL union over 13 tag selectors (internet_access, cafes, libraries, coworking, universities, comms masts). Elements are de-duplicated by type/id, put into one of 11 kinds, and scored as kind base plus an internet_access bonus or penalty, multiplied by a linear distance falloff with a 0.15 floor. The sort is deterministic: score, then name, then id. The top 40 are returned with a disclaimer that this is tag data, not measured throughput.

What the app refuses to do is enforced in code. A bun test asserts that no person, imei, track or camera route directories exist, so adding one fails the suite. Upstream failures come back as 502 with the upstream status in the message, and Zod validation failures come back as 400 with per-field issues.

How it holds up

Auth
None. Public read-only GET endpoints with no user accounts and nothing stored; the only identity is the outbound User-Agent sent to upstreams.
Data
Stateless: no database or tables. Typed response shapes (ReverseResult, SearchHit, NearbyPlace, WikiHit, ConnectionObservation) are mapped from raw upstream JSON in each service.
Consistency
A module-level timestamp gate serialises Nominatim calls to at least 1,100 ms apart (per process, not distributed). Overpass results are de-duplicated by OSM type/id, and ranking breaks ties by name and then id, so the same input always gives the same order. All upstream fetches use cache: no-store.
Failure
Overpass tries two mirrors in order and rethrows the last error only when both fail. The connection trace and Wikipedia summaries degrade gracefully: inbound headers or bare geosearch hits are still returned. A typed UpstreamError maps to HTTP 502 with a truncated upstream body, and Zod errors map to 400 with per-field issues. There are no explicit request timeouts apart from Overpass's own [timeout:25].
Testing
7 bun unit test files with 23 cases (classify 5, score 5, validate 5, geo 4, nominatim 2, nearby 1, exclusions 1); all upstreams are faked by passing in a fetch implementation, using Ibadan fixtures. 1 Playwright spec with 2 cases against the production build.

Techniques

  1. Upstream politeness gateEvery Nominatim call waits on a shared timestamp so requests are at least 1,100 ms apart, and every outbound fetch gets an identifying User-Agent. This is what OSM's usage policy requires before a public endpoint can sit behind an app.src/server/services/nominatim.ts:3 src/server/services/nominatim.ts:9 src/server/services/http.ts:22
  2. Ordered mirror failoverOverpass queries go to two mirrors in turn. A failing mirror is skipped and the last error is rethrown only when both fail, so one overloaded public instance does not take the feature down.src/server/services/overpass.ts:3 src/server/services/overpass.ts:58
  3. Deterministic connectivity scoringOne Overpass union over 13 tag selectors is de-duplicated by OSM id, put into 11 kinds, and scored as kind base plus an access bonus, scaled by a floored distance falloff. Ties break by name and then id, so the ranking can be reproduced and tested.src/server/services/overpass.ts:21 src/server/services/nearby.ts:17 src/server/services/score.ts:73 src/server/services/score.ts:78
  4. Partial-result degradationWikipedia summaries are fetched in parallel, and a failed summary still returns the geosearch hit. A failed connection trace still returns the inbound forwarded-for headers. Routes map UpstreamError to 502 and validation errors to 400.src/server/services/wikipedia.ts:53 src/server/services/ip.ts:33 src/app/api/nearby/route.ts:20
  5. Scope limits enforced as a testA unit test fails if person, IMEI, tracking or camera route directories are ever added, which turns the README's 'will not' list into a CI-checkable invariant.src/server/services/exclusions.test.ts:4
  6. Injected fetch for hermetic testsEvery service takes a fetch implementation as a parameter, so the tests run real mapping and header logic against fixtures without touching the network.src/server/services/http.ts:3 src/server/services/nominatim.test.ts:11

Counted

  • 2commits
    git rev-list --count HEAD
  • 5API route handlers
    ls src/app/api | wc -l
  • 7unit test files
    git ls-files 'src/**/*.test.ts' | wc -l
  • 23unit test cases
    grep -cE '^\s*(test|it)\(' per test file
  • 2e2e cases
    grep -c test( e2e/home.spec.ts
  • 13Overpass tag selectors
    src/server/services/overpass.ts:25-37
  • 1022server + API LOC
    git ls-files 'src/server/*' 'src/app/api/*' | xargs wc -l | tail -1
  • 2251total TS/TSX LOC
    git ls-files '*.ts' '*.tsx' | xargs wc -l | tail -1