How Plant Mood live-streams its microgreen rack with an ESP32-CAM and keeps it secure

By Geert Warmenbol · Published 19 May 2026

How Plant Mood live-streams its microgreen rack with an ESP32-CAM and keeps it secure

Key takeaways

An ESP32-CAM broadcasting MJPEG on a default port with no authentication is publicly accessible the moment port forwarding is active -- Cloudflare Tunnel eliminates the need to open any inbound port at all.

Plant Mood pushes sensor readings from AHT20, BH1750, AS7341, capacitive soil moisture, and DS18B20 to a Supabase table every 5 minutes via a single HTTP POST with a service-role key stored in firmware.

RTSP requires a persistent TCP session and breaks behind most NAT configurations without a STUN or relay server -- MJPEG over HTTP is simpler to proxy and easier to restrict with middleware.

A signed URL with a 60-second expiry on the stream embed means a scraped embed code is useless after the token expires -- this is the minimum viable access control for a public-facing grow camera.

Securing an ESP32 behind a reverse proxy uses the same logic as putting any internal service behind a firewall -- the device should never be the one deciding who gets access.

TL;DR

We built the Plant Mood monitoring stack on ESP32 hardware with five sensor types, pushed every reading to Supabase, and added a live camera feed to the public dashboard. The stream took two hours to wire up and three days to secure properly. If you skip the reverse proxy step, you are not streaming your microgreens -- you are handing out an unauthenticated entry point to your local network. This article covers both halves: how the hardware is wired and why the security layer is not optional.

The camera was live before the firewall was

The first time the ESP32-CAM broadcast a live feed of the microgreen rack, I could see it from my phone on the other side of the city within four minutes of enabling port forwarding. Tray of sunflower on day three, moisture mat visible, temperature widget in the corner. Clean. The problem was that anyone else with the IP address and the default port could see the same thing -- and more importantly, could attempt to talk to the same device on the same network segment as everything else in the apartment.

That four-minute demo is where most small grow operations stop thinking about this problem. The stream works, so the job is done. What I had actually built was an unauthenticated embedded device with a live connection to my local network, sitting behind nothing but a consumer router with default settings. The camera was the least interesting attack surface.

How the sensor stack is wired and what each device does

The Plant Mood monitoring setup runs on an ESP32 development board paired with an ESP32-CAM module. Each sensor handles one measurement domain. The AHT20 covers ambient temperature and relative humidity over I2C -- it is more stable than a DHT22 in high-humidity environments and does not degrade as fast with repeated reads. The BH1750 measures lux over I2C as well, which gives a calibrated light intensity reading rather than the raw analogue voltage you get from a cheap photoresistor. The AS7341 is a ten-channel spectral sensor that reports across eight visible wavelengths plus near-infrared and clear, which matters when you are running mixed-spectrum LED panels and want to know what is actually hitting the canopy, not just what the light is rated to output. Soil moisture comes from a capacitive sensor -- resistive probes corrode fast in wet coco or hemp mat. The DS18B20 sits on a one-wire bus and reads substrate or water temperature depending on placement.

All five feed into the ESP32 main board. Every five minutes a function fires, reads each sensor, packages the payload as a JSON object, and sends it via HTTP POST to a Supabase edge function endpoint. The edge function validates the payload structure and writes the row to a time-series table. The service-role key that authorises the write is stored in the ESP32 firmware as a compile-time constant -- not ideal from a secrets-management standpoint, but acceptable for a device that never leaves a locked apartment and whose key can be rotated from the Supabase dashboard in under two minutes.

A typical payload looks like this: timestamp, device ID, temperature in Celsius, relative humidity as a percentage, lux as a float, AS7341 channel values as an array of integers, moisture as a raw ADC value mapped to a 0-to-100 scale, and substrate temperature. The edge function rejects any payload missing a required field, which means a firmware bug that drops one sensor does not silently write partial rows -- it fails loud and the gap is visible in the dashboard.

