Files
2026-10-23 22:37:55 +08:00

42 KiB
Raw Permalink Blame History

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": <anyone>} 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