Back to blog

IoT Fundamentals: Architecture, Protocols & Device Communication

iotmqttarchitectureprotocolstutorial
IoT Fundamentals: Architecture, Protocols & Device Communication

Welcome to Phase 1 of the IoT Patterns & Strategies Roadmap! This post builds the foundation you need before diving into specific protocols, data pipelines, or production patterns. We'll cover how IoT systems are structured, how devices communicate, and which technologies fit which use cases.

If you're a backend or full-stack developer, many IoT concepts will feel familiar — message queues, APIs, databases — but the constraints are fundamentally different. Devices have kilobytes of RAM, networks drop constantly, and your code might run for a decade without an update.

By the end of this post, you'll understand the complete IoT technology stack and be able to make informed decisions about architecture, connectivity, and protocols for any IoT project.

What You'll Learn

✅ Understand the four-layer IoT architecture model
✅ Identify different device types and their constraints
✅ Compare connectivity options (WiFi, BLE, LoRaWAN, Zigbee, Cellular, NB-IoT)
✅ Choose the right communication protocol (MQTT, CoAP, AMQP, HTTP, WebSocket)
✅ Implement publish-subscribe, request-response, and store-and-forward patterns
✅ Understand digital twins and device shadow patterns
✅ Distinguish between IoT and Industrial IoT (IIoT)


The Four-Layer IoT Architecture

Every IoT system, from a smart thermostat to an industrial plant, follows the same fundamental layered architecture:

Layer 1: Perception Layer (Devices & Sensors)

The perception layer is where IoT meets the physical world. Devices sense (temperature, motion, pressure, GPS location) and act (turn on a motor, open a valve, display a message).

Key constraints at this layer:

  • Memory: Microcontrollers (MCUs) have 64KB-512KB of RAM — not megabytes, not gigabytes
  • CPU: Clock speeds of 80-240MHz — thousands of times slower than your laptop
  • Power: Many devices run on batteries for months or years
  • Storage: No disk — firmware and data share a few megabytes of flash memory
// A typical sensor reading in an IoT device
interface SensorReading {
  deviceId: string;
  timestamp: number;       // Unix timestamp (saves bytes vs ISO string)
  type: "temperature" | "humidity" | "pressure" | "gps";
  value: number;
  unit: string;
  batteryLevel?: number;   // 0-100, critical for power management
}
 
// Example: temperature sensor reading
const reading: SensorReading = {
  deviceId: "greenhouse-sensor-042",
  timestamp: 1739520000,
  type: "temperature",
  value: 23.5,
  unit: "celsius",
  batteryLevel: 87,
};

Layer 2: Network Layer (Connectivity)

The network layer transports data between devices and the processing layer. Unlike web development where you assume reliable HTTP, IoT connectivity varies wildly:

ConnectivityRangeBandwidthPowerCostBest For
WiFi50mHigh (Mbps)HighLowIndoor, plugged-in devices
BLE10-30mLow (1Mbps)Very LowLowWearables, beacons
Zigbee10-100mLow (250Kbps)LowMediumSmart home mesh
LoRaWAN2-15kmVery Low (50Kbps)Very LowLowAgriculture, outdoor sensors
Cellular (4G/5G)UnlimitedHigh (Gbps)HighHighFleet tracking, video
NB-IoTUnlimitedLow (250Kbps)LowMediumMetering, asset tracking

Layer 3: Processing Layer (Edge & Cloud)

This is where raw sensor data becomes actionable intelligence. Processing happens at two levels:

Edge Processing — at or near the device:

  • Filter noise and invalid readings
  • Aggregate data before sending to cloud (send average, not every reading)
  • Run local ML models for real-time decisions
  • Buffer data during connectivity outages

Cloud Processing — in the data center:

  • Long-term storage in time-series databases
  • Complex analytics and ML training
  • Cross-device correlation
  • Dashboard and alerting systems

Layer 4: Application Layer

The top layer is where humans and other systems interact with IoT data:

  • Dashboards: Real-time visualization of device health and metrics (Grafana, custom UIs)
  • APIs: RESTful or GraphQL APIs for integrating IoT data into business applications
  • Alerting: Automated notifications when thresholds are crossed (temperature too high, device offline)
  • Analytics: Historical analysis, trend detection, predictive maintenance
  • Automation: Rules engines that trigger actions based on device data (if temp > 30°C, turn on fan)