The five-minute push interval is a deliberate trade-off. An ESP32 polling at one-minute intervals on battery would discharge a 3000mAh cell in under eight hours. On mains power with the WiFi radio active continuously, the board runs warm. Five minutes is the interval at which the data is still granular enough to catch a temperature spike during the afternoon heat peak but the radio duty cycle is low enough that the board stays under 40 degrees Celsius even in a 32-degree room.

MJPEG vs RTSP: why the protocol choice is also a security choice

RTSP (Real Time Streaming Protocol) is what most people reach for when they think about IP camera streaming. It is the protocol used by Hikvision, Dahua, and most commercial CCTV hardware. The ESP32-CAM can broadcast RTSP. The reason we do not use it for the Plant Mood dashboard is not image quality -- it is infrastructure.

RTSP requires a persistent TCP session between the viewer and the device. That session does not survive a NAT boundary cleanly without a relay or STUN server. In a home or apartment setup where the ESP32 is behind a consumer router, you either open a port (which exposes the device directly), set up a dedicated relay server (which adds a dependency and cost), or accept that the stream breaks every time the router reassigns a local IP. None of these are acceptable for a dashboard that needs to be reliably embeddable on a public webpage.

MJPEG over HTTP is simpler. The ESP32-CAM serves a stream of individual JPEG frames at a configurable rate over a standard HTTP connection. A reverse proxy can sit in front of that HTTP endpoint and handle authentication, rate limiting, and TLS termination without caring that the underlying content is a video stream. The stream is just HTTP -- the proxy handles it the same way it handles any other request.

At 640x480 resolution and 10 frames per second, the MJPEG stream from the ESP32-CAM uses approximately 800 kilobits per second of bandwidth on the local network. At 320x240 and 5fps -- which is enough to see whether a tray has germinated or whether the lux panel has dropped -- it drops below 200 kilobits per second. On a 50Mbps residential upload, either setting has no measurable impact on other traffic.

One real limitation of MJPEG over HTTP for grow monitoring: every new viewer connection opens a new HTTP connection to the ESP32-CAM. The ESP32-CAM can handle two to three simultaneous MJPEG clients before frame rate degrades. If the dashboard is public and the stream embed gets significant traffic, the proxy should buffer the stream server-side and serve viewers from that single upstream connection rather than forwarding each viewer's request directly to the device.

How to lock down an ESP32-CAM stream without opening a single inbound port

Install cloudflared on a machine on the same local network as the ESP32-CAM. This can be a Raspberry Pi, a spare laptop, or any always-on device. Run 'cloudflared tunnel create grow-cam' to register a named tunnel with Cloudflare. This step has no external network exposure -- the tunnel is outbound-only from the local machine to Cloudflare's edge.

A tunnel credential file is created locally. No inbound ports are opened on the router. The ESP32-CAM is still completely unreachable from outside the local network.

Configure the tunnel to forward a Cloudflare-managed subdomain to the ESP32-CAM's local IP and MJPEG stream port (default 81 on the ESP32-CAM firmware). The config file maps the public hostname to http://192.168.x.x:81/stream. Start cloudflared as a system service so it restarts on reboot.

The stream is now reachable at a Cloudflare-proxied HTTPS URL. TLS is handled by Cloudflare. The ESP32 itself still speaks plain HTTP internally -- which is fine because the traffic never leaves the local network unencrypted.

Enable Cloudflare Access on the tunnel's hostname. Set the policy to 'one-time pin' or 'service token' depending on whether the stream is for personal viewing or for embedding on a website. A service token is a client ID and secret pair that is added as a header on authenticated requests -- the dashboard backend holds the token, not the browser.

Any request to the stream URL without a valid Access token returns a 302 to Cloudflare's login page or a 401 depending on the policy type. Unauthenticated browsers cannot reach the stream.

