zevOS

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

01

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.

02

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.

03

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.

04

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.

05

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.

06

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.