OCPP WS IOocpp-ws-io
OCPP WS UI

Architecture

Understanding the internal architecture, data flow, and component interactions of ocpp-ws-board.

Architecture

This document explains the internal architecture of ocpp-ws-board and how data flows through the system.


High-Level Overview


Components

1. OCPP Plugin (BoardPlugin)

A passive observer plugin that hooks into the OCPP server lifecycle:

// Passive event listeners
onConnection: (client) => store.addConnection(client)
onMessage: (message) => {
  store.addMessage(message)
  messageBroker.emit('message', message)
}
onDisconnect: (client) => store.removeConnection(client)
onSecurityEvent: (event) => {
  store.addSecurityEvent(event)
  securityBroker.emit('security', event)
}

Key Features:

  • Zero performance impact on message processing
  • Non-blocking async operations
  • Thread-safe event handling

2. MemoryStore (CompressedMemoryStore)

In-memory ring buffers with configurable limits:

Ring Buffer Implementation:

  • Automatically evicts oldest entries when limit reached
  • O(1) append operations
  • Memory-efficient with no leaks
  • Configurable per-collection limits

3. SSE Brokers (SSEBroker)

Server-Sent Events broadcast system:

class SSEBroker {
  private clients: Set<ReadableStreamDefaultController>
  
  // Add new client
  subscribe(): ReadableStream
  
  // Broadcast to all clients
  emit(event: string, data: unknown): void
  
  // Heartbeat to keep connections alive
  private heartbeat(): void
}

Three Separate Brokers:

  1. MessageBroker — Live OCPP messages
  2. TelemetryBroker — Server metrics (2s interval)
  3. SecurityBroker — Security events

4. Hono REST API

Lightweight HTTP framework serving:

  1. REST API Endpoints/api/* routes
  2. SSE Streams/api/*/stream endpoints
  3. Static Files — Pre-built React SPA

Authentication Middleware:

function requireAuth(c, next) {
  if (!auth.requiresAuth()) return next()
  
  const sessionId = parseSessionCookie(c.req.header("cookie"))
  if (!sessionId) return c.json({ error: "Unauthorized" }, 401)
  
  const session = auth.getSession(sessionId)
  if (!session) return c.json({ error: "Session expired" }, 401)
  
  c.set("user", session.user)
  return next()
}

5. React UI

Single-page application built with:

  • React 19 — Latest features and performance
  • React Router 7 — Client-side routing
  • Tailwind CSS 4 — Utility-first styling
  • Recharts 3 — Data visualization
  • Shadcn/ui — Accessible component library

Key Pages:

  • Overview (dashboard stats)
  • Connections (station list)
  • Messages (packet inspector)
  • Logs (terminal viewer)
  • Telemetry (performance charts)
  • Security (event monitor)

Data Flow

Message Flow


Telemetry Flow


Authentication Flow


Memory Management

Ring Buffer Eviction

// When buffer exceeds maxMessages
if (messages.length > maxMessages) {
  messages.splice(0, messages.length - maxMessages)
  // Oldest messages removed automatically
}

Memory Footprint

ComponentTypical SizeMax Configurable
Messages50-100KB (5000 msgs)~1MB
Connections5-10KB~100KB
Telemetry<1KBN/A
Security Events10-20KB~500KB

Total: Typically <200KB in memory for a busy server


Performance Considerations

SSE Optimization

  • Heartbeat interval: 15s default (prevents timeout)
  • Batching: Telemetry sent every 2s
  • Backpressure: Slow clients are automatically disconnected
  • Compression: None (SSE is text-based)

REST API

  • Caching: None (real-time data)
  • Pagination: Limit/offset for large collections
  • Response time: <10ms typical
  • Concurrency: Hono handles thousands of requests

UI Performance

  • Virtualization: Large lists use virtual scrolling
  • Debounce: Search inputs debounced 300ms
  • Memoization: React.memo for expensive components
  • Code splitting: Lazy-loaded routes

Extensibility

Custom Store

You can replace the default CompressedMemoryStore with your own implementation:

import { createBoard } from "ocpp-ws-board";

class RedisStore {
  async addMessage(msg) { /* ... */ }
  async getMessages(limit) { /* ... */ }
  // ... implement all store methods
}

const board = createBoard({
  store: new RedisStore(),
});

Custom Authentication

const board = createBoard({
  auth: {
    mode: "custom",
    validate: async (credentials) => {
      // Check against your database
      const user = await db.users.find(credentials.token)
      if (!user) throw new Error("Invalid token")
      return { name: user.name, role: user.role }
    },
  },
});

Custom Metrics

Extend telemetry with your own metrics:

const board = createBoard({});

// Periodically update custom metrics
setInterval(() => {
  const customMetrics = {
    activeChargingSessions: getActiveSessions(),
    totalEnergyDelivered: getEnergyDelivered(),
  }
  board.telemetryBroker.emit("telemetry", customMetrics)
}, 2000)

Security

Data Protection

  • In-memory only: No data persisted to disk
  • Session expiry: Automatic cleanup of expired sessions
  • Input validation: All API inputs sanitized
  • Rate limiting: Built-in protection against abuse

Network Security

  • CORS: Configurable allowed origins
  • HTTPS: Required in production
  • Cookie flags: HttpOnly, Secure, SameSite
  • Token storage: Server-side only (no client storage)

Next Steps

On this page