For public dashboard embeds where viewers should not need to authenticate, generate the stream embed URL server-side using a short-lived signed token. The dashboard backend holds the service token, fetches a proxied frame or short clip on the viewer's behalf, and serves it. Do not embed the Cloudflare Access tunnel URL directly in public HTML -- it will break as soon as a visitor without credentials loads the page.

Public visitors see the live feed without any authentication prompt. The service token never appears in the browser. The ESP32-CAM is never directly addressable from the internet.

Set a firewall rule on the router (or on the cloudflared host itself using iptables) that blocks all inbound connections to the ESP32-CAM's local IP from any source other than the cloudflared host. This is belt-and-suspenders -- the tunnel is already outbound-only, but if another device on the local network is compromised, this rule contains the blast radius.

The ESP32-CAM is reachable from cloudflared and from devices used for firmware updates. It is not reachable from anything else, including other devices on the same WiFi network.

Step 4 is the one most developers skip. They set up the tunnel correctly, add Cloudflare Access, and then embed the protected URL directly in the public webpage. It breaks for every unauthenticated visitor and they either remove Access entirely or add a note that the stream requires login. The fix -- generating the embed server-side using the service token -- takes about thirty minutes to implement and means the public dashboard works without ever exposing credentials.

What the sensor logs actually showed versus what the setup was supposed to do

In the first 15 minutes after a LED panel powers on, the AS7341 channel values for blue (450nm) and red (670nm) are consistently 8 to 12 percent lower than their steady-state values. The panel is rated to reach full output at power-on. The datasheet does not mention a warm-up curve. We noticed this pattern after plotting spectral readings against timestamps across 40 cycles and seeing the same dip at the start of every photoperiod. (Plant Mood grow log data, 2024.)

The AS7341 spectrum sensor reads differently depending on panel warm-up time

If you are logging spectrum data to correlate with germination outcomes, readings taken in the first 15 minutes of a photoperiod are not representative of the light environment the seedlings are actually experiencing at steady state. Any grower using early-cycle spectral data to make intensity decisions is working with numbers that understate the real output.

The ESP32 uses a simple retry loop: if the HTTP POST to Supabase fails, it waits 30 seconds and tries again up to three times before logging a failed write and moving on. Across six months of logs, approximately 70 percent of all failed writes occur between 2am and 4am local time. The local ISP appears to have a maintenance or throttling window in that period. The failures are not long enough to trigger any sensor alert -- they appear as single missing rows in the time-series table. (Plant Mood grow log data, 2024.)

HTTP POST failures cluster around the 2am to 4am window in Bangkok

Any anomaly detection logic that relies on the absence of a row as an indicator of a device failure will generate false positives during this window. The system now distinguishes between a missing row caused by a write failure (logged separately) and a missing row caused by a device power loss -- the failure type matters for triage.

Across 22 tray cycles using the same capacitive sensor unit, the raw ADC value for a dry coco coir substrate at the start of a cycle differs by an average of 4 to 6 percent from the same substrate condition at day 14. The sensor reads wetter as the cycle progresses, even when watering volume and frequency are held constant. The most likely cause is temperature-dependent capacitance shift -- the sensor's dielectric changes slightly as the substrate warms from seedling respiration. (Plant Mood grow log data, 2024.)

Capacitive moisture readings drift upward over a 14-day tray cycle without any change in actual moisture

If a moisture threshold alert is set based on readings calibrated at day 1, it will trigger progressively earlier in the cycle as the crop ages. We now calibrate the alert threshold separately for the first week and the second week of a cycle rather than using a single fixed value.

Plant Mood grow log data, 2024

The service-role key in firmware is not the security problem you think it is

