Firebase Realtime Database with ESP32 Using the REST API and Auth

Firebase Realtime Database is a cloud-hosted JSON tree from Google. Web and mobile apps that use the Firebase SDK receive changes to that tree almost instantly, which makes it popular for student IoT projects: the ESP32 writes a reading, and an Android app or web page shows it immediately. In the other direction, the app writes a value such as relay: true, and the ESP32 reads it and switches a load.

Several Arduino libraries wrap Firebase, but their APIs have changed significantly between major versions. This tutorial uses the plain Firebase REST API with the ESP32's built-in HTTPClient, so the code depends only on documented HTTP endpoints. You will sign in a device user with Firebase Authentication, write and push data, read a command, and protect the database with security rules.

How the REST API Maps to the Database

Every node in the JSON tree has a URL: the database URL, the path, then .json. The HTTP method decides what happens.

MethodEffectIoT use
GETRead the value at a pathFetch a relay command or setpoint
PUTReplace the value at a pathOverwrite the latest reading
PATCHUpdate only the given child keysChange one field without touching others
POSTAdd a child with an auto-generated, time-ordered keyAppend a reading to a history list
DELETERemove the pathClear old history
devices/
  <uid of device user>/
    latest:  { temp_c: 28.4, hum_pct: 61, ts: 1760500000000 }
    history: { -Nx1...: {...}, -Nx2...: {...} }
    relay:   true

Writing {".sv":"timestamp"} as a value makes the server fill in its own time in milliseconds since 1970, so the ESP32 does not need an accurate clock.

Firebase Console Setup

  1. In the Firebase console create a project, then open Build > Realtime Database > Create Database. Pick a location and start in locked mode.
  2. Copy the database URL shown above the data tree. It looks like https://PROJECT-default-rtdb.firebaseio.com or, for non-US locations, https://PROJECT-default-rtdb.REGION.firebasedatabase.app.
  3. Open Build > Authentication > Sign-in method and enable Email/Password.
  4. In the Users tab add a user for the device, for example node1@yourproject.dev with a strong password.
  5. Open Project settings > General and copy the Web API Key.
  6. Paste the rules below into the Rules tab of Realtime Database and publish.
Security rules: each signed-in user can only touch its own devices/ branch
{
  "rules": {
    "devices": {
      "$uid": {
        ".read": "auth != null && auth.uid === $uid",
        ".write": "auth != null && auth.uid === $uid"
      }
    }
  }
}

ESP32 Code

The device signs in through the Identity Toolkit REST endpoint, which returns an ID token and the user's UID (localId). The token is valid for one hour, so the sketch signs in again every 50 minutes. Each database request adds ?auth=ID_TOKEN. A relay on GPIO 26 follows the relay value.

ESP32 signs in to Firebase Auth, writes latest and history, and polls a relay command
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>

const char* WIFI_SSID = "YourWiFi";
const char* WIFI_PASS = "YourWiFiPassword";
const char* API_KEY   = "YOUR_WEB_API_KEY";
const char* DB_URL    = "https://PROJECT-default-rtdb.firebaseio.com";
const char* EMAIL     = "node1@yourproject.dev";
const char* PASSWORD  = "device-password";
const int RELAY_PIN = 26;

DHT dht(4, DHT22);
String idToken, uid;
uint32_t tokenTime = 0, lastSend = 0, lastPoll = 0;

// Generic HTTPS request; returns status code and fills 'reply'
int request(const char* method, const String& url, const String& body, String& reply) {
  WiFiClientSecure client;
  client.setInsecure();               // demo only: no certificate check
  HTTPClient http;
  if (!http.begin(client, url)) return -1;
  http.addHeader("Content-Type", "application/json");
  int code = http.sendRequest(method, body);
  if (code > 0) reply = http.getString();
  http.end();
  return code;
}