Device Types and Constraints

Not all IoT devices are created equal. Understanding the spectrum helps you design appropriate communication strategies:

Device TypeRoleExamplesConstraints
SensorsCollect data from environmentTemperature, humidity, motion, GPS, pressureLow power, small payload, periodic reporting
ActuatorsPerform physical actionsMotors, valves, relays, LED displaysNeeds reliable command delivery, power for action
GatewaysBridge devices to cloudRaspberry Pi, industrial gatewaysMore powerful, always-on, protocol translation
Edge DevicesProcess data locallyNVIDIA Jetson, industrial PCsSignificant compute, ML inference, local storage
Constrained DevicesMinimal capabilitySoil sensors, door contacts, tagsKilobytes of RAM, years on a coin battery

The key insight: design your protocol and architecture for the most constrained device in your system, not the most powerful one.

// Different devices send very different payloads
// Constrained sensor: tiny payload, sent every 15 minutes
const constrainedPayload = {
  id: "s042",
  t: 23.5,       // abbreviated keys save bytes
  h: 67,
  bat: 87,
};
 
// Edge device: rich payload with local analytics
const edgePayload = {
  deviceId: "edge-gateway-01",
  timestamp: "2026-02-14T10:30:00Z",
  connectedDevices: 24,
  aggregatedReadings: {
    temperatureAvg: 23.2,
    temperatureMin: 21.1,
    temperatureMax: 25.8,
    humidityAvg: 65,
  },
  alerts: [
    { sensorId: "s017", type: "lowBattery", value: 12 },
  ],
  networkStats: {
    packetLoss: 0.02,
    latencyMs: 45,
  },
};

Connectivity Options

Choosing the right connectivity technology is one of the most important IoT architecture decisions. Here's a decision framework:

WiFi

Best for: Indoor devices with power supply (smart plugs, cameras, displays).

  • Pros: High bandwidth, ubiquitous, easy development
  • Cons: High power consumption, limited range, congestion in dense deployments
  • Typical use: Smart home, indoor asset tracking, factory HMIs

Bluetooth Low Energy (BLE)

Best for: Wearables, beacons, short-range sensor networks.

  • Pros: Very low power, built into smartphones, cheap modules
  • Cons: Short range (10-30m), low bandwidth, needs phone/gateway for cloud connectivity
  • Typical use: Fitness trackers, iBeacons, proximity sensors

LoRaWAN

Best for: Long-range, low-power outdoor deployments.

  • Pros: 2-15km range, years on a battery, good building penetration
  • Cons: Very low bandwidth (50Kbps), high latency (seconds), limited downlink
  • Typical use: Agriculture sensors, smart city infrastructure, water metering

Zigbee / Thread

Best for: Dense mesh networks in buildings.

  • Pros: Self-healing mesh, low power, 250+ devices per network
  • Cons: Limited range per hop, needs coordinator/border router
  • Typical use: Smart home lighting, HVAC control, building automation

Cellular (4G/5G)

Best for: Mobile assets and high-bandwidth applications.

  • Pros: Unlimited range, high bandwidth (especially 5G), reliable
  • Cons: High power, recurring data costs, module cost
  • Typical use: Fleet tracking, connected vehicles, video surveillance

NB-IoT (Narrowband IoT)

Best for: Stationary sensors that need cellular coverage with low power.

  • Pros: Deep indoor coverage, low power, carrier-grade reliability
  • Cons: Low bandwidth (250Kbps), higher latency than LTE, limited mobility support
  • Typical use: Smart metering, utility monitoring, asset tracking

Communication Protocols Overview

Once your device has connectivity, it needs a protocol to structure its messages. Here's how the major IoT protocols compare:

ProtocolTransportPatternQoSOverheadBest For
MQTTTCPPub/Sub0, 1, 2Very Low (2 byte header)Most IoT use cases
CoAPUDPRequest/ResponseConfirmable/Non-confVery LowConstrained devices
AMQPTCPPub/Sub + QueuesAt-most/at-least/exactly-onceMediumEnterprise IoT backends
HTTPTCPRequest/ResponseNone (app-level)High (headers)Cloud APIs, webhooks
WebSocketTCPFull-duplexNone (app-level)Low (after handshake)Real-time dashboards