That assumption is correct as stated, but it misidentifies the actual threat model for a device in a locked apartment that is not commercially distributed. The service-role key is a write key to a single Supabase project. If it is compromised, the attacker can write sensor data to the table or, if row-level security is misconfigured, read data from other tables. Neither outcome is catastrophic for a one-person grow operation. The real security problem is not the key in the firmware -- it is having no RLS policy on the sensor data table and no rate limiting on the edge function endpoint. A compromised key that can only write to a rate-limited endpoint with a strict schema is a low-severity issue. A compromised key with unrestricted database access is a high-severity issue. Most people confuse the key's existence with the damage it can do.

We rotated the service-role key in the firmware twice during 2024 testing -- once after a firmware build was accidentally pushed to a public GitHub repository for 47 minutes before being made private. No anomalous writes were detected in the logs. The table had RLS enabled and the edge function had a 100-writes-per-hour rate limit per device ID. The key rotation took four minutes: generate new key in Supabase, update firmware constant, recompile, flash. The process is fast enough that key rotation is a realistic response to a suspected exposure rather than a worst-case scenario.

Most developers who see a Supabase service-role key stored as a compile-time constant in ESP32 firmware immediately flag it as a critical security issue. The assumption is that someone who extracts the firmware binary can read the key and gain full database access.

Where this kind of setup is heading

Within two years, consumer router firmware will begin flagging ESP32 and similar microcontroller devices attempting outbound connections to cloud endpoints as potential IoT security risks, triggering mandatory captive portal confirmation for first-time connections.

Router vendors are under increasing pressure from ISPs and regulators to reduce botnet participation from home networks. The most common vector is compromised IoT devices making outbound connections. An ESP32 making a POST request to a Supabase URL every five minutes looks, at the packet level, identical to a compromised device checking in with a command-and-control server. The pattern is the same. The volume is the same. As router-level threat detection improves, benign IoT traffic will get caught in the same filters as malicious traffic.

A consumer router firmware update from any of the top five vendors (TP-Link, ASUS, Netgear, Linksys, Ubiquiti) that includes a category for 'unverified IoT outbound connection' with a prompt or block behavior. Check firmware release notes for those vendors in Q1 2026 and Q1 2027.

The combination of spectral sensing and substrate temperature logging will produce the first publicly available dataset showing a correlation between AS7341 blue-channel intensity and germination rate variance across grow media types in tropical environments -- and it will come from a hobbyist operation, not a research institution.

Research institutions running controlled environment agriculture studies do not use $15 spectral sensors on $4 development boards. Hobbyist growers with logging infrastructure do, and they run dozens of cycles per year rather than two or three. The sample size advantage is on the hobbyist side. The data is being collected right now across operations like Plant Mood. What is missing is the publication step.

Search Google Scholar and preprint servers for 'AS7341 germination microgreens' or 'ten-channel spectral sensor germination rate' annually. If no hobbyist-origin dataset appears by end of 2026, the confidence level drops to low.

Securing embedded devices is the same discipline as securing business infrastructure

The mental separation between 'my grow room camera' and 'my business network security' is a fiction that costs real money when it breaks. An ESP32-CAM connected to a home router that also carries a small business's Stripe webhooks, accounting software sessions, and client email is not a grow room camera. It is an endpoint on a mixed-trust network. The security posture for that device should be derived from the most sensitive thing on the same network, not from the fact that it is pointing at a tray of radish.

The counterargument is correct about the direct risk to the grow data. It is wrong about the network. The camera is not the target -- it is the entry point. The same apartment network that hosts the ESP32-CAM also hosts the laptop with saved banking credentials, the NAS with client files, and the phone that receives two-factor authentication codes. An unsecured embedded device on that network is not a grow room problem. It is a business infrastructure problem dressed in a plant costume. The Cloudflare Tunnel setup takes one evening. The firewall rule takes ten minutes. The cost of not doing it is unbounded.

The reasonable counterargument is that this is disproportionate for a hobbyist setup. The device is cheap, the data is not sensitive, and the realistic threat is low. Most hobbyist growers are not targeted by sophisticated attackers. Spending three days securing a camera that watches microgreens is not a rational use of time relative to the risk.

