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:
- MessageBroker — Live OCPP messages
- TelemetryBroker — Server metrics (2s interval)
- SecurityBroker — Security events
4. Hono REST API
Lightweight HTTP framework serving:
- REST API Endpoints —
/api/*routes - SSE Streams —
/api/*/streamendpoints - 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
| Component | Typical Size | Max Configurable |
|---|---|---|
| Messages | 50-100KB (5000 msgs) | ~1MB |
| Connections | 5-10KB | ~100KB |
| Telemetry | <1KB | N/A |
| Security Events | 10-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
- REST API Reference — Complete API documentation
- Integration Guide — Framework setup
- Quick Start — Get started