Files
vngeoguessr/docs/tech-stack.md
T
tiennm99 c9a35dca9a docs: describe the region tree, and finish the coverage page migration
Reconcile every doc with the shipped code. All six described a flat
five-city model, and features.md, tech-stack.md, game-flow.md and
project-structure.md still documented the dart-throw over /images?bbox=
that the prebuilt panorama indexes replaced.

project-overview.md gains a Coverage note that classifies absent coverage
into its three causes -- not yet added, no street imagery, missing from
the boundary -- because a note that only says "partial" teaches
maintainers to ignore real gaps. Cu Chi is named as the one instance of
the third, which is the only one that is a defect.

A context hook had been denying access to src/app/debug/coverage/page.js,
so this also lands the two items earlier phases recorded as
undeliverable: the api/debug/city-coverage -> region-coverage rename, and
the page's migration from a flat city list to RegionSelect. With its last
caller gone, game.js drops CITIES, cities, cityNames, cityCenters and
cityBboxes; getCityIndex, indexedCities and fetchCityPanorama take names
that match what they now take.

Selecting a district with no boundary returned a 400 and left the
previous region's panorama count and outline on screen beside the error,
so Ho Chi Minh's 184,938 read as Cu Chi's. The error path now clears
everything derived from the previous region, and a null boundary removes
the outline rather than skipping the redraw.

Two plan-time claims did not survive contact with the code and the docs
follow the code: VN scores like any other node rather than being a
zero-scoring exploration mode, and the fan-out credits two levels when a
panorama falls outside every district outline.
2026-08-30 19:46:13 +07:00

4.0 KiB

Tech Stack

Frontend Framework

  • Next.js 15.5: React-based full-stack framework with App Router
  • React 19.2: Component architecture
  • Tailwind CSS 4: Utility-first CSS framework for styling

Street View & Mapping

  • Mapillary vector tiles: the z14 image layer is the index source. Read offline by scripts/build-pano-index.mjs with @mapbox/vector-tile + pbf, against a 50,000 requests/day cap
  • Mapillary Graph API: one call per round -- lookup by image id, ~230ms. Its /images?bbox= search is deliberately off the game path: it returns HTTP 500 in every dense district, because it counts images inside the box before applying the limit. See the header of src/lib/mapillary.js. api/debug/mapillary/route.js keeps the search as a diagnostic, and will reproduce those 500s
  • Leaflet: Interactive mapping library for guess placement
  • OpenStreetMap: Map tile provider for base maps; Nominatim supplies the administrative boundaries via scripts/build-region-boundaries.mjs
  • @photo-sphere-viewer/core: 360° panorama viewer

Geographic Processing

  • @turf/turf: distance, point-in-polygon, union, simplify, and point-to-line distance -- used both at runtime and by the offline builds
  • Generated region tree: src/data/regions/ holds the nodes, src/data/boundaries/<province>/ the simplified outlines, and src/data/panos/ the per-province panorama indexes
  • Client/server split: src/lib/regions.js is the client-safe view and imports nothing from src/data/panos/ or src/lib/pano-index.js. That boundary is enforced by an import-graph walk in tests/regions.test.js -- the indexes are ~29MB of exact answers
  • Server-side Calculations: All geographic processing on backend

Data Storage & Session Management

  • Upstash Redis (REST): Session and leaderboard storage via @upstash/redis SDK (REST client, no sockets)
  • Credential Flexibility: Accepts UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKEN (vanilla Upstash) or KV_REST_API_URL+KV_REST_API_TOKEN (Vercel Marketplace aliases)
  • Multi-tenant Key Prefix: All physical keys carry KEY_PREFIX (default vngeoguessr:) to safely share Upstash DB with other Vercel projects. Prefix applied centrally in src/lib/upstash.js; callers use logical keys only.
  • Key Namespaces: session:{id} (30-min TTL), leaderboard:{scope}, distance:{scope} (no expiry). {scope} is vietnam for the country and city:{regionCode} for every province and district -- the city: segment is kept so existing scores stay addressable
  • Sorted Sets: Leaderboard ranking with automatic trimming (top 200)
  • UUID v4: Session identifier generation
  • 30-minute Session Expiry: Automatic TTL-based cleanup

UI Components & Styling

  • shadcn/ui: Complete component library with "new-york" style
  • Radix UI: Headless component primitives -- dialog, label, slot, tabs, plus accordion (province expansion) and select (region picker)
  • Lucide React: Icon library
  • class-variance-authority: Component variant management
  • tailwind-merge + clsx: Dynamic class name handling

Testing

  • Vitest: Test runner for the logic in src/lib/, the API routes, the generated region data, and the leaderboard migration
  • In-memory Upstash fake: Default backing store, no service required
  • SRH + Redis (Docker): Optional lane running the same tests against real Redis

Development & Analytics

  • ESLint: Code linting with Next.js configuration
  • Turbopack: Development server bundler
  • @vercel/analytics: User analytics tracking
  • @vercel/speed-insights: Performance monitoring

Key Dependencies

  • uuid: unique session identifier generation
  • @upstash/redis: REST-based Upstash client; no socket pooling, works in edge compute and serverless
  • JavaScript Only: No TypeScript - pure JavaScript implementation
  • Individual Parameters: Functions use separate parameters instead of object destructuring
  • Note: redis (node-redis) package not used (replaced by Upstash REST SDK)