MQTT — The IoT Standard

MQTT (Message Queuing Telemetry Transport) is the most widely used IoT protocol. It uses a publish-subscribe model where devices publish messages to topics and subscribers receive them through a broker.

import mqtt from "mqtt";
 
// Connect to an MQTT broker
const client = mqtt.connect("mqtt://broker.hivemq.com:1883");
 
// Publish sensor data to a topic
client.on("connect", () => {
  setInterval(() => {
    const payload = JSON.stringify({
      temperature: 22.5 + Math.random() * 5,
      humidity: 60 + Math.random() * 20,
      timestamp: Date.now(),
    });
 
    // Topic structure: {org}/{location}/{device-type}/{device-id}
    client.publish(
      "acme/greenhouse-1/sensors/temp-042",
      payload,
      { qos: 1 } // At-least-once delivery
    );
  }, 10000); // Every 10 seconds
});
// Subscribe to sensor data on the cloud side
const subscriber = mqtt.connect("mqtt://broker.hivemq.com:1883");
 
subscriber.on("connect", () => {
  // Wildcard '+' matches any single level
  subscriber.subscribe("acme/greenhouse-1/sensors/+");
});
 
subscriber.on("message", (topic, message) => {
  const data = JSON.parse(message.toString());
  console.log(`[${topic}] Temperature: ${data.temperature}°C`);
 
  // Alert if temperature exceeds threshold
  if (data.temperature > 35) {
    subscriber.publish("acme/greenhouse-1/alerts", JSON.stringify({
      type: "highTemperature",
      value: data.temperature,
      deviceTopic: topic,
      timestamp: Date.now(),
    }));
  }
});

CoAP — REST for Constrained Devices

CoAP (Constrained Application Protocol) brings the familiar REST model (GET, PUT, POST, DELETE) to devices that can't afford TCP overhead. It runs over UDP.

# CoAP GET request to read a sensor value
from coapthon.client.helperclient import HelperClient
 
client = HelperClient(server=("coap://sensor-042.local", 5683))
 
# GET /temperature — like HTTP but over UDP
response = client.get("temperature")
print(f"Temperature: {response.payload}")  # "23.5"
 
# PUT /actuator/fan — control a device
client.put("actuator/fan", payload="on")
 
# OBSERVE /temperature — like WebSocket but for CoAP
# Server pushes updates when value changes
def callback(response):
    print(f"Updated: {response.payload}")
 
client.observe("temperature", callback)

When to Use Which Protocol

ScenarioRecommended ProtocolWhy
Thousands of sensors reporting periodicallyMQTTEfficient pub/sub, low overhead, QoS levels
Battery-powered sensors with limited RAMCoAPUDP saves power, REST-like simplicity
Enterprise backend-to-backend messagingAMQPQueues, routing, transactional guarantees
Browser dashboard showing live dataWebSocket or MQTT-over-WebSocketFull-duplex real-time
Sending alerts to third-party webhooksHTTPUniversal compatibility
Firmware OTA updates (large files)HTTP/HTTPSReliable delivery, range requests

Message Patterns

IoT systems use four fundamental message patterns. Understanding when to use each is crucial:

Publish-Subscribe (Pub/Sub)

The dominant IoT pattern. Devices publish to topics; interested consumers subscribe. The broker handles routing.

When to use: Sensor data reporting, telemetry, event distribution — any scenario where multiple consumers might need the same data.

Request-Response

The familiar HTTP model. Client sends a request, server sends a response. Used in CoAP and HTTP-based IoT.

When to use: Device configuration, on-demand status checks, actuator commands that need acknowledgment.

Fire-and-Forget

The device sends a message and doesn't wait for confirmation. This is MQTT QoS 0 — fastest but least reliable.

When to use: High-frequency telemetry where occasional data loss is acceptable (e.g., sending GPS coordinates every second — missing one is fine).

Store-and-Forward

The device stores messages locally when connectivity is unavailable and forwards them when the connection is restored. Critical for IoT deployments with unreliable networks.

// Store-and-forward pattern for unreliable connectivity
class StoreAndForwardClient {
  private queue: SensorReading[] = [];
  private isConnected = false;
 
