<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[project]]></title><description><![CDATA[project]]></description><link>https://vinay-project.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 03:18:23 GMT</lastBuildDate><atom:link href="https://vinay-project.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Scalable ESP32 IoT Platform: Architecture & End-to-End Flow Explained]]></title><description><![CDATA[IoT projects often fail not because of hardware limitations, but because of weak architecture. An ESP32 sending sensor data is easy. Managing devices, users, commands, telemetry, and cloud integrations in a secure and scalable way is not.
This articl...]]></description><link>https://vinay-project.hashnode.dev/building-a-scalable-esp32-iot-platform-architecture-and-end-to-end-flow-explained</link><guid isPermaLink="true">https://vinay-project.hashnode.dev/building-a-scalable-esp32-iot-platform-architecture-and-end-to-end-flow-explained</guid><category><![CDATA[iot]]></category><category><![CDATA[iot project]]></category><category><![CDATA[Azure]]></category><category><![CDATA[PowerBI]]></category><category><![CDATA[cloud native]]></category><category><![CDATA[deployment]]></category><category><![CDATA[deployment strategies]]></category><category><![CDATA[projects]]></category><category><![CDATA[ESP32]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[aurdino]]></category><category><![CDATA[microcontroller]]></category><dc:creator><![CDATA[Vinay Tilada]]></dc:creator><pubDate>Mon, 05 Jan 2026 18:30:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ajIiyN9tUPY/upload/6002d222a5f4ad69bdef8d369f66a0a6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>IoT projects often fail not because of hardware limitations, but because of weak architecture. An ESP32 sending sensor data is easy. Managing devices, users, commands, telemetry, and cloud integrations in a secure and scalable way is not.</p>
<p>This article breaks down a <strong>production-style ESP32 platform</strong> I recently built, covering the <strong>device firmware, backend APIs, frontend dashboard, Firebase integration, and Azure IoT Hub telemetry pipeline</strong>. The goal was to design something that behaves like a real-world IoT system rather than a demo project.</p>
<p><a target="_blank" href="https://esp-32-moduler-connection-front-bac.vercel.app/login">Project Link</a> (<strong>Live</strong>)</p>
<p><a target="_blank" href="https://github.com/MightyAcE58/ESP-32_moduler_connection_front_-_backend.git">Github Repo</a></p>
<hr />
<h2 id="heading-high-level-architecture">High-Level Architecture</h2>
<p>At a high level, the system has five major components:</p>
<ol>
<li><p><strong>ESP32 Device (Firmware in C++)</strong></p>
</li>
<li><p><strong>Backend APIs (Next.js on Vercel)</strong></p>
</li>
<li><p><strong>Frontend Dashboard (Next.js App Router)</strong></p>
</li>
<li><p><strong>Firebase (Auth + Firestore)</strong></p>
</li>
<li><p><strong>Azure IoT Hub (Telemetry &amp; Analytics)</strong></p>
</li>
</ol>
<p>The ESP32 never talks directly to Firebase or Azure.<br />Everything flows through the backend, which acts as a controlled gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767006423988/5ad478d7-949f-4c45-8ea9-667286b19b8d.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-device-onboarding-flow-captive-portal-cloud">Device Onboarding Flow (Captive Portal → Cloud)</h2>
<h3 id="heading-1-esp32-boots-in-setup-mode">1. ESP32 Boots in Setup Mode</h3>
<p>When powered on for the first time, the ESP32</p>
<ul>
<li><p>Starts a WiFi Access Point (<code>ESP32-SETUP</code>)</p>
</li>
<li><p>Hosts a captive portal</p>
</li>
<li><p>Waits for user configuration</p>
</li>
</ul>
<p>The user provides:</p>
<ul>
<li><p>Home WiFi SSID &amp; password</p>
</li>
<li><p>Email &amp; password (same credentials used on the dashboard)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767008379915/3a164ad7-59e7-4bbb-8635-b6bb84d80240.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-2-device-authentication-amp-linking">2. Device Authentication &amp; Linking</h3>
<p>After connecting to WiFi, the ESP32 sends a request to the backend:</p>
<pre><code class="lang-json">POST /api/device/login
</code></pre>
<p>The backend:</p>
<ul>
<li><p>Verifies credentials using Firebase Auth REST API</p>
</li>
<li><p>Retrieves the user UID</p>
</li>
<li><p>Links the device to the user in Firestore</p>
</li>
<li><p>Returns success to the device</p>
</li>
</ul>
<p>Important security decision:</p>
<ul>
<li><p>Credentials are <strong>never stored</strong> on the ESP32</p>
</li>
<li><p>They are used once and immediately discarded</p>
</li>
</ul>
<p>At this point, the device switches to <strong>normal operation mode</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767006892291/34330df7-44a5-4738-988f-96a5db8f10b0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-normal-device-operation">Normal Device Operation</h2>
<h3 id="heading-heartbeat-mechanism">Heartbeat Mechanism</h3>
<p>Every 30–60 seconds, the ESP32 sends a heartbeat:</p>
<pre><code class="lang-json">POST /api/device/heartbeat
</code></pre>
<p>This updates:</p>
<ul>
<li><p>Device online/offline status</p>
</li>
<li><p>Last seen timestamp in Firestore</p>
</li>
</ul>
<p>This allows the dashboard to show real-time device status without WebSockets.</p>
<h3 id="heading-command-polling-server-device">Command Polling (Server → Device)</h3>
<p>Since ESP32 cannot receive inbound requests reliably behind NATs, the system uses <strong>polling</strong>.</p>
<p>Every few seconds, the device calls:</p>
<pre><code class="lang-json">POST /api/device/poll
</code></pre>
<p>If a command exists:</p>
<ul>
<li><p>Backend returns it</p>
</li>
<li><p>Marks the command as delivered</p>
</li>
<li><p>ESP32 executes it locally</p>
</li>
</ul>
<p>This approach is simple, reliable, and production-friendly.</p>
<h2 id="heading-remote-sensor-switching-key-feature">Remote Sensor Switching (Key Feature)</h2>
<p>One of the most important design decisions was <strong>multi-sensor support without reflashing firmware</strong>.</p>
<h3 id="heading-supported-sensors">Supported Sensors</h3>
<ul>
<li><p>Soil Moisture</p>
</li>
<li><p>DHT11 / DHT22</p>
</li>
<li><p>LDR</p>
</li>
<li><p>BMP280</p>
</li>
</ul>
<p>The active sensor type:</p>
<ul>
<li><p>Is stored in ESP32 flash (Preferences / NVS)</p>
</li>
<li><p>Can be changed remotely via dashboard commands</p>
</li>
<li><p>Persists across reboots</p>
</li>
</ul>
<h3 id="heading-flow">Flow</h3>
<ol>
<li><p>User selects a sensor from the dashboard</p>
</li>
<li><p>Backend queues a command (<code>SENSOR_DHT11</code>, <code>SENSOR_LDR</code>, etc.)</p>
</li>
<li><p>ESP32 polls and receives the command</p>
</li>
<li><p>Firmware switches logic and telemetry format dynamically</p>
</li>
</ol>
<p>This turns a single ESP32 into a <strong>remotely configurable sensor node</strong>.</p>
<hr />
<h2 id="heading-device-groups-managing-iot-at-scale">Device Groups: Managing IoT at Scale</h2>
<p>As the number of devices grows, managing them individually becomes inefficient. Real-world IoT platforms rarely operate on a single-device model. Instead, devices are organized into <strong>groups</strong> based on location, purpose, or integration requirements.</p>
<p>To address this, the platform introduces a <strong>Device Group feature</strong>, allowing users to logically organize and manage multiple ESP32 devices together.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767008476343/282681dd-3c0e-4cc3-9d78-1a52445fab8f.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-firestore-group-data-model">Firestore Group Data Model</h2>
<p>A simplified schema:</p>
<pre><code class="lang-json">groups/
├── {groupId}/
│   ├── name: <span class="hljs-string">"Farm Sensors"</span>
│   ├── uid: <span class="hljs-string">"user_12345"</span>
│   ├── hasAzureIoT: <span class="hljs-literal">true</span>
│   ├── azureConnectionString: <span class="hljs-string">"HostName=..."</span>
│   ├── createdAt: Timestamp
│
groupDevices/
├── {autoId}/
│   ├── groupId: <span class="hljs-string">"group_abc"</span>
│   ├── deviceId: <span class="hljs-string">"ESP32_XYZ"</span>
│   ├── addedAt: Timestamp
</code></pre>
<h2 id="heading-groups-and-azure-iot-hub-integration">Groups and Azure IoT Hub Integration</h2>
<p>Groups are the <strong>control point for Azure integration</strong>.</p>
<p>When telemetry arrives from an ESP32:</p>
<ol>
<li><p>Backend checks which group the device belongs to</p>
</li>
<li><p>If the group has Azure IoT enabled:</p>
<ul>
<li><p>Device is auto-registered in Azure IoT Hub</p>
</li>
<li><p>Telemetry is forwarded to Azure</p>
</li>
</ul>
</li>
<li><p>If not:</p>
<ul>
<li>Data is stored only in Firestore</li>
</ul>
</li>
</ol>
<p>This avoids hardcoding cloud credentials per device and keeps sensitive Azure credentials off the ESP32.</p>
<hr />
<h2 id="heading-design-principles-behind-the-group-feature">Design Principles Behind the Group Feature</h2>
<p>The group system follows three core principles:</p>
<ol>
<li><p><strong>Firmware simplicity</strong><br /> ESP32 firmware remains generic and reusable.</p>
</li>
<li><p><strong>Backend-driven control</strong><br /> All orchestration happens server-side.</p>
</li>
<li><p><strong>Cloud-agnostic design</strong><br /> Today it’s Azure. Tomorrow it could be AWS or GCP without touching devices.</p>
</li>
</ol>
<hr />
<h2 id="heading-telemetry-flow-esp32-backend-azure">Telemetry Flow (ESP32 → Backend → Azure)</h2>
<h3 id="heading-step-1-device-sends-telemetry">Step 1: Device Sends Telemetry</h3>
<p>The ESP32 sends sensor data to the backend:</p>
<pre><code class="lang-json">POST /api/device/telemetry
</code></pre>
<p>Payload structure depends on the active sensor but always includes:</p>
<ul>
<li><p>deviceId</p>
</li>
<li><p>timestamp</p>
</li>
<li><p>sensor_type</p>
</li>
<li><p>sensor-specific values</p>
</li>
</ul>
<h3 id="heading-step-2-backend-processing">Step 2: Backend Processing</h3>
<p>The backend:</p>
<ol>
<li><p>Stores telemetry in Firestore</p>
</li>
<li><p>Checks if the device belongs to an Azure-enabled group</p>
</li>
<li><p>Registers the device automatically in Azure IoT Hub (if needed)</p>
</li>
<li><p>Forwards telemetry to Azure IoT Hub</p>
</li>
</ol>
<p>The ESP32 remains completely unaware of Azure.</p>
<h3 id="heading-step-3-azure-iot-hub">Step 3: Azure IoT Hub</h3>
<p>Once in Azure IoT Hub, the data can be:</p>
<ul>
<li><p>Visualized using Azure Metrics</p>
</li>
<li><p>Streamed to Power BI</p>
</li>
<li><p>Processed via Azure Functions</p>
</li>
<li><p>Routed using Stream Analytics</p>
</li>
</ul>
<p>This separation keeps firmware simple while enabling enterprise-grade analytics.</p>
<hr />
<h2 id="heading-frontend-dashboard-nextjs">Frontend Dashboard (Next.js)</h2>
<p>The dashboard is built using <strong>Next.js 14 App Router</strong> and provides:</p>
<ul>
<li><p>User authentication (Firebase Web SDK)</p>
</li>
<li><p>Device listing with online/offline state</p>
</li>
<li><p>Last seen timestamps</p>
</li>
<li><p>Command sending interface</p>
</li>
<li><p>Sensor switching via quick commands</p>
</li>
</ul>
<p>All API calls from the frontend:</p>
<ul>
<li><p>Use Firebase ID tokens</p>
</li>
<li><p>Are verified server-side using Firebase Admin SDK</p>
</li>
</ul>
<p>This ensures users can only control their own devices.</p>
<hr />
<h2 id="heading-backend-api-design">Backend API Design</h2>
<p>The backend is structured into two API categories:</p>
<h3 id="heading-device-apis-no-auth-token">Device APIs (No Auth Token)</h3>
<p>Used only by ESP32 devices:</p>
<ul>
<li><p><code>/api/device/login</code></p>
</li>
<li><p><code>/api/device/poll</code></p>
</li>
<li><p><code>/api/device/heartbeat</code></p>
</li>
<li><p><code>/api/device/telemetry</code></p>
</li>
</ul>
<h3 id="heading-user-apis-auth-required">User APIs (Auth Required)</h3>
<p>Used by the dashboard:</p>
<ul>
<li><p><code>/api/user/devices</code></p>
</li>
<li><p><code>/api/user/send-command</code></p>
</li>
</ul>
<p>Ownership checks are enforced at every step.</p>
<hr />
<h2 id="heading-firestore-data-model">Firestore Data Model</h2>
<p>The database schema is intentionally simple:</p>
<ul>
<li><p><code>users/{uid}</code></p>
</li>
<li><p><code>devices/{deviceId}</code></p>
</li>
<li><p><code>deviceCommands/{commandId}</code></p>
</li>
</ul>
<p>This allows:</p>
<ul>
<li><p>Horizontal scaling</p>
</li>
<li><p>Easy debugging</p>
</li>
<li><p>Clear ownership boundaries</p>
</li>
</ul>
<h2 id="heading-why-this-architecture-works">Why This Architecture Works</h2>
<p>This design solves several real-world IoT problems:</p>
<ul>
<li><p>Devices behind NATs</p>
</li>
<li><p>Secure user-device linking</p>
</li>
<li><p>Remote device control</p>
</li>
<li><p>Firmware simplicity</p>
</li>
<li><p>Cloud scalability</p>
</li>
<li><p>Vendor lock-in avoidance</p>
</li>
</ul>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This project taught me that <strong>IoT is more about systems design than sensors</strong>. The ESP32 is just one part of a much larger pipeline involving authentication, security, reliability, and data flow.</p>
]]></content:encoded></item></channel></rss>