RideKit
Docs/Reference/Full codebase audit — 5 September 2026

Full codebase audit — 5 September 2026

928 files, ~180,000 lines: backend, admin, both mobile apps, sales site.

Run because four white-label bugs had just been found by eye, all of one class, all in files nobody had happened to open. That is not a method. The scanners behind this report live in tools/audit/ and are re-runnable.

264 raw hits → 6 real defects. The rest are documented false positives; the tooling README lists the known-good patterns so the next run is faster to triage.


Fixed

1. Any signed-in user could read any ride — 3 routes · security

GET /rides/:id, /rides/:id/timeline and /rides/:id/candidates sat behind JwtAuthGuard alone and read only the id from the URL. No handler looked at who was asking.

Exposed: both parties' user ids, pickup and dropoff addresses and coordinates, the fare, the full status history — and, on /candidates, the list of nearby drivers with their positions.

Reachable in practice, not in theory: a driver is handed the id of every ride they are offered, and declining does not forget it. A driver could decline a trip and then keep reading it, watching where that passenger was actually taken.

The proof it was an oversight rather than a design choice: the same candidatesFor() call is exposed on the dispatcher controller behind @RequirePermissions('ride.assign'). One controller guarded it; the other handed it to anyone.

Fixed with RidesService.getForParty() — rider, driver, or an operator holding ride.view_all. /candidates now requires the same permission its twin always did (no client ever called this copy). 13 regression tests in ride-access.spec.ts, including one asserting no @Get(':id…') handler reads an id without also reading the viewer — the check that would have caught all three.

A sweep of all 166 id-bearing routes across 37 controllers found no others.

2. Two dispatch timers with no rejection boundary — latent crash

setTimeout(() => { this.onTimeout(rideId, driverId); }, OFFER_TTL_MS);
setTimeout(() => { void this.runAutoAccept(...); }, delayMs);

onTimeout is async and reaches the database. void marks a promise as deliberately unawaited; it does not handle a rejection. Neither callback had a .catch(), and the process had no unhandledRejection handler — where Node's default is to terminate.

Honest severity: not a live crash. Every helper down that tree currently guards itself, so nothing is known to reject today. But the safety of the busiest path in the product rested on all ~15 of them continuing to do so forever, with a timer firing every 15 seconds of every dispatch to collect on the first one that stopped.

Fixed: explicit .catch() + logger.error on both callbacks, plus a process-level unhandledRejection net in main.ts that logs loudly and reports to Sentry instead of ending the process. uncaughtException deliberately left alone — a synchronous throw really can leave state inconsistent, and exiting is the safer answer there.

3. Every phone field in the admin showed an Indian number

placeholder="+919800000000" in Drivers, Riders, Team and Settings — on a product sold into 37 markets. An operator in Riyadh was shown an Indian number as the format to follow.

The preset pack has carried dial and phonePlaceholder per country all along; nothing served them. Fixed: both now come through /branding and useInstallation().phoneExample.

4. The browser tab still said "RideKit"

app/layout.tsx exports a static metadata title, resolved at build time — so every buyer's admin announced itself in the tab, the bookmark and browser history as RideKit — Admin. The one piece of chrome an operator looks at all day.

Fixed: corrected client-side once /branding answers; the static export stays as the pre-hydration fallback.

5–6. Two white-label leaks in the driver app

  • The background-location foreground-service notification used getCachedBrandName() on one line and a hardcoded #4f46e5 on the next — a buyer's driver ran an entire trip with our indigo in their status bar.
  • The notification channel LED colour was literal, while the rider app derived it. The two apps had drifted.

Both were caused by the driver app mirroring only brandName for non-hook callers and never the colour. Fixed by remembering them together through one rememberBrand(), so a third cannot appear.


Found, not fixed — needs your decision

A. A failed request renders as "you have no data" — ~80 sites

.catch(() => setItems([])) across both mobile apps. A network failure becomes an empty list, indistinguishable from genuinely having nothing. The rider sees "No saved places" when their addresses are fine, or an empty chat thread when the driver did write.

This is a known-real class here — the admin Providers screen had exactly it, and the code comment there calls it "the most expensive lie on this screen".

Not fixed because it is not a find-and-replace: each screen needs an error state and a retry, which is a design decision per screen. Roughly 20 of the 80 are harmful (lists a user would believe); the rest are decorative fetches where empty is honest. Worth scoping as its own piece of work.

site/src/app/legal/terms/page.tsx renders TODO(owner): legal entity and TODO: confirm to visitors. Not a code bug — it needs your legal entity name and a decision on support scope.

C. Ride-category tile colours

countries.ts gives Ride/Bike/Auto three fixed hues, the first of which is our teal. They read as category colours rather than brand, but a red-branded install shows a teal "Ride" tile. Your locked design direction, so not mine to override.


Checked and clean

Result
Secrets in source None. 3 hits, all test fixtures
Money as float 0 real defects in 19 hits — all display formatting, documented rounding, or safe conversion. Verified across 100M values that Number(paise)/100 always stringifies to a clean ≤2dp decimal, so GST e-invoices are exact
IDOR beyond /rides None across 166 id-bearing routes
Public controllers 2, both correct: /health and the Play-Store-mandated account-deletion form
Operator route permissions All guarded, most at class level

Verification

Backend 1180 · admin 355 · rider 456 · driver 237 — 2,228 tests, four clean typechecks. 13 of those tests are new and pin the security fix.

source: docs/AUDIT-2026-09-05.md (ships identically in the product zip)