  constructor(private mqttClient: mqtt.MqttClient) {
    mqttClient.on("connect", () => {
      this.isConnected = true;
      this.flush(); // Send queued messages
    });
    mqttClient.on("offline", () => {
      this.isConnected = false;
    });
  }
 
  publish(reading: SensorReading): void {
    if (this.isConnected) {
      this.mqttClient.publish(
        `sensors/${reading.deviceId}`,
        JSON.stringify(reading),
        { qos: 1 }
      );
    } else {
      // Store locally when offline
      this.queue.push(reading);
      console.log(`Queued message (${this.queue.length} pending)`);
    }
  }
 
  private flush(): void {
    console.log(`Flushing ${this.queue.length} queued messages...`);
    while (this.queue.length > 0) {
      const reading = this.queue.shift()!;
      this.mqttClient.publish(
        `sensors/${reading.deviceId}`,
        JSON.stringify(reading),
        { qos: 1 }
      );
    }
  }
}

Pattern Selection Guide

PatternReliabilityLatencyPower UsageUse Case
Pub/SubMedium-High (QoS dependent)LowLowTelemetry, events, alerts
Request-ResponseHighMediumMediumConfig, commands, status
Fire-and-ForgetLowVery LowVery LowHigh-frequency GPS, metrics
Store-and-ForwardHighVariableMediumOffline-capable systems

Device-to-Cloud Communication

Devices reach the cloud through three main paths:

Path 1: Direct Connection

The device connects directly to the cloud broker or API. Simplest architecture, but requires the device to have internet connectivity (WiFi, Cellular).

Pros: Simple, low latency to cloud Cons: Requires always-on internet, no local processing, each device needs cloud credentials

Path 2: Via Gateway

Constrained devices (BLE sensors, Zigbee nodes) connect to a local gateway that bridges them to the cloud. The gateway handles protocol translation.

// Gateway: bridge BLE sensors to MQTT cloud
import mqtt from "mqtt";
 
const cloudClient = mqtt.connect("mqtts://iot.cloud-provider.com", {
  clientId: "gateway-01",
  username: "gateway-01",
  password: process.env.GATEWAY_TOKEN,
});
 
// Simulating BLE sensor data received by the gateway
function onBLESensorData(sensorId: string, data: Buffer) {
  const reading = parseBLEPayload(data); // Decode BLE advertisement
 
  // Translate and forward to cloud via MQTT
  cloudClient.publish(
    `devices/${sensorId}/telemetry`,
    JSON.stringify({
      ...reading,
      gatewayId: "gateway-01",
      receivedAt: Date.now(),
    }),
    { qos: 1 }
  );
}
 
function parseBLEPayload(data: Buffer): Record<string, number> {
  // BLE payloads are compact binary — decode to JSON
  return {
    temperature: data.readInt16LE(0) / 100,
    humidity: data.readUInt8(2),
    battery: data.readUInt8(3),
  };
}

Pros: Supports constrained devices, local protocol translation, centralized credentials Cons: Gateway is a single point of failure, added complexity

Path 3: Via Edge Node

The most sophisticated path. An edge node (small server, industrial PC) processes data locally before sending summaries to the cloud. This reduces bandwidth, latency, and cloud costs.

Pros: Local processing, offline resilience, reduced cloud costs Cons: Most complex, requires managing edge infrastructure


Digital Twins

A digital twin is a virtual representation of a physical device, maintained in the cloud. It mirrors the device's current state and desired configuration.

The key idea: applications interact with the twin, not the device. If the device is offline, the twin still holds the last known state, and any configuration changes are queued until the device reconnects.

// Digital twin: device shadow pattern
interface DeviceTwin {
  deviceId: string;
  // Reported state: what the device says it IS
  reported: {
    temperature: number;
    humidity: number;
    fanSpeed: "off" | "low" | "high";
    firmwareVersion: string;
    lastSeen: number;
  };
  // Desired state: what the application WANTS it to be
  desired: {
    fanSpeed: "off" | "low" | "high";
    reportingInterval: number; // seconds
    firmwareVersion: string;   // for OTA updates
  };
  // Metadata
  metadata: {
    lastReportedUpdate: number;
    lastDesiredUpdate: number;
  };
}
 