bool signIn() {
  String url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=" + String(API_KEY);
  JsonDocument req;
  req["email"] = EMAIL;
  req["password"] = PASSWORD;
  req["returnSecureToken"] = true;
  String body, reply;
  serializeJson(req, body);
  if (request("POST", url, body, reply) != 200) {
    Serial.println("Sign-in failed: " + reply);
    return false;
  }
  JsonDocument res;
  deserializeJson(res, reply);
  idToken = res["idToken"].as<String>();
  uid     = res["localId"].as<String>();
  tokenTime = millis();
  Serial.println("Signed in, uid = " + uid);
  return true;
}

String path(const char* child) {      // https://.../devices/<uid>/<child>.json?auth=...
  return String(DB_URL) + "/devices/" + uid + "/" + child + ".json?auth=" + idToken;
}

void sendReading() {
  float t = dht.readTemperature(), h = dht.readHumidity();
  if (isnan(t) || isnan(h)) return;
  JsonDocument doc;
  doc["temp_c"] = t;
  doc["hum_pct"] = h;
  doc["ts"][".sv"] = "timestamp";     // server fills in the time
  String body, reply;
  serializeJson(doc, body);
  int c1 = request("PUT", path("latest"), body, reply);   // overwrite
  int c2 = request("POST", path("history"), body, reply); // append
  Serial.printf("latest %d, history %d", c1, c2);
  Serial.println();
}

void pollRelay() {
  String reply;
  if (request("GET", path("relay"), "", reply) == 200) {
    reply.trim();                     // body is true, false or null
    digitalWrite(RELAY_PIN, reply == "true" ? HIGH : LOW);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  dht.begin();
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  while (!signIn()) delay(10000);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); delay(2000); return; }
  if (millis() - tokenTime > 50UL * 60 * 1000) signIn();  // refresh before 1 h expiry
  if (millis() - lastSend > 60000) { lastSend = millis(); sendReading(); }
  if (millis() - lastPoll > 3000)  { lastPoll = millis(); pollRelay(); }
}

Understanding the Design

  • http.sendRequest(method, body) lets one helper send GET, PUT, POST or PATCH, which keeps the code short.
  • PUT on latest keeps one up-to-date record that apps can display, while POST on history builds a time-ordered log with unique keys generated by Firebase.
  • The UID in the path matches the security rule, so a leaked device password only exposes that one device's branch.
  • Polling every 3 seconds is simple but uses data on every poll. The REST API also supports streaming with Accept: text/event-stream, which pushes changes over a single open connection; it is more complex to parse on a microcontroller.
  • Relay test: in the console, add a key relay with value true under your device's UID. The relay should click within a few seconds.

Testing from a PC first

You can check the rules and the URL with curl before involving the ESP32. A request without a token must fail; a request with a valid token for that user must succeed. Get a token by calling the sign-in endpoint with curl and copying the idToken value from the reply.

Check the security rules with curl
# Should return 401 / Permission denied: no auth token
curl "https://PROJECT-default-rtdb.firebaseio.com/devices.json"

# Should return the device branch when the token belongs to that UID
curl "https://PROJECT-default-rtdb.firebaseio.com/devices/UID/latest.json?auth=ID_TOKEN"

If the first request returns data, your rules are open and must be fixed before you deploy anything.

Troubleshooting

  • 401 Permission denied: the token is missing or expired, or the path does not match the rules. Print the UID and check it is the same key you created in the console.
  • Sign-in returns 400 with OPERATION_NOT_ALLOWED: Email/Password sign-in is not enabled.
  • 404 or DNS errors: the database URL is wrong. Copy it exactly, including the region part for databases outside the US.
  • Heap runs low and requests start failing: each TLS session needs a large block of RAM. Avoid many large String concatenations and do not open two secure connections at the same time.
  • History grows forever: schedule a cleanup from a server or app, or keep only latest on the device side and log history elsewhere.

Summary

Using the REST API, an ESP32 can sign in to Firebase Authentication, write live and historical readings, and read commands, all with the standard HTTPClient. Per-user security rules keep each device in its own branch. Any app built with the Firebase SDK can now display the data and toggle the relay in real time.