Mobility · Flutter (iOS & Android)

EV Bharat — EV Charging Mobile App

The map was only as good as the database behind it — so I replaced the database. A solo-built charging-station discovery app for Indian EV drivers: Flutter client, NestJS + PostgreSQL API, live Google Places discovery.

Shipped — signed release build (APK + AAB)

Overview

EV Bharat helps electric-vehicle drivers find a working charger nearby, plan a road trip around charging stops, and trust what the app tells them — in India, where infrastructure is growing fast but unevenly, so 'nearby' has to mean something in a mid-sized city, not just the metros.

The first build used a self-hosted PostgreSQL table of stations seeded from OpenStreetMap and served through a NestJS API with geospatial nearby/search queries. A coverage audit broke it: Jaipur — a city of three million — returned zero chargers within 50 km, against a dataset of 570+ seeded records. Delhi had 139, Bengaluru 53, Mumbai 8, Chennai 5, Hyderabad 2. The gap wasn't a bug, it was the ceiling of the approach.

I rebuilt the discovery layer on Google Places API (New), querying its EV-charging place type by GPS radius and free-text search. Jaipur alone came back with 10+ real stations — IndianOil, Statiq, Tata Power, Adani and independents. Reviews, favorites, issue reports and charging history all keyed off station IDs, so instead of rewriting every feature I kept one interface — getNearby / search / getById — and mapped the Places response (connector type, power rating, availability) onto the exact same station model. Only the data source underneath changed.

Search now geocodes a typed city, pins it and reloads nearby chargers around that point instead of device GPS. Journey planning turned the origin field from a read-only GPS point into an autocompleting text field, and samples chargers every 30 km along the route against the same live layer.

Auth runs on SMS one-time codes: cryptographically secure generation, each new code invalidating the last, ten-minute single-use expiry, and a development bypass hard-disabled outside non-production. A dedicated audit against the live backend found two real issues — any signed-in user could call the admin bulk-upload endpoint and overwrite the charger database (fixed with a constant-time admin-key guard independent of login), and rate limiting was keying off the reverse proxy's rotating address so bursts sailed through (rewritten to read the forwarded client IP, re-verified live: five requests pass, the sixth and seventh are rejected). Production now refuses to boot on missing or default JWT secrets, API docs are off in production unless explicitly enabled, and CORS is domain allow-listed.

Shipping meant a dedicated 4096-bit RSA release keystore instead of debug signing, R8/ProGuard shrinking and obfuscation with rules for the Flutter engine, Google Maps and Play deferred components, the SDK target Play Console currently requires, and both an installable APK and a Play-ready App Bundle from one signed configuration.

Scope

  • Product discovery & user journeys
  • Mobile UI/UX design
  • Flutter application development (iOS + Android)
  • NestJS + PostgreSQL backend API
  • Geospatial discovery layer migration to Google Places (New)
  • OTP authentication & JWT session handling
  • Live security audit and remediation
  • Release signing, shrinking and store-ready builds

Tech stack

Mobile

  • Flutter
  • Dart
  • Riverpod
  • GoRouter
  • Google Maps SDK

Backend

  • NestJS
  • TypeORM
  • PostgreSQL
  • JWT access + refresh

Infrastructure & integrations

  • Railway
  • Twilio SMS (OTP)
  • Google Places API (New)
  • Geocoding

Release

  • RSA-4096 keystore
  • R8 / ProGuard
  • Signed AAB + APK

Deliverables

  • Flutter mobile client for Android and iOS with map-based charger discovery
  • NestJS + PostgreSQL API for accounts, reviews, issue reports, favorites and charging history
  • Pluggable discovery layer (getNearby / search / getById) backed by Google Places API (New)
  • Geocoded place search that re-centres nearby results on the searched location
  • Journey planner with autocompleting origin and chargers sampled every 30 km along the route
  • SMS OTP authentication with secure code generation, single use and 10-minute expiry
  • Security hardening: admin-key guard, proxy-aware rate limiting, strict CORS, boot-time secret checks
  • Signed release keystore, obfuscated build config, and Play-ready App Bundle plus installable APK

Outcomes

Coverage gap found
0 chargers within 50 km of Jaipur (self-hosted dataset)
After the pivot
10+ real stations, same city, live data, same day
Auth throttle
5 / min OTP send + verify, keyed to the real client IP
Release signing
RSA-4096 dedicated keystore, Play Store–compliant

Frequently asked questions

Who built EV Bharat?
Karnveer Singh built it solo, end to end — Flutter mobile client, NestJS and PostgreSQL backend, infrastructure, security hardening and the signed release build.
Why move charger data from a self-hosted database to Google Places?
A coverage audit found zero chargers within 50 km of Jaipur in a 570-record self-hosted dataset, while Google Places returned 10+ real stations in the same city. A manually seeded table could never match live coverage, so the discovery layer was rebuilt on Google Places API (New) behind an unchanged station interface.
How did the migration avoid breaking reviews, favorites and history?
Those features keyed off station IDs. Rather than rewrite them, the app kept one discovery interface — getNearby, search and getById — and mapped the Places response, including connector type, power rating and availability, onto the existing station model.
What did the security audit find?
Two real issues: the admin bulk-upload endpoint only checked login, not role, so any signed-in user could overwrite the charger database; and rate limiting keyed off the reverse proxy's address, so it never fired. Both were fixed with a constant-time admin-key guard and a forwarded-IP limiter, then re-verified against the running backend.
What's next for the app?
Letting drivers favorite Google-sourced chargers, not just original-database ones, and adding refresh-token revocation so a compromised long-lived token can be killed instead of just expiring.