// Application sets desired state
function setFanSpeed(twin: DeviceTwin, speed: "off" | "low" | "high") {
  twin.desired.fanSpeed = speed;
  twin.metadata.lastDesiredUpdate = Date.now();
  // The platform delivers this to the device when it's online
}
 
// Device reports its current state
function onDeviceReport(twin: DeviceTwin, report: Partial<DeviceTwin["reported"]>) {
  Object.assign(twin.reported, report);
  twin.metadata.lastReportedUpdate = Date.now();
 
  // Check if device has caught up with desired state
  if (twin.reported.fanSpeed !== twin.desired.fanSpeed) {
    console.log("Device hasn't applied desired fan speed yet");
  }
}

Digital Twin Use Cases

  • Smart home: Set desired thermostat temperature; device applies when it wakes up
  • Fleet management: Track last known GPS location even when vehicle is in a tunnel
  • Industrial: Monitor machine state, queue maintenance commands for next service window
  • OTA updates: Set desired firmware version; device downloads when convenient

AWS IoT Core calls this "Device Shadow." Azure IoT Hub calls it "Device Twin." The pattern is the same.


IoT vs IIoT (Industrial IoT)

While the architecture is similar, Industrial IoT has unique requirements:

AspectIoT (Consumer/Commercial)IIoT (Industrial)
ScaleThousands to millions of devicesHundreds to thousands, but mission-critical
ReliabilityBest-effort acceptable99.999% uptime required
SafetyMinor inconvenience on failurePhysical danger, regulatory compliance
LatencySeconds acceptableMilliseconds required (real-time control)
Device lifetime2-5 years10-30 years
ProtocolsMQTT, HTTP, BLEOPC-UA, MQTT Sparkplug B, Modbus, PROFINET
StandardsVaries by marketIEC 62443, ISA-95, ISO 27001
NetworkInternet/WiFiAir-gapped networks, dedicated industrial networks
SecurityImportantCritical (physical safety implications)
Data ownershipCloud-firstOn-premise first, selective cloud

Key takeaway: IIoT adds strict requirements for safety, reliability, and real-time control that consumer IoT rarely needs. If you're building for factories or utilities, study OPC-UA and industrial safety standards.


Putting It All Together: Smart Greenhouse

Let's apply everything we've learned to design an IoT system for a smart greenhouse:

Requirements

  • Monitor temperature, humidity, soil moisture, and light levels
  • Automatically control fans, irrigation, and shade screens
  • Send alerts when conditions are out of range
  • Work even when internet connectivity is intermittent
  • Support 50 sensor nodes across a 2,000m² greenhouse

Architecture Decision

Decisions Made

DecisionChoiceReasoning
ConnectivityZigbeeLow power, mesh networking covers 2,000m², 50+ devices per network
ProtocolMQTT (to cloud), Zigbee (local)MQTT for cloud telemetry, Zigbee for device-to-gateway
ProcessingEdge-firstFans and irrigation must respond in seconds, can't wait for cloud round-trip
Message patternStore-and-forwardInternet may be unreliable in rural locations
Device twinYes, for actuator controlSet desired fan/irrigation state from dashboard; edge applies locally
// Edge node: local rules engine for the greenhouse
interface GreenhouseState {
  zones: Map<string, ZoneState>;
}
 
interface ZoneState {
  temperature: number;
  humidity: number;
  soilMoisture: number;
  light: number;
  fanSpeed: "off" | "low" | "high";
  irrigating: boolean;
}
 
function evaluateRules(zone: ZoneState): Action[] {
  const actions: Action[] = [];
 
  // Rule 1: Temperature control
  if (zone.temperature > 30) {
    actions.push({ type: "setFan", value: "high" });
  } else if (zone.temperature > 26) {
    actions.push({ type: "setFan", value: "low" });
  } else {
    actions.push({ type: "setFan", value: "off" });
  }
 
  // Rule 2: Irrigation control
  if (zone.soilMoisture < 30 && !zone.irrigating) {
    actions.push({ type: "startIrrigation", durationMinutes: 15 });
  }
 
  return actions;
}
 
type Action =
  | { type: "setFan"; value: "off" | "low" | "high" }
  | { type: "startIrrigation"; durationMinutes: number };

Common Beginner Mistakes

Mistake 1: Using HTTP for Everything

