01Platform · Architecture
Built so that a charger never waits on a database
Charging platforms fail in a characteristic way: something slow happens deep in the system, a charger times out waiting for a response, disconnects, and reconnects — and now there is a reconnect storm on top of the original problem. Almost every architectural decision here exists to prevent that.
The shape of it
How a frame becomes a record
charger ──wss──▶ OCPP gateway ──event──▶ stream ──▶ workers ──▶ PostgreSQL
│ │
validate, normalise, sessions, billing,
respond immediately availability, alerts
│
console / API ◀──────────────────────────────── read models
driver app ◀── SSE / webhooks ───────────────────┘The gateway’s only job is to answer the charger. It validates the frame against the schema, normalises it into a domain event, responds, and emits. It performs no database writes, applies no billing, and makes no authorisation decisions — which is what keeps its response time independent of everything downstream.
Layers
Five layers, one responsibility each
The boundaries matter more than the components. Each layer can be scaled, replaced or debugged without the others.
- 01
OCPP gateway
Speaks the protocol
Holds a long-lived WebSocket session with every charger. Validates and normalises each frame, responds immediately, and emits a domain event. It writes nothing to the database and applies no billing or authorisation logic — because a gateway that blocks is a charger that disconnects.
- 02
Event stream
Carries the truth
Normalised events — session started, meter value recorded, charger faulted, command acknowledged — move on Redis streams with consumer groups. Unacknowledged messages are reclaimed on restart, so a worker crash delays processing rather than losing it.
- 03
Workers
Does the thinking
Idempotent consumers apply the events: creating and closing sessions, computing cost against the snapshotted tariff, updating availability, triggering payments and refunds, and raising alerts. Every handler is keyed so reprocessing a duplicate is a no-op.
- 04
API & console
Where people work
A FastAPI service with tenant scoping applied in the data-access layer rather than trusted to individual queries, and a Next.js console that renders the same data an operator would get from the API — because it is the same API.
- 05
Driver surfaces
Where money starts
The QR charging flow, the driver apps and the station map. Built to work on a bad connection for someone who has never used them before, under your brand rather than ours.
Principles
The six rules the codebase is held to
These are enforced in review rather than aspired to in a document — most of them exist because the alternative caused an incident somewhere.
The gateway never blocks
Protocol handling is separated from business logic entirely. A charger waiting on a response while a billing query takes a lock is a charger that times out, disconnects and reconnects — and now you have a reconnect storm layered on a slow query.
Every consumer is idempotent
At-least-once delivery guarantees duplicates eventually. Handlers are keyed on natural identifiers so reprocessing a StopTransaction is a no-op rather than a second bill.
Tenant isolation lives in the data layer
Scoping is applied where queries are built, not remembered by each developer at each call site. One operator cannot reach another’s data through any surface, including the API and exports.
Protocol versions stay isolated
OCPP 1.6 and 2.0.1 have separate handlers that normalise into one internal event vocabulary. Everything downstream — billing, analytics, uptime, the console — consumes the normalised form, so adding a version is not a platform-wide change.
Money is written once and reconciles
Payments, refunds, wallet movements and settlements post to a ledger against the session they belong to. Aggregates are always traceable to the rows that produced them.
Long work belongs in workers
No API request performs a settlement run, a bulk firmware rollout or a report generation. Endpoints stay fast, and heavy work scales horizontally and retries safely.
Failure modes
What happens when something goes wrong
A charging platform is judged on its bad days. These are the five that matter and how each is handled.
A charger disconnects mid-session
The drop is recorded and the connector stops accepting new sessions. If the charger buffered the transaction locally, it is replayed and reconciled on reconnect — and because the handler is keyed on the transaction, the replay is a no-op rather than a second bill.
A worker crashes mid-event
The message was never acknowledged, so it is reclaimed by the consumer group on restart. Processing is delayed, not lost. Every handler is idempotent precisely because this path exists.
The database pool is exhausted
Long before CPU or network saturate, connections run out — and the symptom is silence rather than errors. Pools are kept small and explicit, utilisation is a first-class metric, and event processing runs under a hard timeout so a stuck handler cannot deadlock its consumer group.
A charger sends a malformed payload
The frame fails schema validation at the gateway and is rejected with a protocol error rather than propagating into the event stream. The frame is retained in the message log so the vendor quirk can be identified.
A payment capture fails after the session
The session is complete and the energy delivered, so the failure is recorded against the session and surfaced for follow-up rather than silently written off. Refunds and captures are ledgered against the same session record.
Stack
What it runs on
Nothing exotic. The interesting decisions are about boundaries, not about components.
Security and data handling- API
- Python 3.11+, FastAPI, fully async
- Database
- PostgreSQL with connection pooling and explicit transactions
- Cache & streams
- Redis — rate limiting, ephemeral state, event streams
- Protocol
- OCPP-J over WebSocket, isolated handlers per version
- Console
- Next.js App Router, TypeScript strict, server components by default
- Driver surfaces
- Mobile web charging flow plus native Android and iOS apps
- Multi-tenancy
- Shared schema with tenant scoping applied in the data-access layer
- Deployment
- Containerised services on Indian cloud regions
Bring your architect to the walkthrough
We are happy to go through the protocol layer, the event model, the tenancy boundary and the failure paths in as much detail as your team wants.