Files
Xpress-rider/docs/PORTFOLIO.md
2026-08-12 10:50:45 +05:30

621 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Nearle Rider (`com.nearle.partner`) — Portfolio Technical Summary
A production Android delivery-rider app for a last-mile logistics operation in
Coimbatore, India. ~30,000 lines of Dart plus native Kotlin. It's the app a
delivery rider has open on the handlebar for an entire shift.
There are really **six** distinct systems in here, not one app. Taken in order of
engineering weight.
---
## 1. The GPS Pipeline — Trusted Distance Measurement
### What it is
Riders are paid by the kilometre. The formula lives in `deliveries_controller.dart`:
```
ridercharges = distance_travelled_km × fuelcharge_rate
```
That single line is why this subsystem exists. Consumer phone GPS is noisy: it
drifts several metres while stationary, jumps hundreds of metres when switching
between cell-tower and satellite fixes, degrades under power-saving mode, and can
be trivially faked with a mock-location app. Every one of those failure modes is
*money* — noise inflates payouts, jumps inflate payouts spectacularly, and
spoofing is outright fraud.
So the problem isn't "track the rider." It's: **produce a distance number that is
defensible enough to pay against, from a sensor that lies.**
### How it's built
```
Android GPS (bestForNavigation, distanceFilter: 0)
│ raw Position stream, ~1 Hz
▼
┌──────────────────────────────────────────┐
│ Gate 1: isMocked? → drop │ anti-fraud
│ Gate 2: accuracy > 50 m? → drop │ quality
│ Gate 3: <3 s since last? → drop │ rate limit
│ Gate 4: _isProcessing? → drop │ re-entrancy mutex
└──────────────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 4-D Kalman filter │
│ state = [lat, lng, v_lat, v_lng] │
│ predict(dt) → update(measurement) │
└──────────────────────────────────────────┘
│ smoothed (lat, lng)
├──────────────────────────────► MQTT nearle/riders/{id}/location
│ (QoS 0 — fire and forget)
▼
┌──────────────────────────────────────────┐
│ Per-delivery odometer, in SharedPrefs │
│ Haversine(anchor → now) │
│ Gate 5: implied speed > 120 km/h │
│ AND jump > 200 m → move anchor,│
│ DON'T count the metres │
│ Gate 6: delta < 5 m → skip, DON'T move │
│ anchor (noise floor) │
│ else: cumulativeKm += delta │
└──────────────────────────────────────────┘
```
### The approach, step by step
**The Kalman filter** (`lib/utils/kalman_filter.dart`) is hand-implemented — no
linear-algebra dependency, just nested `List<List<double>>` and explicitly
unrolled matrix helpers (`_multiply2x4_4x2`, `_inverse2x2`, and so on). State is
4-dimensional: position *and* velocity in each axis. The measurement matrix `H`
is 2×4 because GPS only observes position; velocity is inferred from how position
evolves. Measurement noise `R` is set to `1e-5` degrees² (~1 m), encoding "GPS is
accurate to roughly 5–10 m." `dt` is fed in live from wall-clock deltas, so the
filter behaves correctly whether samples arrive 3 seconds apart (foreground) or 30
seconds apart (background).
The payload sends **both** the filtered and the raw coordinates (`lat`/`lng`
alongside `raw_lat`/`raw_lng`). That's a good call: dispatch sees the smooth
track, but the unfiltered truth is preserved server-side for audit or for
re-deriving with different parameters later.
**The two gates are asymmetric on purpose, and this is the cleverest bit.** When
GPS jumps implausibly far, the code moves the anchor point but does *not* add the
distance — the phantom kilometres are discarded. When movement is *below* 5 m, it
does the opposite: it skips *without* moving the anchor. If it moved the anchor on
tiny deltas, a phone sitting still would accumulate GPS jitter forever and
silently print money. Getting both directions right requires actually reasoning
about the failure mode rather than just clamping values.
The jump threshold is also velocity-based rather than fixed:
`max_realistic = elapsed_seconds × 33.3 m/s` (120 km/h). A 200 m step in 3 seconds
is fraud; the same 200 m step across a 60-second background interval is a normal
motorcycle. A fixed distance threshold would have to choose one and be wrong for
the other.
### The tricky case: two isolates, one odometer
Flutter's foreground service runs in a **separate Dart isolate** with its own
memory. So there are two independent copies of this pipeline —
`LiveTrackingService` (main isolate, 3 s cadence) and `_BackgroundRiderLog`
(service isolate, 30 s cadence) — and both want to increment the same odometer.
Naively, every metre gets counted twice and every rider gets paid double.
The coordination is a **heartbeat in shared storage**:
```
main isolate ──writes──► live_tracking_last_update_ms ──reads──► service isolate
│
if (now − heartbeat < 10 s) │
→ main is alive, stand down
else │
→ main is dormant, take over
```
The main isolate stamps a timestamp on every update. The background isolate reads
it and accumulates *only* if the stamp is stale by 10 seconds — meaning the app is
backgrounded, screen-off, or the OS has frozen the main isolate. Handover in both
directions is automatic and requires no signalling. Since `SharedPreferences`
caches in memory per isolate, the background side calls `prefs.reload()` before
reading to force a disk re-read.
*This is a lease/lock pattern implemented over the only shared medium two Dart
isolates reliably have.* It's the single most non-obvious piece of engineering in
the codebase.
### Trade-offs
- **`SharedPreferences` as the IPC channel.** It's key–value storage, not a
database — no transactions, no atomic increments. Two writers to the same key
could theoretically interleave. The 10-second lease makes that window vanishingly
small in practice, and it buys zero extra dependencies plus survival across
process death. A proper answer would be SQLite with a write-ahead log; this is
the pragmatic one.
- **Cumulative distance beats point-to-point.** At delivery confirmation the code
prefers the tracked odometer over any recomputed origin→destination distance,
precisely because the odometer reflects the path actually ridden — detours,
one-ways, wrong turns and all.
- Reading the code honestly: the process-noise matrix `Q` has entries at `[2][0]`
and `[3][1]` that make it non-symmetric, which is unusual for a covariance
matrix. It doesn't destabilise the filter at these magnitudes, but it looks like
a transcription slip rather than a deliberate choice.
---
## 2. The Maps & Routing Migration — Removing Google Entirely
### What it is
The app originally used Google Maps SDK for rendering and the Google Directions
API for road routes. It was billing painfully. The rebuild replaced the entire
stack with open-source components — **no API key of any kind ships in the app
anymore.**
### The diagnosis (this is the interesting part)
The obvious assumption — "map rendering is expensive" — is wrong. Google's mobile
Maps *SDK* is free and unlimited. The bill came from **Directions**, at $5 per
1,000 calls above a 10,000/month free tier. Two things amplified it:
1. The multi-stop screen issued **one Directions request per consecutive pair of
stops**. Opening a 6-stop route cost 5 billable requests — *every time the
screen opened*, and riders open it constantly.
2. The API key was hardcoded in five places including `AndroidManifest.xml`.
Because `flutter_polyline_points` called the Directions **web service** from the
device, the key could not carry an Android-app restriction — Google only
supports that for SDK calls. Anyone who unzipped the APK had an unrestricted,
billable key.
So the fix that actually saves money is replacing *routing*, not rendering. That
reframing is what made the migration tractable.
### The replacement stack
| Concern | Was | Now | Cost |
| --- | --- | --- | --- |
| Rendering | Google Maps SDK | **MapLibre GL** (BSD) | free |
| Tiles | Google | **OpenFreeMap** (OSM data, MIT) | free, keyless, no limits |
| Routing | Directions API | **OSRM** (BSD, self-hostable) | free |
| Turn-by-turn | — | OS intent handoff (`google.navigation:`) | free — it's an intent, not an API |
### Architecture
```
4 map screens (map, multi_map, nav, homepage)
│
┌─────────────────┴─────────────────┐
▼ ▼
NearleMap widget RoutingService
(declarative markers/lines) (OSRM client + cache)
│ │
├── MapMarkerIcons ──┐ │
│ (Canvas → PNG) │ │
▼ ▼ ▼
maplibre_gl MapConfig ──── endpoint URLs
│ (String.fromEnvironment)
▼ ▼
OpenFreeMap tiles OSRM /route/v1/driving/…
```
The provider choice is confined to **two files** (`map_config.dart`,
`routing_service.dart`). No UI file names a vendor. Swapping backends again
wouldn't touch a screen.
### How each hard part was solved
**One call, not N.** OSRM accepts every stop as a coordinate in a single request
path: `lon,lat;lon,lat;lon,lat`. The multi-stop screen now passes intermediate
stops as `waypoints` in one call. N requests → 1.
**Response caching.** `RoutingService` keys its cache on coordinates rounded to 4
decimal places (~11 m), with a 30-minute TTL and a 120-entry cap evicted
oldest-first. Repeated quotes for the same delivery reuse one response.
**A deliberately tolerant parser.** `_parse()` accepts distances as plain numbers
*or* as `{"value": n}` objects, falls back from route-level totals to summing legs,
and hunts for geometry under `geometry` / `overview_polyline` / `polyline`, nested
`{"points": …}`, GeoJSON `coordinates`, and finally by stitching per-step
geometries. This is defensive work that lets a proxy, a self-hosted build, or an
OSRM-compatible alternative (Valhalla, GraphHopper) drop in without a code change.
**The polyline codec.** Both Google and OSRM encode geometry using Google's
Encoded Polyline Algorithm — a variable-length, zigzag-encoded, base-64-offset
delta format. It's implemented from scratch (`decodePolyline`) with the precision
parameterised, because OSRM's default `geometries=polyline` is precision 5 while
its `polyline6` option is precision 6. Get that wrong and routes render as a tiny
smudge near the equator.
**The lon,lat trap.** OSRM takes coordinates as `lon,lat` in the URL path —
reversed from nearly every other geo API. Swapping them silently returns routes in
the wrong hemisphere rather than erroring. The smoke test asserts route endpoints
land within 0.02° of the requested points specifically to catch this.
**Markers had to be redrawn from nothing.** MapLibre has no `BitmapDescriptor`
equivalent. `map_marker_icons.dart` draws teardrop pins, rider dots, and numbered
step circles directly onto a `dart:ui` Canvas and rasterises them to PNG bytes,
cached by a composite key so identical icons register once.
**Diffing instead of clear-and-re-add.** On the navigation screen the rider marker
moves every few metres. Tearing down and re-adding every annotation would flicker
visibly. `_NearleMapState` keeps `Map<String, Symbol>` / `Map<String, Line>`
registries and calls `updateSymbol`/`updateLine` on changed items only. Two
supporting details make this work:
- `NearleMapMarker`'s `==` **deliberately excludes `onTap`**. A new closure on
every rebuild would otherwise mark every marker dirty. Taps are instead resolved
by looking up the ID against the *current* widget's marker list, so the handler
still closes over fresh state.
- Native annotation calls aren't re-entrant, so `_syncing`/`_syncQueued` flags
coalesce overlapping syncs into one trailing run.
**Degenerate camera bounds.** Fitting the camera to a single point (or two points
on an exact horizontal line) makes MapLibre's native fit routine fail. `fitBounds`
nudges any span below `0.0015°` (~165 m) outward around its midpoint first.
### Trade-offs accepted
- **No info windows.** MapLibre has no equivalent. Marker taps still fire and
drive the customer sheet; tap-to-show-title bubbles are gone.
- **No traffic layer, no 3D buildings.** No MapLibre equivalent, and OSRM has no
live-traffic model either — ETAs are free-flow estimates.
- **The public FOSSGIS OSRM server is the default.** It's donation-funded with a
fair-use policy a delivery fleet would abuse. `deploy/osrm/` ships a Docker
Compose stack and preprocessing guide for self-hosting; the whole southern-India
extract is only 0.52 GB. The endpoint is overridable via
`--dart-define=OSRM_BASE_URL=…` with no code change.
### Verification
The migration was checked at the **artifact** level, not the build-log level — the
release AAB was unzipped and inspected: `libmaplibre.so` present for 3 ABIs,
`com/google/android/gms/maps` references **0**, `org/maplibre` references 341.
`test/maps_smoke_test.dart` runs 8 tests including live hits against both
endpoints, validating the polyline decoder against Google's canonical published
example and asserting no `api_key=` appears in the fetched style document.
---
## 3. Delivery Lifecycle & Compensation Engine
### What it is
The state machine a delivery moves through, and the money that falls out of it.
```
accepted → active → arrived → picked → delivered
│ │ │
├─ rejected └─ (or skipped / cancelled)
```
Each transition is a method on `DeliveriesController` and each does far more than
flip a status.
### The delivery-confirmation path
`updateDeliveredStatus()` is ~400 lines and reads like a checklist of everything
that can go wrong in the field:
**Step 1 — Geofence gate.** Compute Haversine distance from the rider to the drop
point. If it exceeds a server-configured radius, show the exact distance and
required radius, and **refuse the update**. This is the anti-fraud gate that stops
"delivered" being tapped from home.
**Step 2 — Distance resolution, four-tier cascade.** This is a
graceful-degradation ladder, best source first:
1. Cumulative GPS odometer (§1) — the actual path ridden
2. Caller-supplied `actualKms`
3. OSRM road distance from a recorded origin — tried against the saved start
location, then the provided pickup, then the cached pickup, then the drop point
4. Straight-line Haversine — if routing is unreachable
Then a floor: if everything failed, use 0.01 km. Never zero, never null. The
reasoning is visible in the code — a null propagating into `ridercharges` means an
unpaid delivery and an angry rider, which is worse than a slightly wrong number.
**Step 3 — Bonus points.** An ETA deadline is stamped at `eta_endtime_{orderId}`
when the delivery goes active. Delivered at or before it → bonus points equal to
`round(distance_km)`.
**Step 4 — Skip penalty.** A 3-hour rolling window tracks skip count. If the rider
is over the limit, bonus points are **forfeited to zero** — the base per-km pay is
untouched. Penalising the incentive rather than the wage is a deliberate, and
fairly humane, design choice.
**Step 5 — Coordinate validation.** Reject `0`, reject `|lat| > 90`, reject
`|lng| > 180`, and abort the whole update rather than post garbage. Compare with
the geofence check, which *fails open* on missing coordinates (warn and proceed) —
the two policies are opposite, and correctly so. A geofence failure shouldn't
strand a rider who genuinely delivered; a malformed coordinate write would corrupt
the record permanently.
**Step 6 — Money.** `ridercharges = km × fuelcharge`, rounded to 2 decimals to
match a `DECIMAL(10,2)` column server-side.
### Route sequencing
Stop ordering (`step`) is assigned **server-side** — the app doesn't solve a
travelling-salesman problem. What it does handle is *display* sequencing:
`preservedStepNumbers` keeps numbering stable as deliveries complete or get
skipped, so a stop the rider has been calling "number 3" doesn't silently renumber
itself mid-shift. Orders arriving without a step get synthesised display numbers
appended after the numbered ones. Small feature, real usability value.
---
## 4. Background Execution & Shift Automation
### What it is
Android aggressively kills background work — Doze mode, app standby, and
OEM-specific battery managers (particularly aggressive on the Chinese-brand
handsets common in this market). But a rider's shift must be logged, and their
shift must end on time, whether or not the app is alive.
This system is a **layered survival strategy**: each layer covers the failure of
the one above.
```
Layer 1 — App foregrounded
LiveTrackingService, 3 s cadence, full fidelity
Layer 2 — App backgrounded
flutter_foreground_task, 30 s repeat, persistent notification,
foregroundServiceType="dataSync|location", wake + WiFi locks
Layer 3 — App process killed
Android AlarmManager, setExactAndAllowWhileIdle (pierces Doze)
→ ShiftEndReceiver.kt, pure Kotlin, no Flutter engine
→ reads Flutter's SharedPreferences directly ("flutter." prefix)
→ raw HttpURLConnection POST, under a 60 s PARTIAL_WAKE_LOCK
Layer 4 — Phone rebooted / alarm lost
On next app launch, main.dart replays the shift-end check
and creates the break log retroactively
```
Layer 3 is the interesting one. The receiver deliberately **does not** boot the
Flutter engine — starting a Dart VM from a broadcast receiver is slow and
unreliable. Instead it reaches into `FlutterSharedPreferences` (Flutter prefixes
every key with `flutter.`, which the Kotlin reads verbatim), hand-builds the JSON
payload as a string, and POSTs it with `HttpURLConnection`. The break log is
created correctly even from a cold-killed app.
### Overnight shift arithmetic
Shifts are stored as bare `HH:mm:ss` strings with no date, which makes "has the
shift ended?" genuinely ambiguous. The logic compares start and end as fractional
hours; if `start > end`, the shift crosses midnight, and "over" means *within the
gap between end and start*, not simply "after end":
```
22:00 start, 06:00 end
now 23:00 → after start → ACTIVE
now 05:00 → before end → ACTIVE (yesterday's shift)
now 10:00 → after end AND before start → OVER
```
That check is implemented three times — Dart foreground, Dart background isolate,
and Kotlin — because each layer may be the only one running. Triplicated logic is a
maintenance liability, and it's the right call anyway: a single shared
implementation isn't reachable from a receiver that can't start Dart.
---
## 5. Offline Resilience
Riders lose signal in basements, lifts, and rural stretches constantly. Every
write path has a queue behind it.
```
POST attempt (8–10 s timeout)
│
success ──► persist "last known good" anchor, done
│
failure ──► append {orderId, payload, timestamp} to a
SharedPreferences string-list queue
│
▼
on next successful cycle / app start / reconnect:
drain queue, retry each, keep failures, rewrite remainder
```
Two independent queues exist — `offline_delivery_logs` and `offline_rider_logs` —
each with an `_isFlushing` re-entrancy guard so overlapping timers can't
double-post. Requests carry explicit timeouts throughout, so a hung socket
degrades to a queued write instead of freezing a UI thread.
Payloads are persisted per order (`delivery_payload_{orderId}`) so a delivery in
progress survives a process kill and resumes with the same identity rather than
starting a fresh log.
---
## 6. Real-Time Telemetry Bus (MQTT)
Location, status, and device health stream to dispatch over MQTT rather than HTTP
polling.
```
nearle/riders/{riderId}/status retained + Last Will (QoS 1)
nearle/riders/{riderId}/location QoS 0, ~every 3 s
nearle/riders/{riderId}/profile retained (QoS 1)
nearle/riders/{riderId}/telemetry every 5 min (QoS 1)
nearle/riders/{riderId}/logs/{event} (QoS 1)
nearle/riders/{riderId}/alerts (QoS 1)
```
The QoS choices are thought through, not defaults. Location is **QoS 0** — a
dropped fix is replaced by a fresher one 3 seconds later, so guaranteed delivery
would be pure overhead. Status is **QoS 1 and retained**, so a dispatcher opening
the console sees current state immediately rather than waiting for the next
publish.
**Last Will and Testament** is the sharp detail: the client registers an `Offline`
message with the broker at connect time. If the rider's phone dies, loses signal,
or force-quits, the *broker* publishes that message on their behalf. Dispatch
learns the rider is gone without any cooperation from the dead client.
Client IDs are salted per isolate startup (`rider_{id}_{base36 timestamp}`) —
otherwise the main and background isolates would connect with identical IDs and
MQTT brokers evict the older session, causing the two to kick each other offline in
a loop.
Alerts fire on device conditions dispatch actually cares about: location services
disabled, battery under 15% and not charging, GPS accuracy worse than 30 m.
---
## Supporting Features
Smaller but real:
- **Auth** — phone precheck → OTP (with `sms_autofill`) → MPIN, with server-side
PIN verification and session refresh
- **Picture-in-Picture** — native Android PiP via `MethodChannel`, so an ETA
countdown stays visible while the rider is in Google Maps
- **Proximity alerts** — within 50 m of the drop, fire a local notification plus a
bundled audio clip, falling back to TTS if playback fails. A per-delivery flag
ensures once only
- **Proof-of-delivery upload** — camera capture → DigitalOcean Spaces
(S3-compatible, via the `minio` client) → CDN URL returned to the API
- **Push** — FCM with per-rider selectable alert sounds; the Android notification
channel is *recreated* under a new ID when the sound changes, because channel
settings are immutable after creation
- **Summary & rewards** — `fl_chart` dashboards, weekly km, scratch-card rewards
(`scratcher` + `confetti`)
- **Support tickets**, FAQ, saved addresses, forced-update gate against the Play
Store version
---
## The Stack
**Language / runtime**
- Dart (SDK ^3.9.2) · Flutter · Kotlin (native Android layer)
**State & architecture**
- GetX (`get`) — DI, reactive state, routing · Provider · MVC-ish split:
`views/` → `controllers/` → `providers/` → `Models/`
**Geo**
- `maplibre_gl` (rendering) · OpenFreeMap (OSM vector tiles) · OSRM (routing) ·
`geolocator` · `geocoding` · custom Kalman filter · custom polyline codec
**Background & lifecycle**
- `flutter_foreground_task` · Android AlarmManager (native) · `wakelock_plus` ·
`battery_plus` · `connectivity_plus`
**Networking & messaging**
- `http` · `mqtt_client` · Firebase Core + Messaging (FCM) · `minio`
(S3-compatible object storage)
**Storage**
- `shared_preferences` (state, queues, cross-isolate coordination) ·
`path_provider` · DigitalOcean Spaces + CDN (media)
**UI**
- `flutter_screenutil` · `fl_chart` · `lottie` · `shimmer` · `confetti` ·
`scratcher` · `flutter_slidable` · `webview_flutter` · Proxima Nova
**Device**
- `permission_handler` · `image_picker` · `flutter_tts` · `audioplayers` ·
`vibration` · `sms_autofill` · `url_launcher` · `floating` (PiP)
**Build**
- Gradle (Groovy) · compileSdk 36 · NDK 28.1.13356709 · R8 minify + resource
shrink · arm64-v8a only · custom release-artifact naming task
**Backend (consumed, not in this repo)**
- REST across two hosts — a primary API and a separate write/queue host · MQTT
broker on a VPS · versioned endpoints (v1/v2/v3) · `dev`/`live` switched by one
constant
---
## What's Genuinely Hard Here
Ranked by how much real thought each needed.
**1. Cross-isolate odometer coordination.** Two Dart isolates with no shared
memory, both holding a GPS stream, both able to increment the same paid counter,
either able to be frozen by the OS without notice. The heartbeat-lease over
`SharedPreferences` solves it in about 15 lines — but arriving at that solution
means understanding Flutter's isolate model, Android's process lifecycle, and why
the obvious fixes (a flag, a mutex, a port) all fail across a process boundary.
**2. Trustworthy distance from an untrustworthy sensor.** Not "apply a Kalman
filter" — the filter is the easy half. The hard half is the *asymmetry*: discard
distance but move the anchor on a jump; discard the sample but hold the anchor on
jitter. Both directions have to be right or the odometer drifts in one direction,
and that direction is money. The velocity-scaled jump threshold, so the same guard
works at 3 s and 60 s cadences, is the same kind of thinking.
**3. Correctly diagnosing the Maps bill.** The valuable engineering was the
analysis, not the code. "Maps SDK is free; Directions is billed; the multi-stop
screen was making N calls per screen-open; the key couldn't be restricted because
the calls were web-service calls" — that chain of reasoning is what turned an
unbounded vendor migration into a well-scoped one that landed. Plenty of teams
would have swapped rendering engines and wondered why the bill barely moved.
**4. Four-layer background survival.** Anyone can start a foreground service.
Making shift-end fire from a *killed process* means: knowing
`setExactAndAllowWhileIdle` is the only AlarmManager call that pierces Doze;
knowing a broadcast receiver shouldn't boot a Flutter engine; knowing Flutter's
`SharedPreferences` keys carry a `flutter.` prefix readable from Kotlin; and
knowing you need a `PARTIAL_WAKE_LOCK` or the device sleeps mid-HTTP-request. Each
of those is a scar.
**5. Flicker-free declarative annotations over an imperative native map.**
MapLibre's controller is imperative — `addSymbol`, `updateSymbol`, `removeSymbol`
— and non-re-entrant. Wrapping that in a declarative Flutter widget requires a
keyed diff, sync coalescing, and an equality contract that excludes callbacks. The
`onTap`-excluded `==` is a subtle, correct call that most implementations get wrong
and then blame on "MapLibre being flickery."
**6. Overnight shift arithmetic.** Deceptively small. Bare `HH:mm:ss` with no date
makes "is the shift over?" ambiguous, and the answer inverts for shifts crossing
midnight. Getting it wrong means riders auto-clocked-out at 10 PM mid-delivery, or
never clocked out at all.
**7. Graceful degradation as a design stance.** The four-tier distance cascade,
the four-tier start-time fallback, the fail-open geofence next to the fail-closed
coordinate validator — these aren't defensive-programming reflexes. Someone
decided, per code path, *which* wrong answer is least harmful, and the policies
differ where they should. That's the mark of software written by someone who had to
answer for its failures in the field.
---
## Security & Sensitivity Flags
**⚠️ Credentials in source — do not include any of this in a portfolio, and rotate
before publishing the repo.** All read directly, not inferred:
| Location | Issue |
| --- | --- |
| `lib/controllers/deliveries_controller.dart:75-76` | DigitalOcean Spaces **access key + secret key** hardcoded. These ship inside the APK and are recoverable with `strings`. They grant object-storage writes. |
| `lib/views/helpers/constants/mqtt_constants.dart` | Broker **IP, username, and password** hardcoded. Port 1883 is **unencrypted MQTT** — credentials and every rider's live location cross the network in plaintext. |
| `lib/helpers/http_overrides.dart` | `badCertificateCallback` returns `true` unconditionally, installed globally in `main()` **and** in the background isolate. This disables TLS certificate validation app-wide, making every API call MITM-able. |
| Repo root & `android/` | `nearlerider-keystore.jks`, `release-key.jks`, `android/key.properties`, `upload_certificate.pem` are **tracked in git**. The signing keystore plus its password file is the set that lets someone publish as you. |
| git history | The previous Google Maps API key remains in history and in every already-shipped APK. It needs revoking in Google Cloud Console regardless of the migration. |
**Commercially sensitive — describe generically, don't quote:**
- The compensation formula and its rate key, bonus-point rules, and the 3-hour
skip-penalty window
- Geofence radius policy
- Backend host topology and endpoint paths (two distinct hosts, versioned APIs)
- MQTT topic schema and broker address
**Inference vs. reading.** Everything above is read from source except: the backend
implementation (only its HTTP surface is visible from here), the server-side `step`
assignment algorithm (the app consumes it, doesn't compute it), and the actual
production billing figures for the old Google setup (the pricing model is
documented, the invoice isn't in the repo).
**Also worth knowing for accuracy:** the app is Android-only in practice —
`abiFilters "arm64-v8a"`, and the iOS directory has no Podfile. The root
`README.md` is still the unmodified Flutter template.