// ❌ BAD: Polling HTTP every 5 seconds from 1,000 devices
setInterval(async () => {
  const response = await fetch("https://api.example.com/telemetry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(sensorData),
  });
  // Each request: TCP handshake + TLS + headers = ~1KB overhead
  // 1,000 devices × 12 requests/min = 12,000 HTTP requests/min
}, 5000);
// ✅ GOOD: Use MQTT with persistent connection
const client = mqtt.connect("mqtt://broker.example.com");
// One TCP connection, kept alive. Publish overhead: ~2 bytes
client.publish("sensors/temp-042", JSON.stringify(sensorData), { qos: 1 });
// 1,000 devices × 1 persistent connection = 1,000 TCP connections

HTTP creates a new connection per request (or keeps one alive but adds massive header overhead). MQTT maintains a single persistent connection with a 2-byte minimum header.

Mistake 2: Sending Full JSON When Bytes Matter

// ❌ BAD: Verbose JSON payload (180 bytes)
{
  "deviceIdentifier": "greenhouse-sensor-042",
  "readingType": "temperature",
  "readingValue": 23.5,
  "readingUnit": "celsius",
  "readingTimestamp": "2026-02-14T10:30:00.000Z"
}
// ✅ GOOD: Compact payload (28 bytes) for constrained devices
{
  "id": "s042",
  "t": 23.5,
  "ts": 1739520600
}
// Or even better: binary protocol like CBOR or Protobuf

On LoRaWAN with a 51-byte payload limit, verbose JSON literally doesn't fit. Use short keys, Unix timestamps, and consider binary formats.

Mistake 3: No Offline Strategy

// ❌ BAD: Drop data when offline
function publishReading(data: SensorReading) {
  if (!client.connected) {
    console.log("Offline, discarding reading");
    return; // Data lost forever!
  }
  client.publish(`sensors/${data.deviceId}`, JSON.stringify(data));
}
// ✅ GOOD: Queue and retry with store-and-forward
function publishReading(data: SensorReading) {
  if (!client.connected) {
    offlineQueue.push(data);
    return;
  }
  client.publish(`sensors/${data.deviceId}`, JSON.stringify(data), { qos: 1 });
}
 
client.on("connect", () => {
  while (offlineQueue.length > 0) {
    const data = offlineQueue.shift()!;
    client.publish(`sensors/${data.deviceId}`, JSON.stringify(data), { qos: 1 });
  }
});

IoT devices disconnect regularly. Always design for intermittent connectivity.

Mistake 4: Same Architecture for All Device Types

// ❌ BAD: Treating a coin-battery sensor like a cloud server
// Don't make constrained devices do TLS handshakes every 10 seconds
// Don't require JSON parsing on 64KB RAM devices
// Don't expect reliable connectivity from outdoor LoRaWAN sensors
 
// ✅ GOOD: Match architecture to device capability
// Constrained → CoAP/UDP, binary payloads, gateway-assisted
// Mid-range → MQTT, JSON, direct connection
// Powerful → HTTPS, rich JSON, local processing

Summary and Key Takeaways

The four-layer IoT architecture (Perception, Network, Processing, Application) applies to every IoT system
Design for the most constrained device in your system — not the most powerful
MQTT is the default protocol for most IoT use cases — lightweight, pub/sub, QoS levels
Connectivity choice depends on range, power, and bandwidth — WiFi for indoor, LoRaWAN for outdoor, Cellular for mobile
Store-and-forward is essential — IoT devices disconnect regularly, never drop data silently
Digital twins decouple applications from devices — always read/write the twin, not the device directly
Edge processing reduces latency and cloud costs — filter and aggregate data before sending to cloud
IIoT adds safety and real-time requirements that consumer IoT rarely needs
Choose compact payloads for constrained devices — short keys, binary formats, Unix timestamps


What's Next

Now that you understand IoT architecture and fundamentals, it's time to go deeper into the protocols that power device communication.

Next: IOT-3: IoT Communication Protocols (MQTT, CoAP, AMQP, WebSocket) — Deep dive into each protocol with hands-on setup, configuration, and practical examples using Mosquitto, RabbitMQ, and HiveMQ.



This is Part 2 of the IoT Patterns & Strategies series. Start from the beginning or jump to any topic that interests you.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.