Stack reference¶
Every tool, library, and service in LocaScore — what it is, why it was chosen, and where it's used. Keep this page up to date when dependencies change.
For the logical components (pipeline, worker, frontend, etc.) and how they fit together, see Architecture.
Data pipeline — Python¶
All pipeline dependencies live in the hood-analyzer conda env.
pandas + numpy¶
What: Tabular data + numerics. The foundation. Used for: Every KPI dataframe, percentile ranks, score computation, parquet I/O.
geopandas¶
What: Pandas with spatial geometry (via Shapely). Used for: Loading GDB/GPKG files (SITG sources), commune assignment, coordinate transformations (EPSG:4326 ↔ EPSG:2056 Swiss LV95).
h3 (h3-py)¶
What: Python bindings for Uber's H3 hexagonal grid library.
Why: Equal-area hexagons at fixed resolutions, fast lat/lng→cell
lookups, easy k-ring neighborhoods. Resolution 10 (~150m cells) gives
us ~17K cells for the whole canton — a sweet spot between granularity
and compute cost.
Used for: The grid itself (generate_h3_grid), cell ↔ lat/lng
conversions, k-ring counts (e.g., playgrounds within 800m = k=5).
pandana¶
What: Network analysis on OSM street graphs. Uses contraction hierarchies under the hood for fast shortest-path queries. Why: Orders of magnitude faster than networkx for repeated shortest-path calls on the same graph. We compute ~17K × N POI distances per KPI — networkx would take hours, pandana takes seconds. Used for: Walking distances to transit stops, schools, parks, supermarkets, etc. Also underpins car/bike travel times via the per-mode network builds. Key limitation: Treats all edges as undirected — ignores one-way streets. The car model compensates with a hypercenter congestion multiplier. See Data pipeline.
osmnx¶
What: Downloads and processes OSM street networks as networkx
graphs. Can filter by network type (walk, bike, drive).
Used for: Downloading the three travel networks (walk, bike, drive)
for the canton. Output is converted to pandana format for routing.
Caching: We cache the downloaded networks in data/cache/ to avoid
hitting the Overpass API every run.
r5py¶
What: Python wrapper around R5, a multimodal routing engine from
Conveyal that handles GTFS transit + walking + cycling.
Why: The only realistic way to compute transit travel times in
Python. Uses GTFS schedules for TPG + SBB services.
Used for: transit_*_min KPIs (Tuesday 8:30 AM departure to
Cornavin, airport, CERN, UN).
Requires: Java 21+. Pipeline gracefully degrades if r5py is missing
(loads from previous parquet or falls back to Euclidean distances).
rasterio¶
What: Reads and samples geospatial rasters (GeoTIFFs).
Used for: Noise rasters (OFEV), air quality rasters (SPAIR). We sample
at each H3 cell centroid with a handful of vectorized rasterio.sample calls.
pyproj¶
What: Cartographic projections and transformations. Used for: Converting between WGS84 (EPSG:4326, for web/H3) and Swiss LV95 (EPSG:2056, for accurate metric distance calculations).
duckdb¶
What: Fast in-process OLAP database with excellent parquet support. Used for: Parquet I/O (reading cached data, writing the main output).
shapely¶
What: Planar geometry operations (polygons, buffers, unions). Used for: Canton polygon buffering (for OSM download extents), point-in-polygon for commune assignment.
Frontend — React / TypeScript¶
React 19¶
What: UI library. Why: Mature, huge ecosystem, fits a SPA perfectly. React 19's concurrent features + automatic batching help smooth out the report page animations.
Vite¶
What: Build tool + dev server. ESM-native, fast HMR. Why chosen over Next.js: LocaScore doesn't need SSR — the teaser and report pages are behind JS and partially behind auth. Vite's dev experience is much faster (HMR < 100ms vs Next.js' several seconds) and the production build is simpler (static files, no Node runtime).
TypeScript 5.9¶
What: Typed JavaScript.
Used for: Everything. The CellData type in src/types/report.ts
defines the contract with the worker (which has a matching type in
worker/src/types.ts::KVCellData).
Tailwind CSS v4¶
What: Utility-first CSS framework. v4 uses Oxide (Rust) for
compilation — very fast.
Why: Zero runtime cost, dark mode via dark: modifier, responsive
via md:/lg:, consistent spacing/color scale, great AI-assisted
authoring. Custom brand colors defined in src/index.css (--brand-*).
framer-motion¶
What: Animation library for React. Declarative, gesture-aware. Used for: Page enter animations, score gauge reveals, modal enter/exit, success state celebrations.
@tanstack/react-query (v5)¶
What: Server state management with caching, revalidation, mutations.
Why: The teaser/report/tokens data lifecycle is exactly what React
Query is built for. Cache invalidation on unlock is one line
(queryClient.invalidateQueries(['tokens'])).
react-router v7¶
What: Client-side routing.
Used for: All page navigation. Plus trackPageView hook in
router.ts to fire GA page views on every navigation.
react-i18next¶
What: i18n framework for React.
Why: We needed FR + EN from day one. i18next's browser language
detection, interpolation (t('key', { count })), and pluralization
(tokens_available_plural) cover all our needs. JSON files for
translations.
@nivo/radar + @nivo/bar¶
What: D3-based React chart library. Why chosen over Recharts: Nivo has better radar charts and more flexible theming (important for dark mode). Used for the Daily Life / Family / Smart Living radar charts and percentile bars.
react-map-gl + deck.gl¶
What: React wrapper for Mapbox GL JS + deck.gl for data visualization
layers (including H3HexagonLayer).
Used for: Report page maps (POI markers), future explorer map
(canton-wide hex overlay).
Lazy-loaded: ~1.6 MB gzipped, dynamically imported only when the user
opens a map.
h3-js¶
What: JavaScript port of H3. Used by deck.gl's H3HexagonLayer and
indirectly on the frontend (we never convert lat/lng to H3 on the client
— the worker does it server-side).
@supabase/supabase-js¶
What: Official Supabase client. Used for: Auth flows (signIn, signUp, onAuthStateChange) and getting the current session JWT to pass to the Worker.
Playwright¶
What: Modern e2e testing framework from Microsoft.
Why chosen over Cypress: Faster, better TypeScript support, better
multi-tab handling, headless mode works out of the box, and can test
mobile viewports with one config change.
Tests: tests/e2e/*.spec.ts. Run with npm run test:e2e. See
Frontend.
ESLint + eslint-plugin-react-hooks¶
What: Linting. Used for: Catching unused imports, hook rule violations, accessibility issues.
Backend — Cloudflare Worker¶
Cloudflare Workers¶
What: V8 isolates running TypeScript/JavaScript at Cloudflare's edge. Cold start < 5ms, stateless. Why: Zero cold starts (critical for UX on the teaser endpoint), edge-cached automatically, free tier is generous, and the KV + Workers integration is seamless.
h3-js (server-side)¶
What: Same H3 library, TypeScript build.
Used for: lat/lng → cellId in worker/src/utils/h3.ts before any
KV lookup. Never exposed to the client.
@supabase/supabase-js¶
What: Same client library, used on the worker with the service role key.
Used for: REST calls to tables and RPC functions (e.g., unlock_report).
Services (infra)¶
Cloudflare Pages¶
What: Static site hosting with global CDN, unlimited bandwidth on
the free tier, automatic HTTPS.
Used for: Hosting the frontend build (dist/). Custom domain
locascore.ch + wildcard preview URLs per deployment.
Cloudflare Workers (Paid plan — $5/month)¶
What: Edge compute + KV + Durable Objects. Why Paid: The free tier caps KV writes at 1,000/day — one full pipeline upload is 17,097 writes. The paid plan bumps this to 1M writes/month. Also unlocks 30s CPU limits (vs 50ms free) for future heavy endpoints.
Cloudflare KV¶
What: Eventually-consistent key-value store. Eventually-consistent reads at the edge (sub-10ms globally). Why: Perfect for pre-computed, read-heavy, immutable data. We can afford the ~60s global propagation delay on writes because data changes only when the pipeline runs.
Cloudflare Turnstile¶
What: "Privacy-preserving CAPTCHA" that Cloudflare built to replace reCAPTCHA. Invisible most of the time. Status: Currently dormant. The geocode endpoint still supports it but the frontend switched to Swiss Federal API geocoding (no CAPTCHA needed). Would re-enable for email capture if abuse becomes an issue.
Cloudflare Bot Fight Mode¶
What: Edge-level bot filtering. Blocks known bad bots and challenges suspicious ones. Used for: First line of defense on all public endpoints (teaser, email capture).
Supabase (Auth + Postgres)¶
What: Firebase-alternative with open-source Postgres, RLS, auth, REST (PostgREST), realtime, storage. Why: Generous free tier (50K MAU), real Postgres (not a NoSQL clone), built-in auth we don't have to maintain, and RLS gives us multi-tenant data isolation at the DB layer. Single platform for user data + auth is a huge simplification for a solo dev. Plan: Free tier — 500 MB DB, 50K MAU, 5 GB egress/month.
Stripe (+ TWINT)¶
What: Payment processor with Swiss market support. Why: Best-in-class developer experience, supports CHF natively, TWINT enabled (the Swiss mobile payment standard — essential for the local market). Used for: Checkout sessions for single/pack3/pack10 token purchases. Webhook for credit on success.
Mapbox¶
What: Map tiles, geocoding API, style editor. Used for: Vector tile rendering on the report page (client-side via react-map-gl). Not used for: Geocoding (we switched to Swiss Federal API). Plan: Free tier (50K map loads/month) — currently under budget.
Swiss Federal Geocoding API (api3.geo.admin.ch)¶
What: Official Swiss geocoder from the federal office. Covers all Swiss addresses with high accuracy. Why: Free, no API key, no commercial license restrictions (unlike Mapbox which has some real estate use limits), and it handles Swiss address formats natively ("Rue du Rhône 42, 1204 Genève"). Used for: Landing page address autocomplete + resolution.
Google Analytics 4 (optional)¶
What: Web analytics.
Status: Consent-gated — only loaded after the user accepts the
cookie banner. VITE_GA_MEASUREMENT_ID is optional; if unset, analytics
is a no-op (but events still log to console.debug in dev).
Used for: Funnel tracking. 33 events instrumented — see
Frontend.
External data sources¶
| Source | Format | License | Used for |
|---|---|---|---|
SITG (ge.ch/sitg) |
GDB via ZIP URLs | Free, attribution required | Canton boundary, municipalities, TPG stops, schools, business registry |
| OpenStreetMap | via osmnx/Overpass | ODbL, attribution | Walk/bike/drive networks, parks, playgrounds, POIs |
| OFEV/BAFU | GeoTIFF rasters | Free, source citation | Noise maps (road/rail, day/night) |
| SPAIR | GeoTIFF rasters | Restricted (permission pending) | Air quality (NO2, PM10, PM2.5) |
| OCSTAT | Hardcoded table | Free | Commune apartment prices (2024 resale) |
| Cantonal tax office | Hardcoded table | Free | Centimes additionnels per commune |
| opentransportdata.swiss | GTFS | Free, source citation | Transit schedules for r5py routing |
Current mitigation for SPAIR: air quality is shown in the free teaser only (not behind the paywall) until commercial licensing is resolved.
Development tools¶
conda (hood-analyzer env)¶
What: Python package manager. Why: The pipeline uses a mix of pure Python (pandas, numpy) and native-code libraries (geopandas, rasterio, pandana, h3) that are a pain to install with pip. Conda handles the binary dependencies cleanly. Setup: See Data pipeline.
Node.js (frontend/, worker/)¶
What: JS runtime for build tooling. Version: 22+ (Vite 8 requires it).
npm¶
What: Node package manager. Vanilla npm — no pnpm or bun yet.
wrangler (Cloudflare CLI)¶
What: Official Cloudflare CLI for Workers + Pages + KV. Used for: Deploying the worker, deploying Pages, uploading KV, managing secrets.
git + GitHub¶
What: Version control.
Repo: github.com/daderidd/localscore
MkDocs + Material theme¶
What: Static site generator for Markdown docs. Material theme adds
a polished look + search + dark mode + code highlighting.
Used for: This wiki. Built via mkdocs build, deployed to
Cloudflare Pages.
Why chosen: Python-native (fits the pipeline-heavy repo), beautiful
output, excellent AI-assisted authoring, and the user was already
familiar with it.
VS Code¶
What: Editor. Not prescribed, but file path references in this wiki
use the file:line format that VS Code recognizes for click-to-open.
What we're NOT using (and why)¶
No Next.js / Remix / Astro¶
We considered Next.js but picked Vite instead. The teaser and report pages are behind JS (no SEO benefit from SSR), auth flows don't need server components, and Vite's dev speed is materially better. If we ever add public marketing pages that need SEO, we'd consider adding a static Astro site alongside Vite.
No Redux / Zustand / Jotai¶
We have @tanstack/react-query for server state and useState for
local state. No global store. This is intentional — the component tree
is shallow, and the cross-component state we need (user session, auth)
is handled by AuthContext.
No Tailwind UI / shadcn-ui component library¶
All components are custom, written directly with Tailwind utility classes. We have <30 UI components total, and the design system is simple enough that importing a 200-component library would be overkill.
No test framework for the Python pipeline¶
We have Playwright for the frontend, but no pytest yet for
code/. The pipeline is deterministic + slow, so we rely on eyeballing
the parquet output after changes. Would add pytest if the pipeline
becomes more complex.
No CI / GitHub Actions¶
Deploys are manual via wrangler. Tests are manual via npm run test:e2e.
Would add CI if anyone else joined the project.
No Sentry / error monitoring¶
Cloudflare Workers has built-in logs and a dashboard. Sentry is overkill for a solo project with <100 users. Would add it before any serious launch.
No CDN for assets beyond Cloudflare Pages¶
Pages already CDN-caches everything globally. No separate asset CDN needed.
No Redis / other cache¶
KV is our cache. For reports, the Worker sets
Cache-Control: private, max-age=3600 so the browser handles
client-side caching.
No queue / background jobs¶
Pipeline runs manually. The Stripe webhook is handled synchronously in the Worker. If we ever need async work (email sending, PDF generation), we'd use Cloudflare Queues.
No serverless functions outside Cloudflare¶
One platform, one mental model. Supabase Edge Functions exist but we haven't needed them — the Worker handles all custom logic.