Skip to content

Stack reference

← Back to index

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.


← Back to index