All projects

2026-09-13

FlexiCharge

An EV charging app - find a station on a map, start a session, watch it charge in real time, and get a receipt when you stop.

FlexiCharge

There's no hosted live demo for this project, because it runs locally with Docker. However, the video above shows a full charging session from start to finish.

Why I built this

I reached a point where I didn't want to build another standard app with CRUD. I wanted to build something that communicates with real hardware (mocked battery in this case). In order to gain experience with a hardware communication protocol, I picked OCPP, the protocol that real-world Electric Vehicle chargers speak. I was inspired by a university project in partnership with Knowit, where I was on the backend team. So, I decided to build the entire app solo, and instead of using real hardware, I created a simulator that mimics the behavior of a real charger

What it does

  • Find a charger: browse stations on a map or in a list, or type the code written on a charger.
  • Charge: start a session and watch the charge level, energy and cost update live, then stop it whenever you like.
  • Pay automatically: save a card once, and every session is charged to it with a receipt. Payments are simulated, so no real card is ever charged.
  • My pages: see your profile, receipts and charging history.
  • Admin dashboard: watch live sessions, look through past ones, manage stations and chargers, and administer user accounts.

See it in action

Below is a full walkthrough, created and narrated using AI.

Built with

  • Frontend: React, TypeScript, Vite, Tailwind CSS, TanStack Query, Zustand
  • Backend: Node.js, Express, Drizzle ORM
  • Charger communication: Python, FastAPI, OCPP 2.0.1
  • Database and messaging: PostgreSQL, Redis
  • Maps: Leaflet, OpenStreetMap
  • Testing: Vitest, pytest, Playwright
  • Tooling: Docker Compose, GitHub Actions

Notable decisions

  • A separate service for the charger link: the OCPP tooling I wanted only exists in Python, so charger communication runs as its own FastAPI service instead of being forced into the Node backend. This keeps the physical charger protocol and the app API isolated and focused.
  • Real-time sync: I wanted to avoid using the polling method. Instead, a charger's live events (status, meter values) go over WebSocket into the Python gateway, onto a Redis/BullMQ queue, and the Node API consumes them, writes PostgreSQL as the single source of truth, then streams the update straight to the browser over Server-Sent Events.
  • Docker for infra only: every service was containerized at the start. For a project this size, I realized that only running Redis and Postgres in Docker while the app and rest of services locally turned out to be a better choice for the development experience. Doing so showed instant feedback without needing to restart containers to see a change.