Plan a confident EV road trip across India — real charging-stop simulation against your vehicle's actual battery profile, India-focused charger discovery from three merged data sources, live traffic-aware routing, and an AI Copilot that curates places to stop along the way.
Built as a full-stack portfolio project on free tiers and open data only — no paid APIs, anywhere.
- Product Showcase
- Features
- Tech Stack & Data Sources
- Architecture
- Project Structure
- Getting Started
- Environment Variables
- Deployment
- Notable Engineering Decisions
- License
- Turn-by-turn routing via OpenRouteService, rendered as a live polyline on a MapLibre GL JS map
- Real battery/range simulation — real-world efficiency derived from claimed range, adjusted per-segment for elevation gain, a seasonal heat heuristic, and highway-vs-city road type
- Automatic charging-stop planning — inserts charging stops along the route based on the selected vehicle's actual battery profile and the driver's start-charge / minimum-arrival-charge preferences
- Alternative routes — requests a genuinely distinct second route where one exists, so the driver isn't silently stuck with whichever route the API picks first
- Manual waypoints — add intermediate stops to route through, same way Google/Apple Maps let you add a stop
- Live traffic overlay via TomTom, degrades gracefully if unavailable
- Three merged data sources — e-AMRIT (Government of India / NITI Aayog public dataset), Open Charge Map, and OpenStreetMap Overpass — deduplicated by proximity + name similarity, since no single free source has adequate India coverage alone
- Nearby-search and along-route charger queries
- Gemini-powered curation of attractions, food, lodging, and amenities anchored to the actual route and charging stops, not arbitrary route-position windows
- Wikipedia-enriched descriptions and images for points of interest
- Preference-aware scoring (interests, dietary needs) with a tiered "curated, not chaotic" display cap
- A no-LLM degraded fallback path so the feature never hard-fails if the AI call is unavailable
- ~38 India-market EV models/trims with a
verifiedflag per entry — premium/import figures are explicitly flagged pending manual OEM-spec cross-check rather than silently presented as certain
- GSAP-driven landing page: scroll-triggered hero scenes (intro → vehicle → trip → charging → destination) built around a cockpit/instrument-cluster visual motif
- Interactive 3D charger-coverage globe (
react-globe.gl+ Three.js) - Product-preview sections reuse real dashboard components with real data — no hand-drawn mockups
- Fully responsive: dedicated mobile drawer/bottom-sheet patterns for map controls, not just a squeezed desktop layout
- Session-scoped trip persistence — a refresh restores your in-progress trip, but closing the tab clears it (no stale trips resurfacing days later, no cross-tab overwrite conflicts)
- App-wide error boundary + custom 404 page
- Per-endpoint rate limiting tuned to each free-tier API's actual quota (protects the shared Gemini/ORS quota from being burned by one script)
- Helmet security headers, strict CORS allowlist, graceful SIGTERM/SIGINT shutdown for clean redeploys
Frontend
| Framework | React 18 (Create React App) + React Router 6 |
| Mapping | MapLibre GL JS |
| Animation | GSAP + @gsap/react |
| 3D | react-globe.gl + Three.js |
| Styling | Tailwind CSS |
| Icons | Phosphor Icons |
Backend
| Runtime | Node.js + Express 5 |
| AI | @google/genai (Gemini) |
| Security | Helmet, express-rate-limit, CORS |
External APIs & Data — all free tier, verified zero-cost
| Service | Used for |
|---|---|
| OpenRouteService | Turn-by-turn routing & directions |
| Open Charge Map | EV charger data |
| e-AMRIT (NITI Aayog) | Government of India public charging station dataset |
| OpenStreetMap Overpass | Charger data + points of interest |
| Mappls (MapmyIndia) | India-focused geocoding/autosuggest |
| TomTom | Live traffic |
| Google Gemini | AI Trip Copilot |
| Wikipedia | Point-of-interest enrichment (descriptions/images) |
The frontend never talks to OpenRouteService, Open Charge Map, TomTom, Mappls, or Gemini directly — every external call is proxied through the backend, which is the only place any API key ever lives.
flowchart LR
A[React Frontend] -->|REST| B[Express Backend]
B --> C[OpenRouteService<br/>routing]
B --> D[Open Charge Map]
B --> E[e-AMRIT<br/>Govt. of India]
B --> F[OSM Overpass]
B --> G[Mappls<br/>geocoding]
B --> H[TomTom<br/>traffic]
B --> I[Google Gemini<br/>AI Copilot]
B --> J[Wikipedia]
See docs/BACKEND_IMPROVEMENT_PLAN.md for the full backend design rationale.
Voltpilot/
├── frontend/ React app (Create React App)
│ ├── src/
│ │ ├── components/ Shared UI primitives + landing-page sections
│ │ ├── features/
│ │ │ ├── dashboard/ AI Copilot panel, analytics, battery chart
│ │ │ ├── map/ MapLibre canvas, route layer, map controls
│ │ │ ├── planner/ Trip planner state + location fields
│ │ │ └── vehicles/ Vehicle showroom, brand grid, specs
│ │ ├── pages/ DashboardPage, LandingPage, NotFoundPage
│ │ ├── data/ Vehicle database (vehicles.json)
│ │ ├── utils/ Battery simulation, trip persistence
│ │ └── assets/ Optimized images, fonts, icons
│ └── public/
│
├── backend/ Express API
│ ├── routes/ route, chargers, geocode, copilot, traffic
│ ├── services/ Charger aggregation, AI trip planning,
│ │ Wikipedia enrichment, POI clustering
│ └── middleware/ Rate limiters
│
├── docs/ Project documentation
└── render.yaml Backend deployment config (Render)
cd backend
npm install
cp .env.example .env # then fill in real values, see below
npm start # http://localhost:5000cd frontend
npm install
npm start # http://localhost:3000backend/.env
| Variable | Required? | Notes |
|---|---|---|
ORS_API_KEY |
Yes | Free key from openrouteservice.org |
GEMINI_API_KEY |
Recommended | Free key from Google AI Studio. Powers /api/copilot/*; Copilot falls back to a degraded result if unset, rather than failing |
MAPPLS_KEY |
Yes | Free key from Mappls, used for India-focused geocoding |
OCM_KEY |
Optional | Free key from openchargemap.org. Charger coverage degrades gracefully to e-AMRIT + Overpass if unset |
TOMTOM_API_KEY |
Optional | Free key from TomTom Developer Portal. Traffic-aware routing degrades gracefully if unset |
ALLOWED_ORIGIN |
Yes | CORS allowlist, e.g. http://localhost:3000 |
PORT |
No | Defaults to 5000 |
frontend/.env.local
| Variable | Notes |
|---|---|
REACT_APP_API_BASE_URL |
Where the backend is running — http://localhost:5000 locally, or the deployed backend URL in production |
The frontend never reads
REACT_APP_ORS_KEYorREACT_APP_OCM_KEY— those API keys live server-side only.
- Backend → Render free tier. Root directory
backend/, set the env vars above in the dashboard. Free tier sleeps after inactivity — first request after idle takes ~30–60s to wake up. - Frontend → Vercel or Netlify free tier, static CRA build. Set the project's root/base directory to
frontend/in the platform dashboard, andREACT_APP_API_BASE_URLto the deployed Render URL.
- Charger aggregation (
backend/services/chargerAggregator.js) — merges and deduplicates e-AMRIT, Open Charge Map, and OSM Overpass by proximity + name similarity, since no single free source has adequate India coverage alone. - Range modeling (
frontend/src/utils/batterySimulation.js) — real-world efficiency is derived from claimed range at a fixed real-world/claimed ratio, then adjusted per-segment for elevation gain, a static hot-season heuristic (no live weather API, by design), and highway-vs-city road type. - AI Copilot anchoring (
backend/services/tripClusterer.js) — curated places are anchored to the trip's start/charging-stops/destination by real proximity radius, not naive route-position windows, so suggestions are actually reachable from where the driver will be stopped. - Rate limiting (
backend/middleware/rateLimiters.js) — scoped per-endpoint to each external API's actual free-tier quota (Gemini's ~20 req/day shared quota is by far the tightest constraint), not a single blanket limit. - Session-scoped trip persistence (
frontend/src/utils/tripPersistence.js) — deliberatelysessionStorage, notlocalStorage: matches the actual need ("survive a refresh") without the staleness and cross-tab conflicts that permanent storage introduced.
Personal portfolio/student project. All rights reserved unless a LICENSE file is added.








