<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ mqtt - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ mqtt - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 27 Jul 2026 20:15:18 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/mqtt/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Real-Time Update Systems with MQTT and Express.js  ]]>
                </title>
                <description>
                    <![CDATA[ Real-time updates are everywhere – like live sports scores, stock tickers, chat applications, and IoT dashboards. If you want to build systems that push data to users the moment it changes, you need t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-real-time-update-systems-with-mosquitto-and-expressjs/</link>
                <guid isPermaLink="false">69b04145abc0d950017e4629</guid>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mqtt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web application ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Tue, 10 Mar 2026 16:05:25 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f39f54d7-d39b-46aa-9046-9a88315369d4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Real-time updates are everywhere – like live sports scores, stock tickers, chat applications, and IoT dashboards. If you want to build systems that push data to users the moment it changes, you need the right tools.</p>
<p>Message Queuing Telemetry Transport (MQTT) is a lightweight messaging protocol that excels at this. Combined with a broker like Mosquitto and a web framework like Express, you can build a production-ready real-time system in a single afternoon.</p>
<p>In this tutorial, you'll build a complete real-time football (soccer) sports update system from scratch. You'll create an admin interface for uploading scores and match details, a viewer interface for watching live updates, and a backend that uses MQTT to broadcast changes instantly to every connected client.</p>
<p>By the end of this guide, you'll understand how to integrate MQTT with Express, set up the Mosquitto broker, and deliver real-time data to web browsers using Server-Sent Events. You will have a working system that you can extend for production use.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-understanding-the-architecture">Understanding the Architecture</a></p>
</li>
<li><p><a href="#heading-what-is-mqtt-and-why-use-it">What is MQTT and Why Use It?</a></p>
<ul>
<li><p><a href="#heading-mqtt-topic-design">MQTT Topic Design</a></p>
</li>
<li><p><a href="#heading-why-serversent-events-instead-of-websockets">Why Server-Sent Events Instead of WebSockets?</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-mqtt-broker">How to Set Up the MQTT Broker</a></p>
<ul>
<li><p><a href="#heading-option-1-docker-recommended">Option 1: Docker (Recommended)</a></p>
</li>
<li><p><a href="#heading-option-2-local-install">Option 2: Local Install</a></p>
</li>
<li><p><a href="#heading-option-3-public-test-broker">Option 3: Public Test Broker</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-express-server">How to Build the Express Server</a></p>
</li>
<li><p><a href="#heading-how-to-implement-the-match-routes">How to Implement the Match Routes</a></p>
</li>
<li><p><a href="#heading-how-to-bridge-mqtt-to-the-browser-with-serversent-events">How to Bridge MQTT to the Browser with Server-Sent Events</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-admin-upload-interface">How to Build the Admin Upload Interface</a></p>
<ul>
<li><p><a href="#heading-admin-html-structure-and-styles">Admin HTML Structure and Styles</a></p>
</li>
<li><p><a href="#heading-admin-javascript-logic">Admin JavaScript Logic</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-live-viewer-interface">How to Build the Live Viewer Interface</a></p>
<ul>
<li><a href="#heading-viewer-javascript-logic">Viewer JavaScript Logic</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-home-page">How to Build the Home Page</a></p>
</li>
<li><p><a href="#heading-how-to-run-and-test-the-system">How to Run and Test the System</a></p>
</li>
<li><p><a href="#heading-how-to-extend-the-system-for-production">How to Extend the System for Production</a></p>
</li>
<li><p><a href="#heading-api-reference">API Reference</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<p>During this tutorial, you'll learn how to:</p>
<ul>
<li><p>Connect an Express server to an MQTT broker using the MQTT.js library</p>
</li>
<li><p>Publish and subscribe to MQTT topics for real-time messaging</p>
</li>
<li><p>Use Server-Sent Events to push MQTT messages to web browsers</p>
</li>
<li><p>Build a REST application programming interface (API) for match and score management</p>
</li>
<li><p>Create a simple admin interface for uploading match data</p>
</li>
<li><p>Create a viewer interface that updates in real time without page refreshes</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, you should have:</p>
<ul>
<li><p>Node.js version 18 or later installed on your machine</p>
</li>
<li><p>Basic familiarity with JavaScript, Express, and HTML</p>
</li>
<li><p>A terminal or command line for running commands</p>
</li>
<li><p>Docker installed (optional, for running Mosquitto in a container)</p>
</li>
</ul>
<p>If you don't have Node.js installed, you can download it from the official Node.js website.</p>
<h2 id="heading-understanding-the-architecture">Understanding the Architecture</h2>
<p>The system has three main parts:</p>
<ol>
<li><p><strong>Admin interface</strong> – A web page where you create matches, update scores, and add events such as goals and cards.</p>
</li>
<li><p><strong>Express server</strong> – Receives HyperText Transfer Protocol (HTTP) requests from the admin, publishes data to MQTT, subscribes to MQTT topics, and streams updates to viewers via Server-Sent Events.</p>
</li>
<li><p><strong>Viewer interface</strong> – A web page that connects to the server and displays live scores and events as they arrive.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/e6d0b007-764d-41bf-8d05-9e37971bd78e.png" alt="Architecture-diagram" style="display:block;margin:0 auto" width="2100" height="890" loading="lazy">

<p><strong>How the flow works</strong><br>When you submit a score update in the admin panel, the Express server publishes a message to the MQTT broker. The server also subscribes to those same topics. When a message arrives, the server forwards it to all connected viewers through Server-Sent Events. The viewers update their display without refreshing the page.</p>
<h2 id="heading-what-is-mqtt-and-why-use-it">What is MQTT and Why Use It?</h2>
<p>MQTT stands for Message Queuing Telemetry Transport. It's a lightweight, publish-subscribe messaging protocol designed for low bandwidth and unreliable networks. It's widely used in Internet of Things (IoT) applications, but it works well for any real-time system where you need to broadcast updates to many subscribers.</p>
<p>Here are some reasons to use MQTT for a sports update system:</p>
<ul>
<li><p><strong>Low overhead</strong>: Messages are small and efficient, which helps when you have many clients.</p>
</li>
<li><p><strong>Built-in Quality of Service (QoS)</strong>: You can choose how many times a message is delivered (at most once, at least once, or exactly once).</p>
</li>
<li><p><strong>Topic-based routing</strong>: You organize messages by topic (for example, <code>sports/football/match/123</code>) so subscribers receive only what they need.</p>
</li>
<li><p><strong>Broker-based</strong>: A central broker (Mosquitto) handles all message distribution, so your application logic stays simple.</p>
</li>
</ul>
<p>Mosquitto is a popular, open-source MQTT broker that's easy to install and configure.</p>
<h3 id="heading-mqtt-topic-design">MQTT Topic Design</h3>
<p>MQTT uses a hierarchical topic structure. For this project, the topics are:</p>
<ul>
<li><p><code>sports/football/match/{id}</code>: One topic per match. When you publish the full match object here, any subscriber receives the complete state. This makes it easy to add new match fields later without changing the topic structure.</p>
</li>
<li><p><code>sports/football/scores</code>: A single topic for score-related notifications. Messages include a <code>type</code> field (<code>match_created</code> or <code>score_update</code>) so subscribers can handle them differently.</p>
</li>
<li><p><code>sports/football/events</code>: A topic for match events such as goals and cards. Subscribers receive <code>{ type: 'match_event', matchId, event }</code>.</p>
</li>
</ul>
<p>The <code>#</code> wildcard in a subscription means "match this level and all levels below." So <code>sports/football/#</code> subscribes to every topic under <code>sports/football</code>, including <code>sports/football/match/abc123</code> and <code>sports/football/scores</code>. The <code>+</code> wildcard matches exactly one level. For example, <code>sports/+/match/#</code> would match any sport, not just football.</p>
<h3 id="heading-why-server-sent-events-instead-of-websockets">Why Server-Sent Events Instead of WebSockets?</h3>
<p>You might wonder why this tutorial uses Server-Sent Events (SSE) instead of WebSockets. Both can push data to the browser. The main difference:</p>
<ul>
<li><p><strong>Server-Sent Events</strong>: One-way (server to client). Built on HTTP. Automatic reconnection in the browser. Simpler to implement. No extra libraries.</p>
</li>
<li><p><strong>WebSockets</strong>: Two-way. Requires a different protocol. More flexible but more complex.</p>
</li>
</ul>
<p>For a sports score viewer, you only need server-to-client updates. The viewer never sends messages back over the same channel. Server-Sent Events is a better fit. If you later need the client to send commands (for example, to filter by league), you can add a separate HTTP API or switch to WebSockets.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new folder for your project and initialize it with npm. The <code>mkdir</code> command creates the directory, <code>cd</code> moves into it, and <code>npm init -y</code> creates a <code>package.json</code> file with default values without prompting you for input.</p>
<pre><code class="language-shell">mkdir mqtt-football-scores
cd mqtt-football-scores
npm init -y
</code></pre>
<p>Install the required dependencies. Each package serves a specific role in the application. Run this command in the project root directory.</p>
<pre><code class="language-shell">npm install express cors mqtt uuid
</code></pre>
<ul>
<li><p><code>express</code>: Web framework for the HTTP server and API. It provides routing, middleware, and static file serving.</p>
</li>
<li><p><code>cors</code>: Enables Cross-Origin Resource Sharing so your frontend can call the API from a different origin (for example, if you serve the HTML from a different port or domain).</p>
</li>
<li><p><code>mqtt</code>: MQTT client for Node.js. It handles connection, publish, subscribe, reconnection, and Quality of Service (QoS) flows.</p>
</li>
<li><p><code>uuid</code>: Generates unique identifiers (Universally Unique Identifiers) for matches and events. Each ID is practically unique across all systems.</p>
</li>
</ul>
<p>Next, create the following folder structure. The <code>server</code> folder holds the Node.js backend code. The <code>public</code> folder holds the HTML, CSS, and client-side JavaScript that the browser loads. The <code>routes</code> subfolder keeps the match-related route handlers separate from the main server file.</p>
<pre><code class="language-plaintext">mqtt-football-scores/
├── server/
│   ├── index.js
│   ├── sse.js
│   └── routes/
│       └── matches.js
├── public/
│   ├── index.html
│   ├── admin.html
│   └── viewer.html
├── mosquitto.conf
├── docker-compose.yml
└── package.json
</code></pre>
<p>Add <code>"type": "module"</code> to your <code>package.json</code> so you can use JavaScript modules (import and export). The <code>type</code> field tells Node.js to treat <code>.js</code> files as ES modules, which allows you to use <code>import</code> and <code>export</code> syntax instead of CommonJS <code>require</code> and <code>module.exports</code>.</p>
<pre><code class="language-json">{
  "name": "mqtt-football-scores",
  "version": "1.0.0",
  "type": "module",
  "main": "server/index.js"
}
</code></pre>
<h2 id="heading-how-to-set-up-the-mqtt-broker">How to Set Up the MQTT Broker</h2>
<p>You need an MQTT broker running before the server can connect. You have three options.</p>
<h3 id="heading-option-1-docker-recommended">Option 1: Docker (Recommended)</h3>
<p>Create a <code>docker-compose.yml</code> file. This file defines a single service named <code>mosquitto</code> that runs the Eclipse Mosquitto 2 image. The <code>ports</code> directive maps port 1883 on your host to port 1883 in the container so your Express server can connect. The <code>volumes</code> directive mounts your local <code>mosquitto.conf</code> into the container so the broker uses your configuration. The <code>restart: unless-stopped</code> option ensures the container restarts automatically if it crashes or if you reboot your machine.</p>
<pre><code class="language-yaml">version: "3.8"

services:
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mqtt-football-mosquitto
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
    restart: unless-stopped
</code></pre>
<p>Create a <code>mosquitto.conf</code> file. The <code>listener 1883</code> directive tells Mosquitto to listen on port 1883 for MQTT connections. The <code>protocol mqtt</code> specifies the standard MQTT protocol (as opposed to WebSocket). The <code>allow_anonymous true</code> setting permits connections without a username and password, which is fine for local development but should be disabled in production. The <code>log_dest stdout</code> and <code>log_type all</code> directives send all log output to the console so you can debug connection issues.</p>
<pre><code class="language-plaintext">listener 1883
protocol mqtt
allow_anonymous true
log_dest stdout
log_type all
</code></pre>
<p>Start the broker:</p>
<pre><code class="language-bash">docker-compose up -d
</code></pre>
<h3 id="heading-option-2-local-install">Option 2: Local Install</h3>
<p>On macOS with Homebrew:</p>
<pre><code class="language-bash">brew install mosquitto
mosquitto -c mosquitto.conf
</code></pre>
<p>On Ubuntu or Debian:</p>
<pre><code class="language-bash">sudo apt install mosquitto mosquitto-clients
sudo systemctl start mosquitto
</code></pre>
<h3 id="heading-option-3-public-test-broker">Option 3: Public Test Broker</h3>
<p>You can use the public test broker at <code>test.mosquitto.org</code> without installing anything. Set the environment variable when you start the server:</p>
<pre><code class="language-bash">MQTT_BROKER=mqtt://test.mosquitto.org npm start
</code></pre>
<p>Note: The public broker is shared and not suitable for production. Use it only for development and testing.</p>
<h2 id="heading-how-to-build-the-express-server">How to Build the Express Server</h2>
<p>In this step, you'll create the main server that powers the real-time application. The Express server handles HTTP requests, serves static files like HTML and JavaScript, and acts as the bridge between the browser and the MQTT broker. It also provides endpoints that allow data to be sent and received in real time. Essentially, this server is the backbone of the application, enabling communication between all components.</p>
<p>Begin by creating the main server file at <code>server/index.js</code>:</p>
<pre><code class="language-typescript">import express from 'express';
import cors from 'cors';
import mqtt from 'mqtt';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { v4 as uuidv4 } from 'uuid';

import { matchRoutes } from './routes/matches.js';
import { setupSSE, addSSEClient } from './sse.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const MQTT_BROKER = process.env.MQTT_BROKER || 'mqtt://localhost:1883';
const PORT = process.env.PORT || 3000;

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(join(__dirname, '../public')));

let mqttClient = null;

function connectMQTT() {
  mqttClient = mqtt.connect(MQTT_BROKER, {
    clientId: `football-scores-${uuidv4().slice(0, 8)}`,
    reconnectPeriod: 3000,
    connectTimeout: 10000,
  });

  mqttClient.on('connect', () =&gt; {
    console.log('Connected to MQTT broker at', MQTT_BROKER);
    mqttClient.subscribe('sports/football/#', { qos: 1 }, (err) =&gt; {
      if (err) console.error('Subscribe error:', err);
    });
  });

  mqttClient.on('error', (err) =&gt; {
    console.error('MQTT error:', err.message);
  });

  mqttClient.on('close', () =&gt; {
    console.log('MQTT connection closed');
  });

  mqttClient.on('reconnect', () =&gt; {
    console.log('MQTT reconnecting...');
  });

  return mqttClient;
}

const mqttClientInstance = connectMQTT();
const { publishMatch, publishScoreUpdate, publishEvent, getMatches } = matchRoutes(mqttClientInstance);
setupSSE(mqttClientInstance);

app.get('/api/events', (req, res) =&gt; addSSEClient(res));
app.post('/api/matches', publishMatch);
app.patch('/api/matches/:id/score', publishScoreUpdate);
app.post('/api/matches/:id/events', publishEvent);
app.get('/api/matches', getMatches);

app.listen(PORT, () =&gt; {
  console.log(`Football Scores Server running at http://localhost:${PORT}`);
  console.log(`Admin (upload):  http://localhost:${PORT}/admin.html`);
  console.log(`Viewer:          http://localhost:${PORT}/viewer.html`);
});
</code></pre>
<p>Here is what each part does:</p>
<ul>
<li><p><strong>Imports</strong>: The <code>fileURLToPath</code> and <code>dirname</code> utilities replicate the <code>__dirname</code> variable that CommonJS provides, since ES modules don't have it. You need <code>__dirname</code> to build the path to the <code>public</code> folder.</p>
</li>
<li><p><strong>Environment variables</strong>: <code>MQTT_BROKER</code> defaults to <code>mqtt://localhost:1883</code> so the server connects to a local Mosquitto instance. You can override it for Docker or a remote broker. <code>PORT</code> defaults to 3000.</p>
</li>
<li><p><strong>Middleware</strong>: <code>cors()</code> allows requests from any origin, which is useful during development. <code>express.json()</code> parses JSON request bodies. <code>express.static()</code> serves files from <code>public</code> so <code>/admin.html</code> and <code>/viewer.html</code> are available.</p>
</li>
<li><p><strong>connectMQTT</strong>: Creates an MQTT client with a unique client ID (required by the broker), a 3-second reconnect interval, and a 10-second connection timeout. On connect, it subscribes to <code>sports/football/#</code> with QoS 1. The <code>#</code> wildcard means "all topics under sports/football."</p>
</li>
<li><p><strong>matchRoutes</strong>: Returns the route handlers that create matches, update scores, and add events. Each handler publishes to MQTT and responds with JSON.</p>
</li>
<li><p><strong>setupSSE</strong>: Registers a listener on the MQTT client's <code>message</code> event. When a message arrives, it forwards the payload to all connected Server-Sent Events clients.</p>
</li>
<li><p><strong>addSSEClient</strong>: Called when a viewer opens <code>/api/events</code>. It sets the response headers for Server-Sent Events, flushes the headers so the connection stays open, and adds the response object to a set of active clients.</p>
</li>
<li><p><strong>Routes</strong>: The GET <code>/api/events</code> route establishes the Server-Sent Events stream. The POST, PATCH, and GET routes for matches delegate to the handlers from <code>matchRoutes</code>.</p>
</li>
</ul>
<p>The server serves static files from the <code>public</code> folder, so your HTML pages are available at the root.</p>
<h2 id="heading-how-to-implement-the-match-routes">How to Implement the Match Routes</h2>
<p>In this step, you'll create the route handlers that manage matches, scores, and events. These routes allow the server to receive requests from the admin interface, update match data, and publish real-time updates to MQTT. They also store match information in memory so it can be retrieved and updated during the session.</p>
<p>Begin by creating the file at <code>server/routes/matches.js</code>:</p>
<pre><code class="language-javascript">import { v4 as uuidv4 } from 'uuid';

const TOPIC_MATCH = 'sports/football/match';
const TOPIC_SCORES = 'sports/football/scores';
const TOPIC_EVENTS = 'sports/football/events';

const matches = new Map();

function publish(client, topic, payload, qos = 1) {
  if (!client?.connected) {
    console.warn('MQTT not connected, message not published');
    return false;
  }
  client.publish(topic, JSON.stringify(payload), { qos, retain: false });
  return true;
}

export function matchRoutes(mqttClient) {
  return {
    publishMatch: (req, res) =&gt; {
      const { homeTeam, awayTeam, league, venue, kickoff } = req.body;
      if (!homeTeam || !awayTeam) {
        return res.status(400).json({ error: 'homeTeam and awayTeam are required' });
      }

      const match = {
        id: uuidv4(),
        homeTeam,
        awayTeam,
        homeScore: 0,
        awayScore: 0,
        league: league || 'Premier League',
        venue: venue || 'TBD',
        kickoff: kickoff || new Date().toISOString(),
        status: 'scheduled',
        minute: 0,
        events: [],
        createdAt: new Date().toISOString(),
      };

      matches.set(match.id, match);

      const topic = `\({TOPIC_MATCH}/\){match.id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_SCORES, { type: 'match_created', match });

      res.status(201).json(match);
    },

    publishScoreUpdate: (req, res) =&gt; {
      const { id } = req.params;
      const { homeScore, awayScore, minute, status } = req.body;

      const match = matches.get(id);
      if (!match) {
        return res.status(404).json({ error: 'Match not found' });
      }

      if (homeScore !== undefined) match.homeScore = homeScore;
      if (awayScore !== undefined) match.awayScore = awayScore;
      if (minute !== undefined) match.minute = minute;
      if (status !== undefined) match.status = status;

      const topic = `\({TOPIC_MATCH}/\){id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_SCORES, {
        type: 'score_update',
        matchId: id,
        homeScore: match.homeScore,
        awayScore: match.awayScore,
        minute: match.minute,
        status: match.status,
      });

      res.json(match);
    },

    publishEvent: (req, res) =&gt; {
      const { id } = req.params;
      const { type, team, player, minute, description } = req.body;

      const match = matches.get(id);
      if (!match) {
        return res.status(404).json({ error: 'Match not found' });
      }

      const event = {
        id: uuidv4().slice(0, 8),
        type: type || 'goal',
        team,
        player: player || 'Unknown',
        minute: minute ?? match.minute,
        description: description || `\({type}: \){player}`,
        timestamp: new Date().toISOString(),
      };

      match.events.push(event);
      if (type === 'goal') {
        if (team === match.homeTeam) match.homeScore++;
        else if (team === match.awayTeam) match.awayScore++;
      }

      const topic = `\({TOPIC_MATCH}/\){id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_EVENTS, { type: 'match_event', matchId: id, event });

      res.status(201).json({ match, event });
    },

    getMatches: (req, res) =&gt; {
      const list = Array.from(matches.values()).sort(
        (a, b) =&gt; new Date(b.createdAt) - new Date(a.createdAt)
      );
      res.json(list);
    },
  };
}
</code></pre>
<p>The routes use an in-memory <code>Map</code> to store matches. In production, you would replace this with a database such as PostgreSQL or MongoDB.</p>
<p>Explanation of the key logic:</p>
<ul>
<li><p><strong>publish</strong>: A helper that checks if the MQTT client is connected before publishing. If the broker is down, it logs a warning instead of throwing. The <code>retain: false</code> option means the broker doesn't store the last message for new subscribers.</p>
</li>
<li><p><strong>publishMatch</strong>: Validates that <code>homeTeam</code> and <code>awayTeam</code> are present. Creates a match object with default values for league, venue, kickoff, status, and events. Stores it in the Map, publishes to the match-specific topic and the scores topic, and returns the match with status 201 (Created).</p>
</li>
<li><p><strong>publishScoreUpdate</strong>: Looks up the match by ID. If not found, returns 404. Updates only the fields that are provided (using <code>!== undefined</code> so you can set a score to 0). Publishes the full match and a score_update notification.</p>
</li>
<li><p><strong>publishEvent</strong>: Creates an event object with a short unique ID. Pushes it to the match's events array. If the event type is <code>goal</code>, increments the home or away score based on the team. Publishes the updated match and an event notification.</p>
</li>
<li><p><strong>getMatches</strong>: Converts the Map to an array, sorts by <code>createdAt</code> descending (newest first), and returns the list as JSON.</p>
</li>
</ul>
<p>MQTT topics used:</p>
<ul>
<li><p><code>sports/football/match/{id}</code>: Full match state. Used when a match is created or updated.</p>
</li>
<li><p><code>sports/football/scores</code>: Score change notifications. Used for match creation and score updates.</p>
</li>
<li><p><code>sports/football/events</code>: Match events such as goals and cards.</p>
</li>
</ul>
<p>The <code>publish</code> function sends JavaScript Object Notation (JSON) payloads with QoS 1 (at least once delivery). MQTT defines three QoS levels: 0 (at most once), 1 (at least once), and 2 (exactly once). Level 1 ensures the broker will retry until the subscriber acknowledges the message, which reduces the chance of losing score updates if the connection drops briefly.</p>
<h2 id="heading-how-to-bridge-mqtt-to-the-browser-with-server-sent-events">How to Bridge MQTT to the Browser with Server-Sent Events</h2>
<p>In this step, you'll create the Server-Sent Events (SSE) module that bridges MQTT messages to the browser. MQTT works over TCP and browsers cannot connect to it directly, so we use SSE as a lightweight HTTP-based streaming channel. SSE keeps an open connection and allows the server to push updates to connected browsers in real time. This is what enables viewers to see match updates instantly without refreshing.</p>
<p>Now create the file at <code>server/sse.js</code>:</p>
<pre><code class="language-javascript">const clients = new Set();

export function setupSSE(mqttClient) {
  if (!mqttClient) return;

  mqttClient.on('message', (topic, message) =&gt; {
    try {
      const payload = JSON.parse(message.toString());
      const data = JSON.stringify({ topic, ...payload });
      clients.forEach((res) =&gt; {
        try {
          res.write(`data: ${data}\n\n`);
        } catch (e) {
          clients.delete(res);
        }
      });
    } catch (e) {
      console.error('SSE parse error:', e.message);
    }
  });
}

export function addSSEClient(res) {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

  clients.add(res);

  res.on('close', () =&gt; {
    clients.delete(res);
  });
}
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><strong>clients Set</strong>: A Set holds all active Server-Sent Events response objects. Using a Set makes it easy to add and remove clients without duplicates.</p>
</li>
<li><p><strong>setupSSE</strong>: Attaches a <code>message</code> listener to the MQTT client. When any message arrives on a subscribed topic, the callback runs. It parses the message payload (which is JSON), merges the topic into the payload with <code>{ topic, ...payload }</code>, and sends the result to every client. The Server-Sent Events format requires each message to be <code>data: {content}\n\n</code> (two newlines). The <code>forEach</code> loop catches write errors (for example, if a client disconnected) and removes the client from the set.</p>
</li>
<li><p><strong>addSSEClient</strong>: Sets the <code>Content-Type</code> header to <code>text/event-stream</code> so the browser treats the response as an event stream. The <code>Cache-Control: no-cache</code> and <code>Connection: keep-alive</code> headers prevent the browser or proxy from caching or closing the connection. The <code>X-Accel-Buffering: no</code> header disables buffering in Nginx, which can delay or block Server-Sent Events. The <code>flushHeaders</code> call sends the headers immediately so the connection is established. The <code>close</code> event handler removes the client when the client disconnects (closes the tab or navigates away).</p>
</li>
</ul>
<p>Server-Sent Events is one-way (server to client). For this use case, that is enough because viewers only need to receive updates, not send messages back over the same channel.</p>
<h2 id="heading-how-to-build-the-admin-upload-interface">How to Build the Admin Upload Interface</h2>
<p>The admin interface is a single HTML page where match creators can create new matches, update scores, and add events such as goals or cards. It uses standard HTML forms so data can be submitted to the server, and JavaScript will later handle form submissions and dynamic updates. All the markup, styles, and script live in <code>public/admin.html</code>, which the Express server serves as a static page.</p>
<h3 id="heading-admin-html-structure-and-styles">Admin HTML Structure and Styles</h3>
<p>The document starts with the standard HTML5 boilerplate. The <code>charset</code> and <code>viewport</code> meta tags ensure proper character encoding and responsive layout on mobile devices. The page loads the Outfit font from Google Fonts for a clean, modern look.</p>
<p>The Cascading Style Sheets (CSS) uses custom properties (variables) in the <code>:root</code> block so you can change the color scheme in one place. The <code>--bg</code> variable holds the dark background color, <code>--surface</code> for card backgrounds, <code>--accent</code> for the green accent color, and <code>--text-muted</code> for secondary text. The <code>.grid</code> class creates a two-column layout for form fields that collapses to one column on screens under 600 pixels wide. The <code>.toast</code> class positions the notification at the bottom-right and uses <code>transform</code> and <code>opacity</code> for a slide-in animation when the <code>.show</code> class is added.</p>
<p>Now create the file at <code>public/admin.html</code>:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Admin - Football Scores Upload&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0f1419;
      --surface: #1a2332;
      --surface-hover: #243044;
      --accent: #00d26a;
      --accent-dim: #00a854;
      --text: #e8edf2;
      --text-muted: #8b9aab;
      --border: #2d3a4d;
      --danger: #ff4757;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      padding: 2rem;
    }
    .container { max-width: 900px; margin: 0 auto; }
    header {
      display: flex;
      align-items: center;
      gap: 1rem;
      margin-bottom: 2rem;
      padding-bottom: 1rem;
      border-bottom: 1px solid var(--border);
    }
    section {
      background: var(--surface);
      border-radius: 12px;
      padding: 1.5rem;
      margin-bottom: 1.5rem;
      border: 1px solid var(--border);
    }
    .grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }
    @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }
    .form-group { display: flex; flex-direction: column; gap: 0.4rem; }
    .form-group.full { grid-column: 1 / -1; }
    input, select {
      padding: 0.65rem 1rem;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: var(--bg);
      color: var(--text);
      font-family: inherit;
    }
    button {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 8px;
      font-family: inherit;
      font-weight: 600;
      cursor: pointer;
    }
    .btn-primary { background: var(--accent); color: var(--bg); }
    .btn-secondary { background: var(--surface-hover); color: var(--text); border: 1px solid var(--border); }
    .actions { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 1rem; }
    .badge { background: var(--accent); color: var(--bg); font-size: 0.75rem; padding: 0.25rem 0.6rem; border-radius: 999px; font-weight: 600; }
    .viewer-link { margin-left: auto; color: var(--accent); text-decoration: none; font-weight: 500; }
    section h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: var(--text-muted); }
    label { font-size: 0.85rem; font-weight: 500; color: var(--text-muted); }
    .toast {
      position: fixed;
      bottom: 2rem;
      right: 2rem;
      padding: 1rem 1.5rem;
      border-radius: 8px;
      font-weight: 500;
      color: var(--bg);
      background: var(--accent);
      transform: translateY(100px);
      opacity: 0;
      transition: transform 0.3s, opacity 0.3s;
      z-index: 100;
    }
    .toast.show { transform: translateY(0); opacity: 1; }
    .toast.error { background: var(--danger); }
    .match-card {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 1rem;
      background: var(--bg);
      border-radius: 8px;
      margin-bottom: 0.5rem;
      border: 1px solid var(--border);
    }
    .match-score { font-size: 1.5rem; font-weight: 700; color: var(--accent); margin: 0 1rem; }
    .match-info { flex: 1; }
    .match-teams { font-weight: 600; font-size: 1rem; }
    .match-meta { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.25rem; }
    .match-actions { display: flex; gap: 0.5rem; }
    .match-actions button { padding: 0.5rem 1rem; font-size: 0.85rem; }
    .match-list { margin-top: 1rem; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="container"&gt;
    &lt;header&gt;
      &lt;h1&gt;⚽ Football Scores&lt;/h1&gt;
      &lt;span class="badge"&gt;Admin&lt;/span&gt;
      &lt;a href="/viewer.html" class="viewer-link"&gt;→ Open Viewer&lt;/a&gt;
    &lt;/header&gt;

    &lt;section&gt;
      &lt;h2&gt;Create New Match&lt;/h2&gt;
      &lt;form id="createMatch"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="homeTeam"&gt;Home Team&lt;/label&gt;
            &lt;input type="text" id="homeTeam" placeholder="e.g. Manchester United" required&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="awayTeam"&gt;Away Team&lt;/label&gt;
            &lt;input type="text" id="awayTeam" placeholder="e.g. Liverpool" required&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="league"&gt;League&lt;/label&gt;
            &lt;input type="text" id="league" placeholder="e.g. Premier League" value="Premier League"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="venue"&gt;Venue&lt;/label&gt;
            &lt;input type="text" id="venue" placeholder="e.g. Old Trafford"&gt;
          &lt;/div&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="kickoff"&gt;Kickoff (ISO)&lt;/label&gt;
            &lt;input type="datetime-local" id="kickoff"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Create Match&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Update Score&lt;/h2&gt;
      &lt;form id="updateScore"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="scoreMatchId"&gt;Select Match&lt;/label&gt;
            &lt;select id="scoreMatchId" required&gt;
              &lt;option value=""&gt;-- Select match --&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="homeScore"&gt;Home Score&lt;/label&gt;
            &lt;input type="number" id="homeScore" min="0" value="0"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="awayScore"&gt;Away Score&lt;/label&gt;
            &lt;input type="number" id="awayScore" min="0" value="0"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="minute"&gt;Minute&lt;/label&gt;
            &lt;input type="number" id="minute" min="0" placeholder="e.g. 67"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="status"&gt;Status&lt;/label&gt;
            &lt;select id="status"&gt;
              &lt;option value="scheduled"&gt;Scheduled&lt;/option&gt;
              &lt;option value="live"&gt;Live&lt;/option&gt;
              &lt;option value="halftime"&gt;Halftime&lt;/option&gt;
              &lt;option value="finished"&gt;Finished&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Update Score&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Add Match Event (Goal, Card, etc.)&lt;/h2&gt;
      &lt;form id="addEvent"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="eventMatchId"&gt;Select Match&lt;/label&gt;
            &lt;select id="eventMatchId" required&gt;
              &lt;option value=""&gt;-- Select match --&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventType"&gt;Event Type&lt;/label&gt;
            &lt;select id="eventType"&gt;
              &lt;option value="goal"&gt;Goal&lt;/option&gt;
              &lt;option value="yellow_card"&gt;Yellow Card&lt;/option&gt;
              &lt;option value="red_card"&gt;Red Card&lt;/option&gt;
              &lt;option value="substitution"&gt;Substitution&lt;/option&gt;
              &lt;option value="penalty"&gt;Penalty&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventTeam"&gt;Team&lt;/label&gt;
            &lt;input type="text" id="eventTeam" placeholder="e.g. Manchester United"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventPlayer"&gt;Player&lt;/label&gt;
            &lt;input type="text" id="eventPlayer" placeholder="e.g. Marcus Rashford"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventMinute"&gt;Minute&lt;/label&gt;
            &lt;input type="number" id="eventMinute" min="0" placeholder="e.g. 23"&gt;
          &lt;/div&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="eventDesc"&gt;Description&lt;/label&gt;
            &lt;input type="text" id="eventDesc" placeholder="Optional description"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Add Event&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Active Matches&lt;/h2&gt;
      &lt;div class="match-list" id="matchList"&gt;&lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;

  &lt;div class="toast" id="toast"&gt;&lt;/div&gt;
</code></pre>
<p>The three forms use <code>id</code> attributes (<code>createMatch</code>, <code>updateScore</code>, <code>addEvent</code>) so the JavaScript can attach submit handlers. The match dropdowns (<code>scoreMatchId</code> and <code>eventMatchId</code>) are populated dynamically when the page loads. The <code>matchList</code> div is the container for the list of active matches. The toast element sits outside the main container so it can be fixed to the viewport.</p>
<h3 id="heading-admin-javascript-logic">Admin JavaScript Logic</h3>
<p>The script block handles all interaction between the admin page and the server. It defines helper functions for showing notifications, fetching matches, updating the UI, and submitting data. When the page loads or after any action (create, update, or event submission), the UI refreshes so the latest match data is always visible.</p>
<pre><code class="language-javascript">  &lt;script&gt;
    const API = '/api';
    const toast = document.getElementById('toast');

    function showToast(msg, isError = false) {
      toast.textContent = msg;
      toast.className = 'toast show' + (isError ? ' error' : '');
      setTimeout(() =&gt; toast.classList.remove('show'), 3000);
    }

    async function fetchMatches() {
      const res = await fetch(`${API}/matches`);
      return res.json();
    }

    function populateSelects(matches) {
      const opts = matches.map(m =&gt; `&lt;option value="\({m.id}"&gt;\){m.homeTeam} vs ${m.awayTeam}&lt;/option&gt;`).join('');
      const html = '&lt;option value=""&gt;-- Select match --&lt;/option&gt;' + opts;
      document.getElementById('scoreMatchId').innerHTML = html;
      document.getElementById('eventMatchId').innerHTML = html;
    }

    function renderMatchList(matches) {
      const list = document.getElementById('matchList');
      if (!matches.length) {
        list.innerHTML = '&lt;p style="color: var(--text-muted);"&gt;No matches yet. Create one above.&lt;/p&gt;';
        return;
      }
      list.innerHTML = matches.map(m =&gt; `
        &lt;div class="match-card" data-id="${m.id}"&gt;
          &lt;div class="match-info"&gt;
            &lt;div class="match-teams"&gt;\({m.homeTeam} vs \){m.awayTeam}&lt;/div&gt;
            &lt;div class="match-meta"&gt;\({m.league} • \){m.status} • ${m.minute}'&lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="match-score"&gt;\({m.homeScore} - \){m.awayScore}&lt;/div&gt;
          &lt;div class="match-actions"&gt;
            &lt;button class="btn-secondary" onclick="quickScore('\({m.id}', \){m.homeScore}, ${m.awayScore})"&gt;Update&lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      `).join('');
    }

    function quickScore(id, h, a) {
      document.getElementById('scoreMatchId').value = id;
      document.getElementById('homeScore').value = h;
      document.getElementById('awayScore').value = a;
    }

    async function loadMatches() {
      const matches = await fetchMatches();
      populateSelects(matches);
      renderMatchList(matches);
    }

    document.getElementById('createMatch').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const body = {
        homeTeam: document.getElementById('homeTeam').value.trim(),
        awayTeam: document.getElementById('awayTeam').value.trim(),
        league: document.getElementById('league').value.trim() || 'Premier League',
        venue: document.getElementById('venue').value.trim() || 'TBD',
        kickoff: document.getElementById('kickoff').value
          ? new Date(document.getElementById('kickoff').value).toISOString()
          : new Date().toISOString(),
      };
      try {
        const res = await fetch(`${API}/matches`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Match created!');
          document.getElementById('createMatch').reset();
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    document.getElementById('updateScore').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const id = document.getElementById('scoreMatchId').value;
      const body = {
        homeScore: parseInt(document.getElementById('homeScore').value, 10),
        awayScore: parseInt(document.getElementById('awayScore').value, 10),
        minute: parseInt(document.getElementById('minute').value, 10) || undefined,
        status: document.getElementById('status').value,
      };
      try {
        const res = await fetch(`\({API}/matches/\){id}/score`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Score updated!');
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    document.getElementById('addEvent').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const id = document.getElementById('eventMatchId').value;
      const body = {
        type: document.getElementById('eventType').value,
        team: document.getElementById('eventTeam').value.trim(),
        player: document.getElementById('eventPlayer').value.trim(),
        minute: parseInt(document.getElementById('eventMinute').value, 10) || undefined,
        description: document.getElementById('eventDesc').value.trim() || undefined,
      };
      try {
        const res = await fetch(`\({API}/matches/\){id}/events`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Event added!');
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    loadMatches();
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><code>showToast</code>: updates the toast text and adds the show class to trigger the CSS animation. The isError parameter switches the toast to a red background for error messages. The setTimeout removes the show class after 3 seconds so the toast slides back out.</p>
</li>
<li><p><code>fetchMatches</code>: calls the GET /api/matches endpoint and returns the parsed JSON so the UI can display the latest data.</p>
</li>
<li><p><code>populateSelects</code>: builds option elements from the matches array and injects them into both dropdowns, so the same match list appears in the Update Score and Add Event forms.</p>
</li>
<li><p><code>renderMatchList</code>: either shows a placeholder when there are no matches or renders each match as a card with team names, scores, league, status, and an Update button.</p>
</li>
<li><p><code>quickScore</code>: pre-fills the Update Score form when you click the Update button on a match card, so you can adjust the score without re-selecting the match.</p>
</li>
<li><p><code>loadMatches</code>: fetches matches, populates the dropdowns, and renders the list. It runs on page load and after every successful create, update, or event submission.</p>
</li>
<li><p><code>onsubmit</code>: calls e.preventDefault() to stop the default form submission, builds a request body from the form values, sends a fetch request to the appropriate endpoint, and on success shows a toast and calls loadMatches to refresh the UI.</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-live-viewer-interface">How to Build the Live Viewer Interface</h2>
<p>The viewer interface displays real-time match updates and connects to the Server-Sent Events endpoint so it can receive data the moment the server pushes it.</p>
<p>Unlike the admin page, the viewer is read-only: it shows live scores, match events, and status updates without requiring user input. The page uses a dark theme and a connection indicator so users can see whether the real-time stream is active.</p>
<p>Now create the file at <code>public/viewer.html</code>:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Live Football Scores&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0a0e14;
      --surface: #131a24;
      --accent: #00d26a;
      --accent-glow: rgba(0, 210, 106, 0.3);
      --text: #e8edf2;
      --text-muted: #8b9aab;
      --border: #2d3a4d;
      --live: #ff4757;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      padding: 2rem;
    }
    .container { max-width: 700px; margin: 0 auto; }
    header { text-align: center; margin-bottom: 2rem; }
    .status {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      font-size: 0.85rem;
      color: var(--text-muted);
    }
    .status-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-muted);
      animation: pulse 2s infinite;
    }
    .status-dot.connected {
      background: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-glow);
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
    .match-card {
      background: var(--surface);
      border-radius: 16px;
      padding: 1.5rem;
      margin-bottom: 1rem;
      border: 1px solid var(--border);
      transition: border-color 0.2s, box-shadow 0.2s;
    }
    .match-card.live {
      border-color: var(--live);
      box-shadow: 0 0 0 1px rgba(255, 71, 87, 0.2);
    }
    .match-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 1rem;
    }
    .league { 
      font-size: 0.8rem; 
      color: var(--text-muted); 
      margin-bottom: 0.25rem; 
    }
    .match-teams {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      margin: 1rem 0;
    }
    .team { flex: 1; text-align: center; font-weight: 600; font-size: 1.1rem; }
    .team.home { text-align: left; }
    .team.away { text-align: right; }
    .score-box {
      display: flex;
      align-items: center;
      justify-content: center;
      min-width: 80px;
      gap: 0.5rem;
    }
    .score { 
      font-size: 2rem; 
      font-weight: 700; 
      color: var(--accent); 
     }
    .status-badge { 
      font-size: 0.7rem; 
      padding: 0.2rem 0.5rem; 
      border-radius: 4px; 
      font-weight: 600; 
     }
    .status-badge.live { background: var(--live); color: white; }
    .status-badge.finished { background: var(--border); color: var(--text-muted); }
    .status-badge.scheduled { background: var(--accent); color: var(--bg); }
    .match-meta { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem; }
    .events { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
    .events h4 { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem; }
    .event { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
    .event:last-child { border-bottom: none; }
    .event-icon { width: 24px; text-align: center; font-size: 1rem; }
    .event.goal .event-icon { color: var(--accent); }
    .event.yellow_card .event-icon { color: #ffd93d; }
    .event.red_card .event-icon { color: var(--live); }
    .feed { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }
    .feed h3 { font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted); }
    .feed-item { font-size: 0.85rem; padding: 0.5rem 0; color: var(--text-muted); border-bottom: 1px solid var(--border); }
    .feed-item:last-child { border-bottom: none; }
    .feed-item strong { color: var(--text); }
    .empty { text-align: center; padding: 3rem 2rem; color: var(--text-muted); }
    .empty-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="container"&gt;
    &lt;header&gt;
      &lt;h1&gt;⚽ Live Football Scores&lt;/h1&gt;
      &lt;div class="status" id="status"&gt;
        &lt;span class="status-dot" id="statusDot"&gt;&lt;/span&gt;
        &lt;span id="statusText"&gt;Connecting...&lt;/span&gt;
      &lt;/div&gt;
    &lt;/header&gt;

    &lt;div id="matches"&gt;&lt;/div&gt;

    &lt;div class="feed" id="feedSection"&gt;
      &lt;h3&gt;Live Feed&lt;/h3&gt;
      &lt;div id="feed"&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><code>header</code>: displays the page title and a connection indicator so users know whether the real-time stream is active.</p>
</li>
<li><p><code>status dot</code>: a small circle that pulses while disconnected and turns green with a glow when the SSE connection is established.</p>
</li>
<li><p><code>matches container</code>: the div with id="matches" is where match cards will be rendered dynamically by JavaScript as data arrives.</p>
</li>
<li><p><code>feed section</code>: shows a chronological list of live updates so users can see recent events at a glance.</p>
</li>
<li><p><code>CSS theme</code>: uses dark colors and custom properties so the look can be adjusted in one place. The live badge and border styles highlight matches that are in progress.</p>
</li>
<li><p><code>Server-Sent Events integration</code>: JavaScript (added in the next step) will connect to /api/events and update this page whenever new data arrives.</p>
</li>
</ul>
<p>The header shows the title and a connection status indicator. The <code>matches</code> div is the container for match cards, populated by JavaScript. The <code>feed</code> div displays the live feed of recent updates.</p>
<h3 id="heading-viewer-javascript-logic">Viewer JavaScript Logic</h3>
<p>The script powers the live viewer interface by maintaining an in-memory collection of matches and a feed of recent updates. It connects to the Server-Sent Events endpoint so data flows from the server to the browser in real time. When messages arrive, the page updates automatically to show the latest scores, events, and match details.</p>
<pre><code class="language-javascript">  &lt;script&gt;
    const API = '/api';
    const matches = new Map();
    const feed = [];
    const MAX_FEED = 20;

    const statusDot = document.getElementById('statusDot');
    const statusText = document.getElementById('statusText');
    const matchesEl = document.getElementById('matches');
    const feedEl = document.getElementById('feed');

    function setStatus(connected) {
      statusDot.classList.toggle('connected', connected);
      statusText.textContent = connected ? 'Live' : 'Reconnecting...';
    }

    function renderMatches() {
      const list = Array.from(matches.values()).sort(
        (a, b) =&gt; new Date(b.createdAt) - new Date(a.createdAt)
      );
      if (!list.length) {
        matchesEl.innerHTML = `
          &lt;div class="empty"&gt;
            &lt;div class="empty-icon"&gt;⚽&lt;/div&gt;
            &lt;p&gt;No matches yet. Updates will appear here in real-time.&lt;/p&gt;
          &lt;/div&gt;
        `;
        return;
      }
      matchesEl.innerHTML = list.map(m =&gt; `
        &lt;div class="match-card \({m.status === 'live' ? 'live' : ''}" data-id="\){m.id}"&gt;
          &lt;div class="match-header"&gt;
            &lt;div&gt;
              &lt;div class="league"&gt;${m.league}&lt;/div&gt;
              &lt;div class="match-meta"&gt;\({m.venue} • \){m.kickoff ? new Date(m.kickoff).toLocaleString() : ''}&lt;/div&gt;
            &lt;/div&gt;
            &lt;span class="status-badge \({m.status}"&gt;\){m.status}&lt;/span&gt;
          &lt;/div&gt;
          &lt;div class="match-teams"&gt;
            &lt;div class="team home"&gt;${m.homeTeam}&lt;/div&gt;
            &lt;div class="score-box"&gt;
              &lt;span class="score"&gt;${m.homeScore}&lt;/span&gt;
              &lt;span&gt;-&lt;/span&gt;
              &lt;span class="score"&gt;${m.awayScore}&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class="team away"&gt;${m.awayTeam}&lt;/div&gt;
          &lt;/div&gt;
          \({m.minute ? `&lt;div class="match-meta"&gt;\){m.minute}'&lt;/div&gt;` : ''}
          ${m.events?.length ? `
            &lt;div class="events"&gt;
              &lt;h4&gt;Events&lt;/h4&gt;
              ${m.events.map(e =&gt; `
                &lt;div class="event ${e.type}"&gt;
                  &lt;span class="event-icon"&gt;${getEventIcon(e.type)}&lt;/span&gt;
                  &lt;span&gt;\({e.minute}' \){e.player} (\({e.team}) - \){e.description || e.type}&lt;/span&gt;
                &lt;/div&gt;
              `).join('')}
            &lt;/div&gt;
          ` : ''}
        &lt;/div&gt;
      `).join('');
    }

    function getEventIcon(type) {
      const icons = { goal: '⚽', yellow_card: '🟨', red_card: '🟥', substitution: '🔄', penalty: '⚽' };
      return icons[type] || '•';
    }

    function renderFeed() {
      const items = feed.slice(-MAX_FEED).reverse();
      feedEl.innerHTML = items.length
        ? items.map(f =&gt; `&lt;div class="feed-item"&gt;${f}&lt;/div&gt;`).join('')
        : '&lt;div class="feed-item"&gt;Waiting for updates...&lt;/div&gt;';
    }

    function addFeedItem(type, msg) {
      const time = new Date().toLocaleTimeString();
      feed.push(`&lt;strong&gt;\({time}&lt;/strong&gt; | \){msg}`);
      if (feed.length &gt; MAX_FEED) feed.shift();
      renderFeed();
    }

    function handleMessage(data) {
      if (data.match) {
        matches.set(data.match.id, data.match);
        renderMatches();
      }
      if (data.id &amp;&amp; data.homeTeam &amp;&amp; data.awayTeam) {
        matches.set(data.id, data);
        renderMatches();
      }
      if (data.type === 'match_created' &amp;&amp; data.match) {
        addFeedItem('match', `New match: \({data.match.homeTeam} vs \){data.match.awayTeam}`);
      }
      if (data.type === 'score_update') {
        const m = matches.get(data.matchId);
        if (m) {
          m.homeScore = data.homeScore;
          m.awayScore = data.awayScore;
          m.minute = data.minute;
          m.status = data.status;
          matches.set(data.matchId, m);
          addFeedItem('score', `\({m.homeTeam} \){data.homeScore}-\({data.awayScore} \){m.awayTeam} (${data.minute || '?'}')`);
          renderMatches();
        }
      }
      if (data.type === 'match_event' &amp;&amp; data.event) {
        const m = matches.get(data.matchId);
        if (m) {
          m.events = m.events || [];
          m.events.push(data.event);
          if (data.event.type === 'goal') {
            if (data.event.team === m.homeTeam) m.homeScore++;
            else if (data.event.team === m.awayTeam) m.awayScore++;
          }
          matches.set(data.matchId, m);
          addFeedItem('event', `\({data.event.type}: \){data.event.player} (\({data.event.team}) - \){data.event.minute}'`);
          renderMatches();
        }
      }
    }

    function handleSSEMessage(msg) {
      try {
        const data = JSON.parse(msg);
        if (data.topic &amp;&amp; /^sports\/football\/match\/([^/]+)$/.test(data.topic) &amp;&amp; data.id) {
          matches.set(data.id, data);
        }
        handleMessage(data);
      } catch (e) {}
    }

    function connectSSE() {
      const es = new EventSource(`${API}/events`);
      es.onopen = () =&gt; setStatus(true);
      es.onerror = () =&gt; {
        setStatus(false);
        es.close();
        setTimeout(connectSSE, 3000);
      };
      es.onmessage = (e) =&gt; handleSSEMessage(e.data);
    }

    async function loadInitial() {
      try {
        const res = await fetch(`${API}/matches`);
        const list = await res.json();
        list.forEach(m =&gt; matches.set(m.id, m));
        renderMatches();
      } catch (e) {
        matchesEl.innerHTML = '&lt;div class="empty"&gt;&lt;p&gt;Could not load matches. Is the server running?&lt;/p&gt;&lt;/div&gt;';
      }
    }

    loadInitial();
    connectSSE();
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p>The <code>matches Map</code>: stores match objects keyed by ID so updates can be applied efficiently without searching arrays.</p>
</li>
<li><p>The <code>feed array</code>: keeps a small history of recent events (limited to MAX_FEED) so the live feed remains lightweight.</p>
</li>
<li><p><code>setStatus</code>: toggles the connected class on the status dot and updates the status text to “Live” or “Reconnecting…” so users know connection status.</p>
</li>
<li><p><code>renderMatches</code>: converts the Map to a sorted array (newest first). If there are no matches, it displays an empty state. Otherwise it renders cards showing league, venue, teams, scores, status badge, minute, and events.</p>
</li>
<li><p><code>getEventIcon</code>: returns an emoji for each event type so events are visually identifiable (goal, card, substitution, and so on).</p>
</li>
<li><p><code>renderFeed</code>: displays the live feed items or a placeholder message when no updates exist.</p>
</li>
<li><p><code>addFeedItem</code>: appends a timestamped message to the feed, keeps only the latest items, and re-renders the feed.</p>
</li>
<li><p><code>handleMessage</code>: processes incoming data. It updates the matches Map for full match objects, score updates, and match events. For score updates and goals it adjusts scores and adds feed items so the viewer reflects real-time changes.</p>
</li>
<li><p><code>handleSSEMessage</code>: parses the Server-Sent Events payload and forwards it to handleMessage. If the message contains a match topic and full match data, it stores it in the Map.</p>
</li>
<li><p><code>connectSSE</code>: creates an EventSource connection to /api/events. On open it marks the connection as live. On error it closes and retries after three seconds so transient network issues do not break the stream.</p>
</li>
<li><p><code>loadInitial</code>: fetches existing matches on page load so the viewer displays data even before real-time updates arrive.</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-home-page">How to Build the Home Page</h2>
<p>The home page (<code>public/index.html</code>) is a simple landing page that links to the viewer and admin interfaces. It uses a centered card layout with two buttons. The primary button (green) goes to the viewer, and the secondary button (outlined) goes to the admin. The page uses the same dark theme and Outfit font for consistency. There is no JavaScript – it's purely static HTML and CSS.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Football Scores - MQTT Real-Time&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0a0e14;
      --surface: #131a24;
      --accent: #00d26a;
      --text: #e8edf2;
      --text-muted: #8b9aab;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 2rem;
    }
    .card {
      background: var(--surface);
      border-radius: 16px;
      padding: 2rem;
      max-width: 400px;
      text-align: center;
      border: 1px solid rgba(255,255,255,0.06);
    }
    h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
    p { color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.5rem; }
    .links { display: flex; flex-direction: column; gap: 0.75rem; }
    a {
      display: block;
      padding: 1rem 1.5rem;
      background: var(--accent);
      color: var(--bg);
      text-decoration: none;
      font-weight: 600;
      border-radius: 10px;
      transition: opacity 0.2s;
    }
    a:hover { opacity: 0.9; }
    a.secondary {
      background: transparent;
      color: var(--text);
      border: 1px solid rgba(255,255,255,0.15);
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="card"&gt;
    &lt;h1&gt;⚽ Football Scores&lt;/h1&gt;
    &lt;p&gt;Real-time updates via MQTT &amp; Mosquitto&lt;/p&gt;
    &lt;div class="links"&gt;
      &lt;a href="/viewer.html"&gt;View Live Scores&lt;/a&gt;
      &lt;a href="/admin.html" class="secondary"&gt;Admin - Upload Scores&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The Express server serves <code>index.html</code> when you visit the root URL (<code>http://localhost:3000/</code>) because the <code>express.static</code> middleware serves files from the <code>public</code> folder, and Express automatically serves <code>index.html</code> when the request path is <code>/</code>.</p>
<h2 id="heading-how-to-run-and-test-the-system">How to Run and Test the System</h2>
<p>Start the MQTT broker (Docker or local install).</p>
<p>Then start the Express server:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>Next, open <code>http://localhost:3000/admin.html</code> in the admin panel. Create a new match (for example, Manchester United vs Liverpool).</p>
<p>Open <code>http://localhost:3000/viewer.html</code> in another tab or window. Then update the score or add an event in the admin panel. The viewer should update within a second without refreshing.</p>
<h2 id="heading-how-to-extend-the-system-for-production">How to Extend the System for Production</h2>
<p>The current implementation uses in-memory storage. For production, you should:</p>
<ul>
<li><p><strong>Add a database</strong>: Store matches in PostgreSQL, MongoDB, or another database. Load matches on startup and persist every create, update, and event.</p>
</li>
<li><p><strong>Add authentication</strong>: Protect the admin routes with JSON Web Tokens (JWT) or session-based auth so only authorized users can upload scores.</p>
</li>
<li><p><strong>Add validation</strong>: Validate request bodies with a library such as Joi or Zod to prevent invalid data.</p>
</li>
<li><p><strong>Enable TLS</strong>: Use HTTPS for the Express server and secure WebSockets or MQTTS for the broker in production.</p>
</li>
<li><p><strong>Scale horizontally</strong>: If you run multiple server instances, each will have its own MQTT connection and SSE clients. The MQTT broker will deliver messages to all subscribers, so each instance will receive updates and forward them to its connected viewers.</p>
</li>
</ul>
<h2 id="heading-api-reference">API Reference</h2>
<p>For quick reference, here are the endpoints your server exposes:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td><code>/api/matches</code></td>
<td>Returns all matches as a JSON array</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/matches</code></td>
<td>Creates a new match. Body: <code>{ homeTeam, awayTeam, league?, venue?, kickoff? }</code></td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/api/matches/:id/score</code></td>
<td>Updates a match score. Body: <code>{ homeScore?, awayScore?, minute?, status? }</code></td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/matches/:id/events</code></td>
<td>Adds an event. Body: <code>{ type?, team, player?, minute?, description? }</code></td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/events</code></td>
<td>Server-Sent Events stream for real-time updates</td>
</tr>
</tbody></table>
<p>The <code>status</code> field can be <code>scheduled</code>, <code>live</code>, <code>halftime</code>, or <code>finished</code>. The <code>type</code> field for events can be <code>goal</code>, <code>yellow_card</code>, <code>red_card</code>, <code>substitution</code>, or <code>penalty</code>.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>You might come across various issues as you build this. Here are some common ones:</p>
<p><strong>The server cannot connect to the MQTT broker.</strong> Check that Mosquitto is running. If you use Docker, run <code>docker ps</code> to verify the container is up. If you use the public broker, ensure you have internet connectivity and that your firewall allows outbound connections on port 1883.</p>
<p><strong>The viewer does not update when you change scores.</strong> Open the browser developer tools and check the Network tab. The <code>/api/events</code> request should show a pending state (it stays open). If it fails or closes, check the server logs for errors. Ensure you are not behind a proxy that buffers or closes long-lived connections.</p>
<p><strong>Matches disappear when you restart the server.</strong> The current implementation stores matches in memory. Restarting the server clears the data. Add a database as described in the production section to persist matches across restarts.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a real-time football sports update system using MQTT, Mosquitto, and Express. You've learned how to:</p>
<ul>
<li><p>Connect an Express server to an MQTT broker</p>
</li>
<li><p>Publish match and score updates to MQTT topics</p>
</li>
<li><p>Subscribe to topics and forward messages to browsers via Server-Sent Events</p>
</li>
<li><p>Build an admin interface for creating matches and updating scores</p>
</li>
<li><p>Build a viewer interface that displays live updates without page refreshes</p>
</li>
</ul>
<p>The same pattern applies to other real-time systems: IoT dashboards, live notifications, collaborative editing, and more. MQTT gives you a reliable, scalable messaging layer, and Server-Sent Events gives you a simple way to push updates to web clients.</p>
<p>The code in this tutorial gives you a solid foundation. Try adding features such as match filtering by league, historical event logs, or push notifications to make the system your own.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Connect the nRF9160 Feather to a Self-Hosted Mosquitto Instance ]]>
                </title>
                <description>
                    <![CDATA[ By Jared Wolff One thing that’s always tripped me up as an IoT developer is figuring out the best way to transmit data. There are many different kinds of radios and mediums. On top of that, there are different protocols to boot. As of this writing, t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-connect-the-nrf9160-feather-to-mosquitto/</link>
                <guid isPermaLink="false">66d8504d5a0b456f4d32147f</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ iot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mqtt ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 05 Aug 2020 16:29:54 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/08/Copy-of-Bluetooth-with-nRF9160-Feather.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Jared Wolff</p>
<p>One thing that’s always tripped me up as an IoT developer is figuring out the best way to transmit data. There are many different kinds of radios and mediums. On top of that, there are different protocols to boot.</p>
<p>As of this writing, there is one protocol that has reigned supreme in the IoT world:</p>
<p>MQTT.</p>
<p>Unlike an HTTP server, a device can connect, publish, and subscribe to topics. These topics then get sent to a broker and distributed to other subscribed devices. It also happens that MQTT on Nordic’s nRF9160 is well supported.</p>
<p>In this post, I’ll show you how to connect the nRF9160 Feather to a self-hosted <a target="_blank" href="https://github.com/eclipse/mosquitto">Mosquitto</a> instance. You’ll learn how to generate your own certificates, and get a hang of how to test your connections.</p>
<p>Ready to play? Let’s get to it.</p>
<h2 id="heading-where-to-host">Where to host?</h2>
<p>If you want to host Mosquitto, you’ll need a server. Since Mosquitto is written in C it’s lightweight and can go almost anywhere. Plus it sips resources so you can install it on a budget VPS without much worry. That’s where a VPS provider like Digital Ocean or Vultr comes in.</p>
<p>To set up a new server here are some steps:</p>
<ul>
<li>Login to Digital Ocean. If you don’t have Digital Ocean and would like to support click <a target="_blank" href="https://m.do.co/c/9574d3846a29">here</a> to create an account.</li>
<li>Create a new Droplet</li>
</ul>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/Screen_Shot_2020-08-02_at_5.21.21_PM.png" alt="Create a new droplet" width="730" height="521" loading="lazy"></p>
<ul>
<li>Choose the FreeBSD 12.1 with UFS.</li>
</ul>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/Screen_Shot_2020-08-02_at_5.21.29_PM.png" alt="Create a FreeBSD 12.1 Droplet with UFS" width="730" height="521" loading="lazy"></p>
<ul>
<li>Choose the $5 instance. That’s usually more than enough.</li>
</ul>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/Screen_Shot_2020-08-02_at_5.21.42_PM.png" alt="Select tier of Droplet" width="730" height="521" loading="lazy"></p>
<ul>
<li>Make sure you import your public key. Otherwise, you won’t be able to immediately use password-less login.</li>
</ul>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/Screen_Shot_2020-08-02_at_5.21.50_PM.png" alt="Choose authentication method" width="730" height="521" loading="lazy"></p>
<ul>
<li>Hit that green <strong>Create Droplet</strong> button, and let’s get this show on the road.</li>
</ul>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/Screen_Shot_2020-08-02_at_5.21.54_PM.png" alt="Create Droplet button" width="730" height="521" loading="lazy"></p>
<h3 id="heading-important-extra-step">Important Extra step</h3>
<p>For the certs to work with Mosquitto, you’ll have to set a domain to point to your VPS IP address. A CNAME or A record works. If you’re unsure how to do that, <a target="_blank" href="https://misago.gitbook.io/docs/setup/domain">here’s a good guide.</a> Note which (sub)domain you’ve used. We’ll need it in a bit…</p>
<h2 id="heading-install-mosquitto">Install Mosquitto</h2>
<p>I run my servers inside of a FreeBSD jail using <a target="_blank" href="https://bastillebsd.org/">Bastille</a>. In this tutorial, we’ll be skipping the jail part and focusing on getting the nRF9160 Feather working.</p>
<ul>
<li>You should be set with a Digital Ocean instance (or similar) using FreeBSD. If you haven't done that yet, head back up to the <strong>Where to host?</strong> section.</li>
<li>Next, to install <code>mosquitto</code> on your droplet, run <code>pkg install mosquitto</code>. If you’re running something other than FreeBSD, this command may differ. <code>apt-get install mosquitto</code> works on Debian-based systems. If you want the most up to date repositories, make sure you run <code>sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa</code> beforehand. Here’s what the full output on FreeBSD looks like:</li>
</ul>
<pre><code class="lang-bash">$ pkg install mosquitto
The package management tool is not yet installed on your system.
Do you want to fetch and install it now? [y/N]: y
Bootstrapping pkg from pkg+http://pkg.FreeBSD.org/FreeBSD:12:amd64/quarterly, please <span class="hljs-built_in">wait</span>...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... <span class="hljs-keyword">done</span>
[mosquitto] Installing pkg-1.14.6...
[mosquitto] Extracting pkg-1.14.6: 100%
Updating FreeBSD repository catalogue...
[mosquitto] Fetching meta.conf: 100%    163 B   0.2kB/s    00:01
[mosquitto] Fetching packagesite.txz: 100%    6 MiB   6.6MB/s    00:01
Processing entries: 100%
FreeBSD repository update completed. 31943 packages processed.
All repositories are up to date.
Updating database digests format: 100%
The following 4 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        c-ares: 1.16.1
        ca_root_nss: 3.55
        e2fsprogs-libuuid: 1.45.6
        mosquitto: 1.6.7

Number of packages to be installed: 4

The process will require 2 MiB more space.
682 KiB to be downloaded.

Proceed with this action? [y/N]: y
[mosquitto] [1/4] Fetching mosquitto-1.6.7.txz: 100%  226 KiB 231.1kB/s    00:01
[mosquitto] [2/4] Fetching ca_root_nss-3.55.txz: 100%  285 KiB 291.5kB/s    00:01
[mosquitto] [3/4] Fetching e2fsprogs-libuuid-1.45.6.txz: 100%   34 KiB  34.7kB/s    00:01
[mosquitto] [4/4] Fetching c-ares-1.16.1.txz: 100%  138 KiB 140.9kB/s    00:01
Checking integrity... <span class="hljs-keyword">done</span> (0 conflicting)
[mosquitto] [1/4] Installing ca_root_nss-3.55...
[mosquitto] [1/4] Extracting ca_root_nss-3.55: 100%
[mosquitto] [2/4] Installing e2fsprogs-libuuid-1.45.6...
[mosquitto] [2/4] Extracting e2fsprogs-libuuid-1.45.6: 100%
[mosquitto] [3/4] Installing c-ares-1.16.1...
[mosquitto] [3/4] Extracting c-ares-1.16.1: 100%
[mosquitto] [4/4] Installing mosquitto-1.6.7...
===&gt; Creating users
Using existing user <span class="hljs-string">'nobody'</span>.
[mosquitto] [4/4] Extracting mosquitto-1.6.7: 100%
=====
Message from ca_root_nss-3.55:

--
FreeBSD does not, and can not warrant that the certification authorities
whose certificates are included <span class="hljs-keyword">in</span> this package have <span class="hljs-keyword">in</span> any way been
audited <span class="hljs-keyword">for</span> trustworthiness or RFC 3647 compliance.

Assessment and verification of trust is the complete responsibility of the
system administrator.

This package installs symlinks to support root certificates discovery by
default <span class="hljs-keyword">for</span> software that uses OpenSSL.

This enables SSL Certificate Verification by client software without manual
intervention.

If you prefer to <span class="hljs-keyword">do</span> this manually, replace the following symlinks with
either an empty file or your site-local certificate bundle.

  * /etc/ssl/cert.pem
  * /usr/<span class="hljs-built_in">local</span>/etc/ssl/cert.pem
  * /usr/<span class="hljs-built_in">local</span>/openssl/cert.pem
=====
Message from mosquitto-1.6.7:

--
The mosquitto MQTT Python driver is now provided by net/py-paho-mqtt
</code></pre>
<p>All installed package configuration lives at <code>/usr/local/etc/mosquitto/</code>. We’ll need to edit <code>mosquitto.conf</code> in that folder to use certificates. Here’s what it looks like:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Daemon configuration</span>
pid_file /var/run/mosquitto.pid
user nobody

<span class="hljs-comment"># Port to use for the default listener.</span>
port 8885

<span class="hljs-comment"># At least one of cafile or capath must be defined.</span>
cafile /root/pki/ca.crt

<span class="hljs-comment"># Path to the PEM encoded server certificate.</span>
certfile /root/pki/issued/mosquitto.crt

<span class="hljs-comment"># Path to the PEM encoded keyfile.</span>
keyfile /root/pki/private/mosquitto.key

<span class="hljs-comment"># Path to CRL file</span>
<span class="hljs-comment">#crlfile /root/pki/crl.pem</span>

<span class="hljs-comment"># Each client has their own cert</span>
require_certificate <span class="hljs-literal">true</span>
use_identity_as_username <span class="hljs-literal">true</span>

<span class="hljs-comment"># listener port-number [ip address/host name]</span>
listener 1883
protocol mqtt

<span class="hljs-comment"># listener port-number [ip address/host name]</span>
<span class="hljs-comment"># listener 8080</span>
<span class="hljs-comment"># protocol websockets</span>

<span class="hljs-comment"># =================================================================</span>
<span class="hljs-comment"># Logging</span>
<span class="hljs-comment"># =================================================================</span>
log_dest syslog

<span class="hljs-comment"># Types of messages to log.</span>
log_type all
<span class="hljs-comment">#log_type warning</span>
<span class="hljs-comment"># websockets_log_level 127</span>

<span class="hljs-comment"># -----------------------------------------------------------------</span>
<span class="hljs-comment"># Default authentication and topic access control</span>
<span class="hljs-comment"># -----------------------------------------------------------------</span>
<span class="hljs-comment"># password_file /usr/local/etc/mosquitto/pwfile</span>
</code></pre>
<p>Before we can start the server we’ll need to provision some RSA certificates. We’ll get to that in the next step.</p>
<h2 id="heading-provision-certs">Provision Certs</h2>
<p>You can use <strong>easy-rsa</strong> to generate a CA server and client certs. (These instructions come from <a target="_blank" href="https://github.com/OpenVPN/easy-rsa/blob/master/README.quickstart.md">this guide</a>.) For production, you should generate your keys and certs on an offline machine. That way your private keys are safe if your server becomes a target.</p>
<p>First, install <code>easy-rsa</code>:</p>
<pre><code class="lang-bash">$ pkg install easy-rsa
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following 1 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        easy-rsa: 3.0.7

Number of packages to be installed: 1

44 KiB to be downloaded.

Proceed with this action? [y/N]: y
[mosquitto] [1/1] Fetching easy-rsa-3.0.7.txz: 100%   44 KiB  44.8kB/s    00:01
Checking integrity... <span class="hljs-keyword">done</span> (0 conflicting)
[mosquitto] [1/1] Installing easy-rsa-3.0.7...
[mosquitto] [1/1] Extracting easy-rsa-3.0.7: 100%
</code></pre>
<p>Then let's begin the cert creation process:</p>
<pre><code class="lang-fallback">$ easyrsa init-pki

Note: using Easy-RSA configuration from: /usr/local/share/easy-rsa/vars

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /root/pki
$
$ easyrsa build-ca

Note: using Easy-RSA configuration from: /usr/local/share/easy-rsa/vars
Using SSL: openssl OpenSSL 1.1.1d-freebsd  10 Sep 2019

Enter New CA Key Passphrase:
Re-Enter New CA Key Passphrase:
Generating RSA private key, 2048 bit long modulus (2 primes)
......................+++++
..................................................................................+++++
e is 65537 (0x010001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:testserver.jaredwolff.com

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/root/pki/ca.crt
</code></pre>
<p><strong>Note:</strong> You will be prompted for a password at the <code>build-ca</code> step. Make sure you keep this password handy.</p>
<p>Then to generate a server cert use:</p>
<pre><code class="lang-fallback">$ easyrsa gen-req mosquitto nopass

Note: using Easy-RSA configuration from: /usr/local/share/easy-rsa/vars
Using SSL: openssl OpenSSL 1.1.1d-freebsd  10 Sep 2019
Generating a RSA private key
...............+++++
........................................+++++
writing new private key to '/root/pki/easy-rsa-82720.X2NVQ0/tmp.akOxhO'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [mosquitto]:testserver.jaredwolff.com

Keypair and certificate request completed. Your files are:
req: /root/pki/reqs/mosquitto.req
key: /root/pki/private/mosquitto.key
$
$ easyrsa sign-req server mosquitto

Note: using Easy-RSA configuration from: /usr/local/share/easy-rsa/vars
Using SSL: openssl OpenSSL 1.1.1d-freebsd  10 Sep 2019

You are about to sign the following certificate.
Please check over the details shown below for accuracy. Note that this request
has not been cryptographically verified. Please be sure it came from a trusted
source or that you have verified the request checksum with the sender.

Request subject, to be signed as a server certificate for 825 days:

subject=
    commonName                = testserver.jaredwolff.com

Type the word 'yes' to continue, or any other input to abort.
  Confirm request details: yes
Using configuration from /root/pki/easy-rsa-82744.hyuGzt/tmp.lZHLEH
Enter pass phrase for /root/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'testserver.jaredwolff.com'
Certificate is to be certified until Nov  3 01:12:53 2022 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

Certificate created at: /root/pki/issued/mosquitto.crt
</code></pre>
<p>You’ll be prompted for both the Common Name (i.e. your server name) and the CA cert password in the above step. <strong>Important</strong>: the <strong>Common Name</strong> needs to match the domain name of your server! (Remember, we wrote that down earlier?)</p>
<p>To generate the nRF9160 cert, use:</p>
<pre><code class="lang-fallback">$ easyrsa gen-req nrf9160 nopass batch
$ easyrsa sign-req client nrf9160 batch
</code></pre>
<p>Follow the same procedure as earlier. The only difference is that we’re generating a <strong>client</strong> cert instead of a <strong>server</strong> cert.</p>
<p>Once complete, we’ll need some files. Here’s a full list:</p>
<p><strong>For your Mosquitto Server</strong></p>
<ul>
<li><code>/root/pki/ca.crt</code></li>
<li><code>/root/pki/private/mosquitto.key</code></li>
<li><code>/root/pki/issued/mosquitto.crt</code></li>
</ul>
<p><strong>For your nRF9160 Feather</strong></p>
<ul>
<li><code>/root/pki/ca.crt</code></li>
<li><code>/root/pki/private/nrf9160.key</code></li>
<li><code>/root/pki/issued/nrf9160.crt</code></li>
</ul>
<p>If you’re using the configuration from above, it’s already pointing to your server certificates. All we have to do now is start <code>mosquitto</code>!</p>
<pre><code class="lang-bash">$ service mosquitto start
Cannot <span class="hljs-string">'start'</span> mosquitto. Set mosquitto_enable to YES <span class="hljs-keyword">in</span> /etc/rc.conf or use <span class="hljs-string">'onestart'</span> instead of <span class="hljs-string">'start'</span>.
</code></pre>
<p>If you get an error about <code>mosquitto_enable</code> simply run:</p>
<pre><code class="lang-bash">$ sysrc mosquitto_enable=YES
$ service mosquitto start
Starting mosquitto.
</code></pre>
<p>This enables <code>mosquitto</code> to start when your system starts.</p>
<p>Now, check if <code>mosquitto</code> is running by using <code>ps aux</code>:</p>
<pre><code class="lang-bash">
$ ps aux
USER     PID %CPU %MEM   VSZ  RSS TT  STAT STARTED    TIME COMMAND
root   82401  0.0  0.2 11472 2424  -  SsJ  01:02   0:00.00 /usr/sbin/syslogd -ss
root   82457  0.0  0.2 11408 2284  -  IsJ  01:02   0:00.00 /usr/sbin/cron -J 60 -s
nobody 82900  0.0  0.6 16352 6212  -  SsJ  01:17   0:00.02 /usr/<span class="hljs-built_in">local</span>/sbin/mosquitto -c /usr/<span class="hljs-built_in">local</span>/etc/mosquitto/mosquitto.conf -d
root   82488  0.0  0.3 12096 2848  0  IJ   01:02   0:00.01 login [pam] (login)
root   82489  0.0  0.3 13092 3504  0  SJ   01:02   0:00.03 -csh (csh)
root   82902  0.0  0.3 11704 2540  0  R+J  01:17   0:00.00 ps aux
</code></pre>
<p>Now that we have a server loaded and running, let’s get the firmware working.</p>
<h2 id="heading-firmware-bits">Firmware bits</h2>
<p>Dealing with certificates on the nRF9160 Feather is a two-step process. The first step is to load the certificates using the <code>at_client</code> firmware. The second is to load the <code>mqtt_simple</code> library with added TLS support. Let’s tackle the certs first.</p>
<h3 id="heading-program-atclient-first">Program <code>at_client</code> first</h3>
<p>Change directories to <code>ncs/nrf/samples/nrf9160/at_client/</code> and start a fresh build:</p>
<pre><code class="lang-c">$ west build -b circuitdojo_feather_nrf9160ns -p
</code></pre>
<p>Then flash to your board using:</p>
<pre><code class="lang-c">$ west flash --erase
$ nrfjprog -r
</code></pre>
<p>We’ll need this sample on your board for the next step.</p>
<h3 id="heading-add-certs-to-device">Add certs to device</h3>
<p>To install our new certs, we’ll need nRF Connect Desktop installed. You can download it <a target="_blank" href="https://www.nordicsemi.com/Software-and-tools/Development-Tools/nRF-Connect-for-desktop/Download#infotabs">by going here.</a></p>
<p>You’ll also need a custom version of <strong>LTE Link Monitor.</strong> You can get the modified version on <a target="_blank" href="http://localhost:3000/files/pc-nrfconnect-linkmonitor-1.1.1.tgz">docs.jaredwolff.com.</a></p>
<p>First, Install nRF Connect Desktop app. Then, copy the LTE Link Monitor .tgz file to <code>%USERPROFILE%\.nrfconnect-apps\local</code> (on Windows) or <code>$HOME/.nrfconnect-apps/local</code> (on Linux/macOS). Here’s an example of where it is on Windows:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-11.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Close and re-open nRF Connect Desktop (if it’s open).</p>
<p>Then, click <em>Open</em> next to the v1.1.1 version of LTE Link Monitor. It will also have <strong>local</strong> written underneath it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-12.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Next, let’s launch it!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-13.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Once you’ve opened the port, hit the reset button. Make sure you turn off <strong>Automatic requests.</strong></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-14.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Then in the command box send <strong>AT+CFUN=4</strong>. This will shut down your modem so it’s ready to upload certs. You can run <strong>AT+CFUN?</strong> to confirm your modem in that mode.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-15.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Open up the Certificate manager.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-16.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Make sure you set the security tag. In this case, I’m using 1234. This is an important identifier that you’ll need later. Make it whatever you want but I would avoid using 16842753. This is the default tag for NRF Cloud. You don’t want to blast away your nRF Cloud certs!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-17.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Copy and paste the contents of your <code>ca.crt</code>, <code>nrf9160.crt</code> and <code>nrf9160.key</code> into the boxes (in that order). You can easily get the certs by using <code>cat</code> on Unix/Linux:</p>
<pre><code class="lang-bash">$ cat cat.crt
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgIUDLkBxLLQO9wosNDtA7E9qvqHOxMwDQYJKoZIhvcNAQEL
BQAwJDEiMCAGA1UEAwwZdGVzdHNlcnZlci5qYXJlZHdvbGZmLmNvbTAeFw0yMDA3
MzEwMTExNDJaFw0zMDA3MjkwMTExNDJaMCQxIjAgBgNVBAMMGXRlc3RzZXJ2ZXIu
amFyZWR3b2xmZi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3
de1v8k+FXzY/Im7Z2YKS7wwbBRft5CUxqP1sdYJgMvheS9LhFufk81URZ0lHD9pK
aNPxU1UEmnLvVDTGLJ+YAmMH08xn17FS1R1UVPYzi2ouwqRM2pR9EStsSlP9Zj44
1MsdizABnnlkZndUVLL/gjc4cNsNncMLBSEbsz6b5WzhtAGg3rOpdAxSSblZVSFw
bquCgg5hb2NUzy+JxGtUIsE5d6CxTDdSs4Z3FK/RRYjmCG6qsaya4N5W35yf8h5O
StfKRecl3kq2kCnWa6P+lErG4wuxIBtMkgz2zV+zd1tz4aHXxSdoZTqLz7dTVbFA
zEVnKD+ZReBG+4fwUL6rAgMBAAGjgZ4wgZswHQYDVR0OBBYEFIvdGnjrxRPzvXQi
7XJ70LzpZSOjMF8GA1UdIwRYMFaAFIvdGnjrxRPzvXQi7XJ70LzpZSOjoSikJjAk
MSIwIAYDVQQDDBl0ZXN0c2VydmVyLmphcmVkd29sZmYuY29tghQMuQHEstA73Ciw
0O0DsT2q+oc7EzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0B
AQsFAAOCAQEAIzz1nSSDkPueNPlADRYMDOMFNkxoKA+gRXwDVa7y39As7IZp7Fqr
KAH79U1XtGyDlt6FPKTvDJ7jtd4y8auIGVQO7z3AG9pVU1imIWZHoIqgBUCEhsjp
uMxD23kRCX5kd9dsmF9WOGGxb4kkMv83Rh2rCONQmvnozuI3fJv2ZFX/ORoADGLP
OPSJPl11x+2rxPxiLi+T8RyzDh3DwqnPVsSnbRWV7hosaN0ip/cbnSTaIul9mbCY
ID6qm9leqlY/gha9aZfg+tv1Lm6PT6o8Pzek2VeDoIS5YERBMOwV84hQrZjV3vIE
jT6y663HGsl7KvqVaWdV3fM6Cr7f0QdR5A==
-----END CERTIFICATE-----
</code></pre>
<p>You’ll need everything from <code>-----BEGIN CERTIFICATE-----</code> to <code>-----END CERTIFICATE-----</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-18.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Check the <strong>Log</strong> area for more details. If all went well, it should say that your certificates updated.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-19.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-using-the-mqttsimple-example">Using the mqtt_simple example</h3>
<p>We’ll be using the <code>mqtt_simple</code> sample within the nRF Connect SDK repository. The full path is: <code>ncs/nrf/samples/nrf9160/mqtt_simple</code>. We’ll need to make a few edits to add full TLS compatibility. All the files are within the <code>mqtt_simple</code> directory.</p>
<p>First, we’ll have to update the <code>proj.conf</code> file. See the highlighted differences:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-20.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The <code># Set the PDP Context</code> section is especially important if you’re using a Hologram SIM card (included with the nRF9160 Feather). If you are using a SIM that doesn’t need it, you do not need this section.</p>
<p>Adopt your <code>CONFIG_MQTT_BROKER_HOSTNAME</code> to your hostname (configured at the beginning of this guide).</p>
<p>You’ll also have to add these lines in <code>KConfig</code>:</p>
<pre><code class="lang-c">config SEC_TAG
    <span class="hljs-keyword">int</span> <span class="hljs-string">"Security tag to use for the connection"</span>
    <span class="hljs-keyword">default</span> <span class="hljs-number">1234</span>

config PEER_VERIFY
    <span class="hljs-keyword">int</span> <span class="hljs-string">"Peer verify parameter for mqtt_client"</span>
    <span class="hljs-keyword">default</span> <span class="hljs-number">1</span>
    help
            Set to <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> VERIFY_NONE, <span class="hljs-number">1</span> <span class="hljs-keyword">for</span> VERIFY_OPTIONAL, <span class="hljs-keyword">and</span> <span class="hljs-number">2</span> <span class="hljs-keyword">for</span> VERIFY_REQUIRED.
</code></pre>
<p>Finally in main add this block to the top of your file:</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">if</span> defined(CONFIG_MQTT_LIB_TLS)</span>
<span class="hljs-keyword">static</span> <span class="hljs-keyword">sec_tag_t</span> sec_tag_list[] = { CONFIG_SEC_TAG };
<span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span> <span class="hljs-comment">/* defined(CONFIG_MQTT_LIB_TLS) */</span></span>
</code></pre>
<p>Then add this block to <code>client_init</code> under <code>#if defined(CONFIG_MQTT_LIB_TLS)</code></p>
<pre><code class="lang-c">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mqtt_sec_config</span> *<span class="hljs-title">tls_config</span> = &amp;<span class="hljs-title">client</span>-&gt;<span class="hljs-title">transport</span>.<span class="hljs-title">tls</span>.<span class="hljs-title">config</span>;</span>

    client-&gt;transport.type = MQTT_TRANSPORT_SECURE;

    tls_config-&gt;peer_verify = CONFIG_PEER_VERIFY;
    tls_config-&gt;cipher_count = <span class="hljs-number">0</span>;
    tls_config-&gt;cipher_list = <span class="hljs-literal">NULL</span>;
    tls_config-&gt;sec_tag_count = ARRAY_SIZE(sec_tag_list);
    tls_config-&gt;sec_tag_list = sec_tag_list;
    tls_config-&gt;hostname = CONFIG_MQTT_BROKER_HOSTNAME;
</code></pre>
<p>The changes should look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-21.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Then build with:</p>
<pre><code class="lang-c">$ west build -b circuitdojo_feather_nrf9160ns -p
</code></pre>
<p>Finally, flash it using <code>west flash</code>:</p>
<pre><code class="lang-c">$ west flash --erase
$ nrfjprog -r
</code></pre>
<p>Open your serial terminal and double check that your nRF9160 Feather is connecting. You can also use LTE Link Monitor to view your progress as well (Example below).</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-22.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Lots of the information above came from <a target="_blank" href="https://devzone.nordicsemi.com/nordic/cellular-iot-guides/b/software-and-protocols/posts/enabling-and-testing-tls-in-mqtt_5f00_simple">Nordic</a>'s post on the subject.</p>
<h2 id="heading-sending-a-message">Sending a message</h2>
<p>We’re almost there! You’ve configured your nRF9160 Feather to connect to Mosquitto using self-generated certificates. The last part is connecting another device to see if the nRF9160 Feather replies to a message.</p>
<p>I’ve created a new set of certs for this purpose. I called them <code>test</code>.</p>
<pre><code class="lang-bash">$ easyrsa gen-req <span class="hljs-built_in">test</span> nopass batch
$ easyrsa sign-req client <span class="hljs-built_in">test</span> batch
</code></pre>
<p>I copied them to my desktop using CyberDuck (a great little visual SFTP client):</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-23.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You can also use something like <code>scp</code> if you're confident in your command line file transfer abilities. Then, open a terminal and run:</p>
<pre><code class="lang-bash">mosquitto_sub --cafile ca.crt --cert test.crt --key test.key -q 1 -d -h testserver.jaredwolff.com -p 8885 -t <span class="hljs-string">"/my/publish/topic"</span> &amp;
mosquitto_pub --cafile ca.crt --cert test.crt --key test.key -q 1 -d -h testserver.jaredwolff.com -p 8885 -t <span class="hljs-string">"/my/subscribe/topic"</span> -m <span class="hljs-string">"hello there"</span>
</code></pre>
<p>You should see an output like this:</p>
<pre><code class="lang-bash">$ mosquitto_sub --cafile ca.crt --cert test.crt --key test.key -q 1 -d -h testserver.jaredwolff.com -p 8885 -t <span class="hljs-string">"/my/publish/topic"</span> &amp;
$ mosquitto_pub --cafile ca.crt --cert test.crt --key test.key -q 1 -d -h testserver.jaredwolff.com -p 8885 -t <span class="hljs-string">"/my/subscribe/topic"</span> -m <span class="hljs-string">"hello there"</span>
Client mosq-CczskQKzMKdtTo4O4s sending CONNECT
Client mosq-CczskQKzMKdtTo4O4s received CONNACK (0)
Client mosq-CczskQKzMKdtTo4O4s sending PUBLISH (d0, q1, r0, m1, <span class="hljs-string">'/my/subscribe/topic'</span>, ... (11 bytes))
Client mosq-CczskQKzMKdtTo4O4s received PUBACK (Mid: 1, RC:0)
Client mosq-CczskQKzMKdtTo4O4s sending DISCONNECT
MacBook-Pro:Downloads jaredwolff$ Client mosq-qK8tMlJk0Qri4Z7jUo sending PINGREQ
Client mosq-qK8tMlJk0Qri4Z7jUo received PINGRESP
MacBook-Pro:Downloads jaredwolff$ Client mosq-qK8tMlJk0Qri4Z7jUo received PUBLISH (d0, q0, r0, m0, <span class="hljs-string">'/my/publish/topic'</span>, ... (11 bytes))
hello there
</code></pre>
<p>Booyah! You have an active working connection to your very own Mosquitto server.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We’ve made it to the end! By this point in the post, you should have a Mosquitto server running and an nRF9160 connected. Now you can use your new-found skills to add more devices to your deployments and more.</p>
<p>If you haven’t had a chance to play with the nRF9160 you should check out the nRF9160 Feather. It has Nordic Semiconductor’s nRF9160 LTE-M, NB IoT + GPS Combo, plus flexible power supply, external flash, and low power shutdown. </p>
<p>Oh, and did I mention it’s 100% open source? Learn more by checking out the campaign on <a target="_blank" href="https://www.jaredwolff.com/store/nrf91-feather/">GroupGets and Hackster Launch.</a> ?</p>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/IMG_8749.jpg" alt="nRF9160 Feather Top Side" width="730" height="730" loading="lazy"></p>
<p><img src="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/images/IMG_8750.jpg" alt="nRF9160 Feather Bottom Side" width="730" height="730" loading="lazy"></p>
<p>Photo credit to the awesome folks at <a target="_blank" href="https://www.groupgets.com/">GroupGets</a>!</p>
<p><strong>You can read this article and lots of other good stuff at <a target="_blank" href="https://www.jaredwolff.com/how-to-connect-nrf9160-feather-to-mosquitto/">jaredwolff.com</a>.</strong></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
