# Pitfalls Research **Domain:** Telegram-first GPS navigation (bot + Mini App + OSM routing), KL metro v1 **Researched:** 2026-10-22 **Confidence:** HIGH for security/platform pitfalls (official Telegram/Cloudflare/OSMF sources), MEDIUM for navigation-engine UX behavior (community + known engineering practice), MEDIUM for Malaysia-specific OSM data quality. > Phase names used below assume a v1 roadmap roughly shaped like: > P1 Design-system foundation → P2 Bot + destination search → P3 Mini App shell + location → P4 Route display → P5 Live turn-by-turn guidance → P6 Hybrid chat guidance → P7 Hardening/private launch. If the actual phase list differs, the "phase to address" still reads naturally by role. --- ## Critical Pitfalls ### Pitfall 1: Trusting initDataUnsafe or validating initData in the wrong place **What goes wrong:** The Mini App receives `initData` (a Telegram-signed payload containing user id, auth_date, etc.). The most common and most fatal Mini App security error is treating anything arriving from the client as proof of identity: using `initDataUnsafe`, accepting a plain `user_id` request parameter, or "validating" the hash client-side. Result: anyone can POST `{"user_id": }` and navigate-as-another-user, and a captured `initData` string works forever because `auth_date` freshness was never checked. **Why it happens:** `initDataUnsafe` is named for convenience, not as a warning label. Telegram's docs state plainly that validation must happen on the bot's server, but client-side dev flows make the unsafe path feel like it works. **How to avoid:** - Send the **raw** `initData` string from the Mini App to the backend (a Cloudflare Worker/Pages Function); never parse it client-side for auth. - On the server: build the data-check string from all key–value pairs except `hash`, sorted alphabetically, joined with `\n` (URL-decode values; watch for escape characters inside values like `photo_url` — a classic implementation bug). Derive the secret key as `HMAC-SHA256(bot_token, "WebAppData")` — the key is **not** the raw bot token (a recurring mistake). Compute `HMAC-SHA256(data_check_string, secret_key)` and compare to `hash` in constant time. - Reject payloads older than a short window (e.g. 5–10 min) by `auth_date`; exchange the validated payload **once** for your own session token and authenticate all later Mini App↔backend calls with that token. - Never send the bot token to the client. If third-party verification is ever needed, the newer Ed25519 signature path exists (signatures are base64url **without padding** — many languages need `=` padding restored before decoding). - Unit-test the validator with vectors from Telegram's docs plus tampered/stale payloads before anything else ships. **Warning signs:** Any auth code path reachable via curl with a hand-rolled JSON body; a "TODO validate" comment; an endpoint that accepts user_id as a plain parameter. **Phase to address:** P1–P2 (build the validation utility as the first backend artifact, before any Mini App page consumes identity). --- ### Pitfall 2: Geolocation that dies silently inside the Telegram WebView **What goes wrong:** `navigator.geolocation` inside a third-party WebView can fail **without ever showing a permission prompt** — the error callback fires with code 1 ("User denied Geolocation") even though the user denied nothing. This is exactly what happens when the host app's WebView does not implement the geolocation permission bridge (`onGeolocationPermissionsShowPrompt` on Android). Telegram ships several clients (iOS, Android, Telegram Desktop) that differ in how — or whether — they pass the prompt and position through. The worst outcome is silent: no prompt, no position, no useful message. A navigation app built against one client's behavior can be non-functional on another. **Why it happens:** Mini Apps run inside Telegram's own WebView, not Chrome; the developer controls nothing about the permission bridge. Developers test in Chrome/Safari first — where the prompt works — and build the entire guidance loop on a primitive that may be dead in-device. **How to avoid:** - Prefer the **WebApp LocationManager** API (Bot API 8.0+: `LocationManager.init()`, access-state check, `requestLocation`) as the primary path — it exposes a location-access state and uses Telegram's own permission plumbing. Keep `navigator.geolocation` as per-client fallback. - Build a **client test matrix from day one**: Android Telegram, iOS Telegram, Desktop, plain browser. Test permission *grant*, *deny*, *re-ask after deny* (denials may persist with no retry path), and *no-fix scenarios* (no signal, stale fix, accuracy 100m+ — normal in Malaysian high-rise canyons). - The very first P3 vertical slice is "read position inside the Mini App on a real phone in Telegram" — nothing else is built until that works across the matrix, with a graceful denied-state screen (re-enable instructions + manual "set origin on map" fallback). **Warning signs:** "Works in browser dev; fires PERMISSION_DENIED on device with no prompt ever shown." Devs testing only in Chrome. No denied-permission screen in the design. **Phase to address:** P3 — multi-client location permission is the exit criterion of the location slice. --- ### Pitfall 3: Turn-by-turn complexity underestimated: snapping jitter, recalculation storms, ETA whiplash, battery drain **What goes wrong (four linked failure modes naive implementations all hit):** 1. **Snapping/jitter:** raw GPS fixes jitter ±5–30m (worse in urban canyons and multi-level roads common in KL — parallel frontage/service roads, flyovers). The puck teleports between the real road and adjacent/parallel/underneath roads; happens *because position is displayed raw*. 2. **Recalculation storms:** recalculating the full OSRM route on every off-route ping (a jitter burst easily reads as off-route) creates request storms, flickering routes — and route *oscillation*: the recalculated route confuses position matching, which recalculates again. 3. **ETA whiplash:** every recalc rewrites remaining time, so the ETA visibly jumps around — the canonical "half-built navigator" smell. 4. **Battery drain:** a continuous high-accuracy `watchPosition` at tight intervals keeps the radio awake and burns battery within minutes; combined with no screen-wake control in a webview, users get "drains my phone" reviews. **Why it happens:** "Position → route progress → next turn" looks trivial in demos; real navigators have a dedicated location/snapping pipeline and an explicit navigation state machine that tutorial projects never show. Web examples (Leaflet + routing plugin) draw routes, not vehicle guidance. **How to avoid:** - Treat guidance as an explicit **state machine**: `en-route → approaching-maneuver → maneuvering → off-route → rerouting → arrived`, with hysteresis: minimum move distance (or ≥20–30m) and heading correlation with the route before "off-route" fires, plus N consecutive bad fixes. - **Snap the position to the route polyline** (project onto active route geometry), not to the nearest road-graph edge — enough for v1, no map-matching infra needed. Maneuver distance/time always computed from snapped progress, not raw fix distance. - **Debounce rerouting:** max one OSRM call per off-route event, cool-down (e.g. ≥5–8s), collapsing pending re-requests. Also protects the Cloudflare subrequest/CPU budgets (Pitfall 8). - **Smooth user-visible ETA:** only surface a recalc if the change is meaningful; average speed over a window, not instantaneous GPS speed. - Throttle position updates adaptively (≈1s while moving, 3–5s standstill); Wake Lock only during active navigation, released otherwise. - Test with **recorded drive replays** (JSON position logs played through the state machine in CI) + a real phone mounted in a car. Golden-route replays double as the Malaysia-data regression net (Pitfall 7). **Warning signs:** Route fetch inside a `watchPosition` callback with no debounce/cooldown; ETA updates every tick; distance-to-turn from raw fix distance; >10% battery/10min in internal QA. **Phase to address:** P5 (live guidance) builds it correctly; flag in P4 so the route model already carries step geometry + snapped-progress support from day one. --- ### Pitfall 4: The Telegram platform bridges are just… different: localStorage, 100vh viewport, back button **What goes wrong (three recurring traps):** 1. **Storage:** `localStorage` inside Telegram's WebView **may be wiped** on iOS and some desktop builds; the official Telegram client storage (`CloudStorage`) exists precisely because in-webview persistence is unreliable. Session tokens cached in `localStorage` vanish → spontaneous logouts after restart. 2. **Viewport / `100vh`:** CSS `100vh` ≠ the Telegram viewport (compact/expanded states, header overlay, keyboard), and the keyboard changes `viewportHeight` while `viewportStableHeight` exists exactly for that. Layouts sized off `window.innerHeight` or `100vh` jump when the keyboard opens and slide under the Telegram header. 3. **Back button:** a custom in-page back arrow competes with Telegram's **BackButton** (Bot API 6.1+) and Android hardware back; a BackButton click with no handler closes the whole Mini App back to the chat; iOS vertical-swipe can close the app unless `disableVerticalSwipes` is called; `enableClosingConfirmation` can prevent accidental closure during navigation (but is annoying while browsing). **Why it happens:** Developers build a normal web app, add the `telegram-web-app.js` script tag, and meet the platform afterward. **How to avoid:** - Session token → `WebApp.CloudStorage` (or an HTTP-only cookie on a same-site backend); `localStorage` only for throwaway UI state. - Shell tracks Telegram viewport: `WebApp.ready()` + `WebApp.expand()` on mount; use the viewport/`viewportStableHeight` values (or bind the SDK's CSS variables); never `100vh` for the app shell; test with the search keyboard open. - Bind Telegram's BackButton to the router (show/hide on route change, `onClick` → router back), keep **one** navigation stack, call `disableVerticalSwipes` (Bot API 7.7+) so map interactions don't close the app, and enable closing confirmation only during active navigation. - `WebApp.isVersionAtLeast()` / feature detection before using any advanced API; graceful fallbacks for old clients. - Test in-device (all four clients from Pitfall 2), not just the browser. **Warning signs:** Content sliding under the Telegram header; keyboard shoving the route list; BackButton click dumps users to the chat mid-navigation; sessions vanishing on iOS restarts. **Phase to address:** P3 (Mini App shell) — the shell must be Telegram-aware from the first commit or every screen inherits the debt. --- ### Pitfall 5: Nominatim as the production geocoder — rate limits and local-landmark weakness **What goes wrong:** Two-part failure. (1) Nominatim's official usage policy allows **at most 1 request/second** with "no heavy use" — a per-user search into the public instance violates the policy and gets 429/blocked (and the block looks like an app bug). (2) The queries users actually type are the weakest cases: "KLCC" (abbreviation for a **complex**: Kuala Lumpur City Centre *and* Suria KLCC), mall names ("Pavilion", "Sunway Pyramid"), subdistrict/kampung names, Malay road abbreviations ("Jln", highway codes like "DUKE", "SKVE", "Sprint"). Public-Nominatim ranking for local landmark phrasing is mediocre without a deliberate alias/index layer. Result: the flagship "search a destination" requirement — a validated core promise — fails on the marquee KL queries it was chosen for. **Why it happens:** Nominatim is the default "free OSM geocoder" and its local-phrasing weakness isn't obvious until someone types "KLCC" and gets something wrong — no feedback loop yet. **How to avoid:** - Build a small **curated landmark/alias index** for KL first: top ~200 destinations (malls, landmarks, transit hubs, attractions) with OSM ids + abbreviation/alias expansion ("KLCC" → Suria KLCC / KLCC Park / KLCC LRT station — user picks). Tiny static dataset (JSON in the bundle or KV) — makes flagship queries natively excellent regardless of geocoder choice, with zero privacy cost. - Route fallback searches to an **OSM geocoder you can scale**: Photon (komoot) is the standard Nominatim successor (better autocomplete; self-host on a Malaysia extract) or a managed OSM provider (Geoapify, MapTiler geocoding — license-compliant). Nominatim public = dev/prototype only. - If any public geocoding is used at all: cache aggressively (results are near-immutable per query), rate-limit client-side (debounce ≥300ms, min 3 chars), honor policy identification (User-Agent/email metadata). - Reverse geocoding (origin label) is low-frequency but cache by geohash/rounded coords. **Warning signs:** "KLCC"-style queries failing or passing only via hardcoded synonyms; any per-keystroke synchronous Nominatim call path; 429s in logs; search latency >1s in-device. **Phase to address:** P2 (destination search) — the alias index and geocoder choice are that phase's substance, not polish. --- ### Pitfall 6: OSRM demo-server dependency + traffic weighting assumed free **What goes wrong:** Developing against `router.project-osrm.org` and never fixing the dependency: the OSRM project itself says the demo server is **NOT SUITABLE FOR PRODUCTION USE** (e.g. leaflet-routing-machine's error text relayed from the demo policy). It also runs only the car profile on a periodically-updated planet extract: no uptime/existence guarantee, arbitrary policy throttling, and **no traffic weighting** — the traffic-update feature needs nightly upstream traffic files and specific ingestion infrastructure that no public server offers. So "we'll add live-ish ETA later" silently implies standing up a data pipeline. Another classic: not switching to a private instance *before* the launch — being the app where "routing is down" on demo-day. **Why it happens:** The demo API is identical to a self-hosted one, so the swap feels like "just change the URL" — until the routing requirements (profile tuning, extrusion from a Malaysia extract, traffic files) reveal a real deployment/task chain. **How to avoid:** - Stand up **your own tiny OSRM** when route display first ships: Geofabrik Malaysia-Singapore-Brunei .pbf extract (~50–100MB) → `osrm-extract` → `osrm-contract` → `osrm-routed`, comfortably within a small VPS/Fly/GCP VM (few GB RAM). The Cloudflare Worker proxies it (respecting Pitfall 8 budgets — this is actually a clean way to stay within subrequest/CPU limits). - Design a **route-provider interface** in code (getRoute(from,to,profile) → geometry+steps+duration) so demo-OSRM vs self-hosted is a config swap, never a rewrite; traffic weighting later slots in without touching the client. - Don't promise traffic-aware ETA in v1: default OSM speeds via OSRM duration-prediction is what you get; ship honest "typical, indicative" ETAs and defer the traffic feature entirely (trigger condition: enough opt-in telemetry, or third-party traffic data chosen — both later milestones with their own research). - Cache route responses keyed on (from, to, profile) briefly (best-effort freshness) both to spare the OSRM instance and to keep the Cloudflare invocation fast. **Warning signs:** `router.project-osrm.org` in any production configuration; no route-timeouts/error handling; no test for the case "demo server slow/absent"; no deployment plan for the OSRM instance anywhere in the roadmap. **Phase to address:** P4 (route display) — *before* guidance makes route calls latency-sensitive and chatty (P5). --- ### Pitfall 7: Malaysia/OSM data quality gaps — the app's is only as good as its map data **What goes wrong:** OSM Malaysia data has known weak zones relative to commercial maps: sparse lane-level geometry (`turn:lanes` tags largely absent on Malaysian roads), inconsistent multi-level/flyover modeling (interchanges where ground and elevated links appear to intersect), inconsistent Malay road naming/aliases (Jalan vs Jln; Lebuhraya vs federal expressway codes like DUKE/SPRINT/SKVE/SILK/KESAS), sparser building/POI coverage in fast-growing outer corridors, and incomplete address/portal/POI detail outside central KL. Each ingredient failure surfaces as a navigation bug: wrong-side/off-by-exit maneuvers, puck positioned on the wrong parallel link, destination snapping to a block centroid instead of the entrance, "turn left in 20m then left in 10m" instruction fragments from data artifacts, and search results not finding POIs that a local knows exist. **Why it happens:** The correctness contract of a navigator ("gets me there, tells me the right turn") depends entirely on the least-good map ingredient; contributors map what they see, not necessarily what a driver needs (turn-lane modeling is obscure and tedious; OSM tooling makes it optional). **How to avoid:** - Set the expectation explicitly in the roadmap: v1 quality bar = **"correct on KL main corridors, known limitations in lane-level and interchange micro-detail"** — not Google-equivalent. Manage this in copy design and docs, not by lying to users. - Define a small **golden-route test set** (10–20 real KL driving pairs: KL Sentral → Mid Valley, Duta–Sungai Buloh, LDP/Penchala interchange, one Ampang KLC-style labyrinth, one Bangsar tangle) and replay them as regression tests through the guidance engine (doubles as Pitfall 3's replay corpus). - Heuristics to normalize Malay road naming and highway-code abbreviations in search input (part of the alias index of Pitfall 5 — make this explicit work in the search phase, not an "if time permits"). - For routing quality bugs, capture **data-vs-code bug categories** (bad one-way? missing lane info? geometry wrong? OSM data missed a flyover/underpass?) — file to OSM-community (added-value via our app; the eventual reports feature becomes this pipeline, but is out of v1 scope per PROJECT.md). Route-level UX behaviors should not depend on lane geometry in v1 (no lane-decision colors); stick to maneuver-level guidance. - Don't promise street/lane-level precision externally in v1; in fact omit any claim of "lane guidance" from copy — managing user expectation honestly is its own v1 feature ("less, but better"). **Warning signs:** Golden-route tests failing on route *shape* only (data, not code); maneuver list contains nonsense micro-steps; no navigation QA on real KL corridors; product copy claims precision unmet by data. **Phase to address:** P4 (golden routes) and P5 (guidance restraint on data-poor routes); bug-category capture from first QA session; OSM report loop explicitly excluded from v1 (and planned in the reports milestone). --- ### Pitfall 8: Cloudflare Pages/Workers limits mistaken for "we don't need websockets so we're fine" **What goes wrong:** Pages Functions are Workers, and Workers have quotas per invocation and per plan: **free plan = 10ms CPU per invocation, 50 subrequests per invocation, 100k requests/day** (paid: 10k subrequests, same daily CPU shape; Pages static assets serve unlimited requests). Per-user backend calls are small (binary initData→session, geocode-proxy, route-proxy, chat turn-cards). But it's easy to silently blow a single invocation's 50-subrequest ceiling by a fan-out handler (e.g. one "plan navigation" endpoint doing: alias lookup → geocoder → OSRM → map static asset) or by calling routing/geocoding per keystroke/pan, or by firing subrequest-heavy jobs post-hoc (during dev only — production plan's 10ms CPU simply isn't enough for heavy parsing/matching/blocking compute, causing silent dropped requests). The common mistake is not realizing Cloudflare *does* provide almost-free hosting — the mistake is architecting phases to require patterns (per-frame subrequests, in-Worker geo computation) that Workers is not built for. **Why it happens:** Pages is described as "free hosting"; the per-invocation subrequest/CPU model applies to Functions and is documented separately. With no background workers ("websockets aren't needed"), the plan model is very easy to satisfy accidentally — hard limits only surface when the architecture is wrong. **How to avoid:** - Architect backend so **one user action = one small invocation chain**: initData authenticate (no sub fetch), geocode (cache + 1 upstream call), route (cache + 1 OSRM call), turn-cards (1 bot API call). Never fan-out into per-item loops in a single invocation. - Use **Cloudflare KV/Cache API** to serve the KL landmark alias index and cached routes — cache-first is the most important budget-free win (a small hotspot region + modest query diversity means routes/alias hits are extremely friendly to caching). - Client-side debounce: search debounce ≥300ms, no per-pan route re-fetch (viewport is device-local; no route call per map move). - Instrument during dev (`wrangler tail` shows subrequest/CPU usage and duration per invocation) and set an explicit, phase-reviewed budget ("route call ≤2 subrequests; session init ≤1; geocoder ≤1"). - Plan-time acceptance: free-plan viable at launch only if **cache-hit-rate > ~85% and ≤6 subrequests worst-case per invocation**; paid plan ($5/mo) is the documented fallback, not a panic. - CPU: keep Worker logic light (validate, proxy, cache lookup); offload any heavy geo computation to the OSRM instance itself, not the Worker. **Warning signs:** `wrangler tail` showing >10 subrequests per invocation on one endpoint; any OSRM-call-per-map-pan logic; no debounce before geocoder; cache hit-rate never measured; first "slow" fan-out endpoint compiles without any cache. **Phase to address:** P4 (route provider + caching), and keep as a recurring review item in every backend-touching phase; a "budgets audit" entry in the pre-launch hardening phase (P7). --- ### Pitfall 9: Design-fidelity death by translation — Tailwind-CDN mockups rebuilt into generic component code **What goes wrong:** The mockups in `stitch_minimalist_route_navigator/` are single-file HTML built with **Tailwind via the Play CDN** — a JIT-in-browser dev tool that encourages arbitrary values (`bg-[#1A1A1A]`, `text-[13.5px]`, one-off letter-spacing tweaks, hand-tuned asymmetrical spacing/micro-offsets) that give the Telemetry Rams look its life. The standard failure mode: the real app is rebuilt from a component library or normalized spacing scale and every one-off arbitrary value gets "cleaned up" into the nearest 4px bucket — cumulatively flattening the hand-tuned, optical, asymmetrical charm into yet another Tailwind dashboard. This never looks like one mistake; it's dozens of small normalization decisions, each individually defensible, that quietly destroy the project's most important intangible: the "less, but better" evidence of taste. The user has flagged this as the **#1 v1 risk**, and the trap is that the *natural act of "doing it properly" (tokens, libraries, scales)* is precisely what erases the fidelity. **Why it happens:** Tailwind-CDN mockups carry no tokens layer; components imported from libraries come with default spacing/rounded/metaphors that fight the design; "refactor" PRs silently restructure spacing/typography; Helvetica Neue's availability differs by platform (system font on some, fetchable elsewhere) while webfonts change optical weights and rhythm. **How to avoid:** - **Extract design tokens first, before any app screen**: write `design-tokens.css`/`tokens.ts` literally *from the mockup files* (colors incl. exact hexes — Signal Orange #EB8008, Functional Red #ED3F1C, Olive Green #736B1E, Soft Beige #D9D2C6, Matte Black #1A1A1A; type scale/weights; 8px grid plus documented *intentional exceptions*; radius set; icon line weights). Anything not in tokens requires a sunset/review flag, not silent deletion. The mockup HTML is the source of truth, not the Tailwind docs. - **Decide the component approach deliberately**: no default-styled component library as the visual base in v1 (not shadcn/MUI-look as base); hand-rolled Rams components for the ~15 pieces v1 actually needs, built on tokens. Map is a container whose overlays use the same tokens (route line color = Signal Orange, etc.). - Wire **font reality** into the design-system phase: expect Helvetica-system-font fallback vs Inter; verify optical weights survive; measure line-height rhythm in real webview render, not in Figma/license. - **Keep fidelity verification as a named acceptance artifact** per UI-bearing phase: side-by-side gallery-vs-mockup review (rotated-mock vs real screenshot at matching viewport), approved by the user *before* screens are declared done. Pass/fail judged against *the intent* — rhythm, optical relationships, asymmetry — not pixel-diff, because wrapped-in-Telegram-chrome and font substitution guarantee pixel drift. - If any "cleanup/refactor" PR touches spacing/color/typography without a mockup diff, reject it at review. **Warning signs:** Component library import in package.json (radix/shadcn MUI defaults); a diff that touches spacing/color/typography without mockup reference; screens built from memory of mockups instead of side-by-side against the live HTML; any "we'll polish fidelity at the end" plan. **Phase to address:** P1 (tokens + component gallery, gated by user approval against mockups); re-verified at the end of every UI-bearing phase (P3, P4, P5, P6). --- ### Pitfall 10: Ops-stack scope creep — RabbitMQ/PostGIS/Grafana before any user **What goes wrong:** IDEAS.md lists RabbitMQ, Prometheus/Grafana, Neon (Postgres), Resend etc. — aspirational services from the vision doc. Classic greenfield-scope mistake: v1 ships nothing navigable because weeks go to infrastructure for scale that doesn't exist and for features (`incident reports`, telemetry pipelines) explicitly **out of scope** this milestone (PROJECT.md). PostGIS has no obvious v1 consumer: one region, a tiny curatable POI index, no user sessions to persist (privacy posture *helps* here — almost nothing must be stored). RabbitMQ serves incident broadcasts — deferred. This pitfall never looks like a mistake in the moment because each piece is "obviously needed eventually". **Why it happens:** Infrastructure feels like progress and produces no design-fidelity anxiety that blocks work; primitives (queues, dashboards) absorb low-confidence energy better than shipping does. **How to avoid:** - Roadmap rule stated up front: **every v1 service exists to serve a shipping requirement or it doesn't ship.** The legitimate v1 backend isolates to: one Worker (initData → session, geocoder + route proxy via cache, turn-card bot replies) + KV/Cache; at most one Postgres (Neon free) when opted-in telemetry or reports ship, **not before**. - Replace each aspirational service in the roadmap with a **trigger condition**: "PostGIS: when incident reports start producing route-impact data" / "Grafana: when load patterns deny CF built-in logs" — this keeps ambition visible without consuming phase capacity. - Grafana/Prometheus = nothing in v1; use Cloudflare's own invocation logs + a Telegram-bot error webhook (the bot is a free ops-alerting channel). - Treat any mid-plan replacement of UI work with infra work as a re-scoping alarm in phase-gate review. **Warning signs:** "Plumbing" tickets with no user-visible change; imports of queue libraries; schema design for tables whose features are out-of-scope; dashboards with no metric consumers. **Phase to address:** Milestone-planning discipline and every phase gate, not a specific phase; IDEAS.md aspiration list should be re-read (and sorted into "now / trigger-conditions") at milestone close. --- ## Technical Debt Patterns | Shortcut | Immediate Benefit | Long-term Cost | When Acceptable | |----------|-------------------|----------------|-----------------| | Route calls direct-to-OSRM from client code (no provider interface) | Zero setup, fastest possible route on screen | Coupled rewrite when self-hosted OSRM + caching + traffic arrives; leaks infra into UI code | Spike/demo phases only; never in the phase that ships route display | | `localStorage` for session token | One line of code | iOS/client-dependent wipe → spontaneous logouts | Never — CloudStorage/cookie is the same one line | | `100vh` / fixed pixel layout | Looks right immediately in dev | Keyboard/header shifts break it on every client | Never — viewport bindings exist | | Trust `initDataUnsafe` in dev, "validation later" | Auth works instantly | Security hole ships if "later" slips; auth-shape rewrite | Never — the validator is one file; build it in the first backend commit | | Demo OSRM + public Nominatim without a provider abstraction | Day-1 route+search | Policy violation/rate walls at launch scale; forced rewrite at both call sites | Spike/prototype only | | Full route recalc on each off-route ping | Simple code path | Request storms, flicker, ETA whiplash | Never — debounce is ~10 lines | | Skipping golden-route test corpus | Faster start | Data/code bugs invisible until real drivers complain; no regression net for guidance changes | Only if human-corridor QA is explicitly scheduled within days of the guidance phase | | Tailwind-Play-CDN styles carried into production app code | Fastest build path | No tokens layer → design fidelity decays in every subsequent PR | Never — extract tokens in P1 instead | | Hardcoding "websockets not needed" as "no server push" | Simplifies P3 planning | Hybrid guidance (P6) needs message timing; re-planning when chat turn-cards arrive | OK for UI-only phases; revisit when P6 design starts | ## Integration Gotchas | Integration | Common Mistake | Correct Approach | |-------------|----------------|------------------| | Telegram initData | HMAC with raw bot token as key; unsorted data-check-string; skipped URL decoding; no `auth_date` freshness check | Key = HMAC(bot_token, "WebAppData"); sorted `\n`-joined string; constant-time compare; short TTL + session-token exchange; test with official vectors + tampered payloads | | Telegram WebApp JS API | Calling features without `WebApp.isVersionAtLeast`; skipping `ready()`; assuming BackButton exists on every client | Feature-detect with version gates; graceful fallback screens; SDK init before any API call | | Telegram LocationManager | Assuming `navigator.geolocation` behaves identically in every client; skipping the access pre-check | Check LocationManager access state first; fall back to `navigator.geolocation`; always weapon the denied screen + manual-origin fallback | | Nominatim | Per-keystroke calls; no cache; exceeding 1 rps by design | Debounce ≥300ms; KV/Cache; own POI alias index; Photon/managed geocoder as the primary | | OSRM | Demo server in production; ignoring mismatch 1-overview vs steps geometry scale; polyline decoded at wrong precision (5 vs 6); forgetting `steps=true` for guidance | Own instance; unit-tested response parser; polyline6 decode; always request `steps` for guidance | | Map tiles | Hotlinking free tiers (style and quota burn); tile style visually indifferent/blank clash shapes with Rams basics | Pay/hosr tiles; apply muted Rams-compatible basemap styling (vector styles or CSS filtered raster); client-side tile caching | | Cloudflare Workers/Pages | Fan-out subrequests in one invocation; no caching; treating .dev vars as secrets | One function per user intent; KV/Cache-first; secrets in dashboard bindings; `wrangler tail` during dev | ## Performance Traps | Trap | Symptoms | Prevention | When It Breaks | |------|----------|------------|----------------| | Per-keystroke geocoding without debounce/cache | Request count ≈ keystrokes × users; 429s; battery | Debounce ≥300ms, min 3 chars, KV cache, local alias index first | Immediately at search ship; hundreds of QPS with a handful of users | | `watchPosition` at max frequency for whole session | Battery death; UI churn; health-app mistrust | Speed-adaptive sampling; stop when app idle/suspended; Wake Lock only during active guidance | Noticeable in QA drain test; ship-blocker above ~8–10%/hour | | Re-fetching full route geometry on map pan/zoom | Workers subrequest storm; janky map | Geometry fetched once per navigation session; viewport decisions are device-local | With the first handful of concurrent navigators | | Route fetch + geocode + POI enrichment in one invocation > free-plan subrequest ceiling | Invocation aborts with opaque limit errors | Cap fan-out, cache, split into separate invocations | The day "plan navigation" composes three things — usually at pre-launch testing | | Bundling all KL POIs into the client bundle | MB-scale JS; slow first open on 4G | POI search stays server-side/aliased; client ships route + narrow nearby-detail JSON on demand | First field test in-device | ## Security Mistakes | Mistake | Risk | Prevention | |---------|------|------------| | Client-side initData validation or trusting `initDataUnsafe` | Full identity spoofing — navigate as any user, spam any scoped API | Server-side HMAC validation (Pitfall 1); session-token exchange | | Bot token shipped in client bundle or exposed in Pages build env | Bot hijack, spam, forged initData | Token only in Worker secret bindings; never client `"NEXT_PUBLIC_"`-style exposure | | Unvalidated `start_param` deep-links | Injection into Mini App navigation logic; stats/games abuse | Treat start_param as untrusted; whitelists only | | Turn-cards or route sharing w/o rate limits | Spam vector; report-feature abuse when it later ships | v1: private-chat flows only; per-(user, minute) rate limits; reports feature gets identity + rate-limit design before it ships | | Server-side location/track persistence "for debugging" | Direct contradiction of the privacy promise; irrecoverable trust hit | No location persistence in v1; ops logs redact coordinates (round to ~100m if ever needed) | ## UX Pitfalls | Pitfall | User Impact | Better Approach | |---------|-------------|-----------------| | Search that doesn't know local landmark language ("KLCC", mall names, subdistrict names) | Faith in the marquee feature breaks first | Alias/POI layer (Pitfall 5); tune from real tester queries | | Guidance UI demanding driver attention (dense lists, small taps while moving) | Unsafe + app abandonment | Rams restraint as an advantage: next-maneuver only, large type, distance-first | | No screen-awake handling — display sleeps mid-navigation (or GPS runs forever after idle) | Anger in both directions | Wake Lock only during active-navigation state (test per-client support); degrade to Telegram chat turn-cards (P6 hybrid) | | Permission prompts at the worst moment (launch straight into guidance with no location) | Denial reflex | Prompt only at the "Start from here?" moment with one-tap confirm; map-origin fallback always available | | Raw-fix puck floating off-road | Distrust ("is this app even right?") | Snap to route; honest "recalculating…" state (Rams microcopy) | | ETA flipping as the vehicle moves | "Numbers wiggling, tired of this" | Smoothing; deltas shown only when meaningful (Pitfall 3) | | Guidance UI lingers forever past arrival (no arrival detection) | Feels unfinished, buggy | Arrival detection on snapped progress → brief completion card → done state | ## "Looks Done But Isn't" Checklist - [ ] **Auth:** tampered hash rejected in CI; stale `auth_date` (e.g. 10-min old) rejected; raw-curl calls rejected; token rotation procedure documented - [ ] **Location:** Android/iOS/Desktop/browser matrix tested including POST-deny retry + indoor no-fix behavior; denied screen and manual-origin fallback shipped - [ ] **Shell:** keyboard-open doesn't shove content; Telegram header never overlaps; BackButton works and never double-pops during active navigation; vertical swipe doesn't close app mid-guidance; sessions survive restart on iOS - [ ] **Guidance:** recorded-drive replay passes; one reroute per off-route event (not a storm); route-ETA stability follows the smoothing rules - [ ] **Golden routes:** 10–20 real KL pairs return sane routes and readable maneuvers (no 12-turn-in-30m sequences on ramp data artifacts) - [ ] **Battery/screen:** hourly drain < ~10% in QA; wake-lock behaves exactly across active navigation vs browsing vs backgrounded - [ ] **Cloudflare budget:** subrequest/CPU usage measured under replay load; cache hit-rate visible in logs; worst-case invocation within free-plan ceiling (or paid-plan budget accepted) - [ ] **Design fidelity:** tokens + component gallery approved by the user side-by-side against mockups (named artifact in the design-system phase), re-verified at every UI phase close - [ ] **Privacy:** no coordinates in any server log; opt-in telemetry only; opt-out exists and works; storage list ("what we store and why") present in the bot or in docs ## Recovery Strategies | Pitfall | Recovery Cost | Recovery Steps | |---------|---------------|----------------| | initData validation shipped broken | LOW (pre-launch) / HIGH (post-users) | Write validator + vectors; invalidate all sessions; rotate the bot token if a spoof already succeeded | | Silent geolocation failure discovered late | MEDIUM | Swap to LocationManager primary path (abstraction makes this cheap); ship denied-help screen; manual-origin fallback unblocks users meanwhile | | Recalc storm in production | LOW | Add debounce + cooldown (small fix); the real cost is data (possible upstream 429 bans) time to recover | | Search fails on real KL queries post-launch | MEDIUM | Alias index is additive, not architectural; add incrementally from real query logs (privacy-respecting aggregate logging only) | | Demo OSRM throttled/gone at launch | MEDIUM | Stand up self-hosted instance (a day, not a quarter — interface means a config swap) | | Design fidelity lost in the app | HIGH — hardest to recover | Rebuild screen-by-screen from tokens with side-by-side mockup verification; every defer costs compounded fidelity decay; precisely why the user rated this #1 | | Ops-stack scope creep mid-build | MEDIUM | Ruthlessly remove or defer; re-scope at true v1 verticals; sunk cost hurts once, ongoing maintenance hurts every week | ## Pitfall-to-Phase Mapping | Pitfall | Prevention Phase | Verification | |---------|------------------|--------------| | initData validation done right | P1/P2 (first backend artifact) | Tampered/stale payload tests in CI; curl-only requests rejected | | Geolocation silent WebView failure | P3 (shell + location slice) | Multi-client device matrix demo includes denied + no-fix paths before phase closes | | Snapping, recalc storms, ETA whiplash, battery | P5 (live guidance), building on route model in P4 | Recorded-drive replay passes: one reroute per off-route event; ETA stability; drain test within budget | | localStorage / viewport / back-button traps | P3 (Telegram-aware shell) | Session survives iOS restart; keyboard test; BackButton never closes the Mini App mid-navigation | | Nominatim limits + landmark weakness | P2 (search) | "KLCC" / "Sunway" / "Mid Valley" / alias queries pass; 1-rps budget enforced; zero 429s in logs | | OSRM demo dependency + missing traffic infrastructure | P4 (route provider) | No demo-OSRM reference in production config; route-provider interface with two impls testable; traffic weighting deferred with explicit trigger condition | | Malaysia OSM data quality | P4 (golden routes) × P5 (guidance restraint) | 10–20 KL golden routes pass with concise maneuvers; known-limitations doc committed | | Cloudflare budgets | P4 onward + budgets audit in P7 | Subrequest/CPU counts instrumented in dev replay; cache hit-rate visible in logs | | Design fidelity (Tailwind-CDN → real code) | P1 (tokens + gallery, user-approved) | Gallery-vs-mockup approval artifact per UI phase (P3–P6) | | Ops-stack scope creep | Milestone planning + every phase gate | Every shipped service maps to a shipped requirement; no v1 infra beyond Worker/KV (+ optional Postgres only when telemetry/reports ship) | ## Sources - Telegram official — Mini Apps / Web App platform docs (initData validation procedure, LocationManager Bot API 8.0+, BackButton Bot API 6.1+, `viewportStableHeight`, CloudStorage, disableVerticalSwipes/closing-confirmation): https://core.telegram.org/bots/webapps — HIGH - "Telegram Mini App (TMA) Development Mistakes and How to Avoid Them" (AdsGram blog, 2025/2026) — initData traps, localStorage wipe on iOS/desktop builds, custom-back-button vs native, viewport symptom table — MEDIUM (corroborated by official docs) - tma.js / @telegram-apps sustain issues and docs (location request patterns, viewport/css var binding, feature detection): https://github.com/Telegram-Mini-Apps/tma.js/issues/617 — MEDIUM - WebView geolocation silent-denial references: even-realities/EvenDemoApp issue #50 (error code 1 without prompt when WebView lacks permission bridge); Stack Overflow "android webview geolocation" thread — MEDIUM (widely-reported failure mode, matches developer reports) - Nominatim Usage Policy (official OSMF operations page; max 1 req/s, no heavy use): https://operations.osmfoundation.org/policies/nominatim/ — HIGH - OSM wiki "Addresses" (interpolation, tagging gaps); community threads on address/POI geocoding quality and abbreviations in OSM search — MEDIUM - OSRM official docs + demo-server policy references (leaflet-routing-machine docs' "**NOT SUITABLE FOR PRODUCTION USE**" OSRM-demo notice; traffic-update feature/datasource notes in OSRM API docs) — HIGH for policy, MEDIUM for profile nuances - Cloudflare Workers limits docs — subrequests (50 free / 10k+ paid), CPU limits, Pages/Workers free-plan request quotas: https://developers.cloudflare.com/workers/platform/limits/ — HIGH - Cloudflare Pages pricing docs — "static unmetered; functions metered" — HIGH - Project-internal: PROJECT.md (Telemetry Rams token list, Tailwind-CDN mockups, privacy constraints, ops appetite, hybrid-guidance decision) and user's stated #1-risk flag on design fidelity — HIGH - Turn-by-turn engineering traps (snapping, off-route hysteresis, ETA smoothing, adaptive sampling, drive-replay testing): synthesized from established navigation-engineering practice (routing engine vs navigation-layer separation), no single canonical source treat as MEDIUM/known-wisdom --- *Pitfalls research for: Telegram-first GPS navigation (araharah)* *Researched: 2026-10-22*