At Renewalytics, we needed a realtime monitoring dashboard for renewable energy plants. Telemetry data flows in every 5 seconds from SCADA systems across dozens of plants. Operators need to see live generation data, alarm states, and performance metrics, with zero perceived latency.
This is a different beast from a typical CRUD dashboard. Here's how we built it.
The system has three layers:
SCADA → MQTT Broker → Ingestion Service → PostgreSQL
↘ WebSocket Gateway → DashboardThe key insight: don't use WebSockets for initial page load. Server-render the current state, then upgrade to WebSocket for live updates. This gives you instant first paint and realtime updates.
The dashboard page loads with the latest telemetry data server-rendered:
async function PlantDashboard({ params }: { params: { plantId: string } }) {
const plant = await db.plant.findUnique({
where: { id: params.plantId },
include: {
latestTelemetry: true,
activeAlarms: true,
},
});
return (
<div>
<PlantHeader plant={plant} />
<TelemetryGrid initialData={plant.latestTelemetry} plantId={plant.id} />
<AlarmPanel initialAlarms={plant.activeAlarms} plantId={plant.id} />
</div>
);
}The TelemetryGrid component connects to WebSocket on mount and merges live updates into the server-rendered state:
"use client";
function TelemetryGrid({ initialData, plantId }) {
const [data, setData] = useState(initialData);
useEffect(() => {
const ws = new WebSocket(`${WS_URL}/plants/${plantId}/telemetry`);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
setData((prev) => ({
...prev,
[update.metricId]: {
...prev[update.metricId],
value: update.value,
timestamp: update.timestamp,
},
}));
};
return () => ws.close();
}, [plantId]);
return (
<div className="grid grid-cols-4 gap-4">
{Object.entries(data).map(([key, metric]) => (
<MetricCard key={key} metric={metric} />
))}
</div>
);
}WebSocket connections drop. Networks are unreliable. You need exponential backoff with jitter:
function useReliableWebSocket(url: string) {
const [data, setData] = useState(null);
const retriesRef = useRef(0);
useEffect(() => {
let ws: WebSocket;
let timeout: NodeJS.Timeout;
function connect() {
ws = new WebSocket(url);
ws.onopen = () => {
retriesRef.current = 0; // reset on successful connection
};
ws.onmessage = (event) => {
setData(JSON.parse(event.data));
};
ws.onclose = () => {
const delay = Math.min(1000 * 2 ** retriesRef.current, 30000);
const jitter = delay * 0.1 * Math.random();
timeout = setTimeout(connect, delay + jitter);
retriesRef.current++;
};
}
connect();
return () => {
ws?.close();
clearTimeout(timeout);
};
}, [url]);
return data;
}Plain PostgreSQL handles our telemetry storage. Why not InfluxDB or Prometheus?
-- Materialized view for hourly generation data
CREATE MATERIALIZED VIEW hourly_generation AS
SELECT
plant_id,
date_trunc('hour', timestamp) AS hour,
AVG(generation_kw) AS avg_generation,
MAX(generation_kw) AS peak_generation
FROM telemetry
GROUP BY plant_id, hour;If you need a realtime dashboard for your operations, IoT fleet, or monitoring system, I've built this at scale. Get in touch.