One thing to do this week

If you already have an ESP32-CAM or any other embedded device on your network with an open port or active port forwarding rule, close the port and install cloudflared this week. The tunnel setup is free, the documentation is clear, and it takes under two hours from scratch. The stream will work better -- stable HTTPS, no IP address management, no router config -- and the device will no longer be directly addressable from outside your network. That is the minimum. If you run any kind of business on the same network as your grow equipment, treat every embedded device as part of your business infrastructure and ask whether your current setup would survive a compromise of any single device on it.

FAQ

How does Plant Mood push sensor data from an ESP32 to Supabase?

The ESP32 reads all sensors every 5 minutes and sends a JSON payload via HTTP POST to a Supabase edge function. The edge function validates the payload schema and writes the row to a time-series table. The service-role key authorising the write is stored as a compile-time constant in firmware. Failed writes are logged separately so gaps in the time-series data can be distinguished from device power loss events.

Is RTSP or MJPEG better for an ESP32-CAM grow room stream?

MJPEG over HTTP is more practical for a home or apartment grow setup. RTSP requires a persistent TCP session that breaks behind most NAT configurations without a relay server. MJPEG is standard HTTP, which means a reverse proxy can sit in front of it for authentication and TLS without any protocol-specific handling. At 320x240 and 5fps, the bandwidth is under 200 kilobits per second -- enough to verify germination status.

How do I secure an ESP32-CAM stream without opening ports on my router?

Install cloudflared on any always-on device on the same local network as the ESP32-CAM. Create a named Cloudflare Tunnel that maps a public HTTPS subdomain to the ESP32's local IP and MJPEG port. The tunnel is outbound-only -- no inbound ports are opened on the router. Add Cloudflare Access on the tunnel hostname and use a service token for authenticated server-side embedding so public viewers never need to authenticate directly.

Does the AS7341 spectral sensor give accurate readings immediately after a LED panel turns on?

No. In Plant Mood's grow log data across 40 cycles, AS7341 blue and red channel values are 8 to 12 percent lower than steady-state in the first 15 minutes after LED panel power-on, despite the panel being rated for immediate full output. Spectral readings taken during this warm-up period will understate actual canopy light levels. Calibrate or exclude readings from the first 15 minutes of each photoperiod if correlating spectral data with germination outcomes. (Plant Mood grow log data, 2024.)

Is it safe to store a Supabase service-role key in ESP32 firmware?

It is an acceptable risk if the key's blast radius is contained. Enable row-level security on all tables the key can access and add rate limiting to the edge function endpoint. A key that can only write to a rate-limited, schema-validated endpoint is low severity if exposed. A key with unrestricted database access is high severity. The key itself is not the problem -- the permissions attached to it are. Plant Mood rotated a key that was briefly exposed publicly and detected no anomalous writes due to RLS and rate limiting being in place.

Why do ESP32 sensor write failures cluster at specific times of night?

In Plant Mood's Bangkok setup, approximately 70 percent of all Supabase HTTP POST failures occur between 2am and 4am local time, consistent with a local ISP maintenance or throttling window. The failures appear as single missing rows in the time-series table. If anomaly detection treats any missing row as a device failure alert, this window will generate false positives. Log failed writes separately with a reason code so the monitoring system can distinguish write failures from device power loss.

Why does a capacitive soil moisture sensor read wetter as a tray cycle progresses?

Across 22 tray cycles at Plant Mood, raw ADC values for the same substrate dryness level drift upward by 4 to 6 percent between day 1 and day 14. The likely cause is temperature-dependent capacitance shift as the substrate warms from seedling respiration. A fixed moisture threshold alert calibrated at day 1 will trigger progressively earlier in the cycle as the crop ages. Calibrate alert thresholds separately for the first and second week of each crop cycle. (Plant Mood grow log data, 2024.)