<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Gerber</title>
    <description>The latest articles on DEV Community by David Gerber (@geda73).</description>
    <link>https://dev.to/geda73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3931969%2F93de54cf-ac93-4c6f-890a-def11f794660.png</url>
      <title>DEV Community: David Gerber</title>
      <link>https://dev.to/geda73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geda73"/>
    <language>en</language>
    <item>
      <title>ThingsBoard CE doesn't speak LoRaWAN — here's a Spring Boot bridge that fixes it</title>
      <dc:creator>David Gerber</dc:creator>
      <pubDate>Thu, 14 May 2026 20:51:03 +0000</pubDate>
      <link>https://dev.to/geda73/thingsboard-ce-doesnt-speak-lorawan-heres-a-spring-boot-bridge-that-fixes-it-3mnn</link>
      <guid>https://dev.to/geda73/thingsboard-ce-doesnt-speak-lorawan-heres-a-spring-boot-bridge-that-fixes-it-3mnn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One service that unifies Swisscom LPN **and&lt;/em&gt;* The Things Network webhooks into a single ThingsBoard CE backend — without paying for the Professional Edition.*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;ThingsBoard&lt;/strong&gt; is one of the most popular open-source IoT platforms out there — 18k+ GitHub stars, used in production by teams from smart-building integrators to industrial automation shops, and shipped with a genuinely generous &lt;strong&gt;Community Edition (CE)&lt;/strong&gt; that you can self-host for free. Device management, rule chains, dashboards, alarms — it's all in CE.&lt;/p&gt;

&lt;p&gt;But if you've tried to wire &lt;strong&gt;LoRaWAN&lt;/strong&gt; devices into ThingsBoard CE specifically, you've probably hit the same wall I did: &lt;strong&gt;ThingsBoard's LoRaWAN integration ships with the Professional Edition only&lt;/strong&gt;. CE users get MQTT, HTTP, and CoAP transport adapters — but no first-class adapter for Swisscom LPN, The Things Stack, or any other LoRaWAN network server.&lt;/p&gt;

&lt;p&gt;What made my situation slightly worse: I had devices on &lt;strong&gt;two different LoRaWAN networks&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some sensors on &lt;strong&gt;Swisscom LPN&lt;/strong&gt; — Switzerland's nationwide carrier-grade LoRaWAN network, running on Actility's ThingPark platform.&lt;/li&gt;
&lt;li&gt;Others on &lt;strong&gt;The Things Network v3&lt;/strong&gt; — the global, community-operated LoRaWAN backbone, used by hundreds of thousands of devices worldwide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same physical radio standard. Same destination dashboard. &lt;strong&gt;Two completely different webhook contracts&lt;/strong&gt; — and ThingsBoard CE can't ingest either of them natively.&lt;/p&gt;

&lt;p&gt;So I built an &lt;strong&gt;open-source Spring Boot service&lt;/strong&gt; that sits between &lt;em&gt;both&lt;/em&gt; LoRaWAN networks and ThingsBoard CE. &lt;strong&gt;One service, two networks, one dashboard.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/geda/lorawan-tb-gateway" rel="noopener noreferrer"&gt;https://github.com/geda/lorawan-tb-gateway&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbkq0fvi0398xjjro3gzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbkq0fvi0398xjjro3gzy.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each LoRaWAN network gets its own HTTP endpoint on the gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/swisscom/uplink&lt;/code&gt; — Swisscom LPN's &lt;code&gt;DevEUI_uplink&lt;/code&gt; envelope (Actility shape).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/ttn/uplink&lt;/code&gt; — The Things Stack v3 webhook payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an uplink arrives, the gateway validates the payload, normalizes it into a network-agnostic internal model, makes sure the corresponding device exists in ThingsBoard (creating it if not), and pushes the telemetry to ThingsBoard's device API. The result is a &lt;strong&gt;single ThingsBoard tenant where devices from both LoRaWAN networks coexist&lt;/strong&gt; — same dashboards, same rule chains, same alarm thresholds, same user permissions. A Swisscom-side sensor and a TTN-side sensor are just two devices in the same tenant; they only differ by the &lt;code&gt;source&lt;/code&gt; field in their telemetry stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get out of the box
&lt;/h2&gt;

&lt;p&gt;Beyond just "forwarding webhooks", a few things make this nicer than rolling your own:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-provisioning.&lt;/strong&gt; The first time a deveui you've never seen before sends an uplink, the gateway creates a matching device in ThingsBoard (using the deveui as the device name and a configurable device type), fetches its access token, caches it, and forwards the telemetry. Add a new LoRaWAN sensor in the field, plug it in — it shows up in your ThingsBoard dashboard a few seconds later. No portal clicks, no token copy-paste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTN payload decoding propagated to ThingsBoard.&lt;/strong&gt; If you've configured a TTN payload formatter on your device (the JavaScript decoder in the TTN console), the resulting &lt;code&gt;decoded_payload&lt;/code&gt; object gets &lt;em&gt;flattened&lt;/em&gt; into the ThingsBoard telemetry envelope. A device decoding to &lt;code&gt;{"battery": 3.27, "distance": 548, "leak": 1}&lt;/code&gt; lands in ThingsBoard as three chartable telemetry keys with the right numeric types — no rule-chain massaging needed. You can drag them straight into a widget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A unified payload shape across both networks.&lt;/strong&gt; Swisscom sends payloads as hex strings; TTN sends them as base64. The gateway lower-cases hex, decodes base64 to hex, and presents both as &lt;code&gt;payloadHex&lt;/code&gt; in ThingsBoard. If you're building dashboards that should work for "all my devices" regardless of which LoRaWAN provider they're on, this matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional ThingsBoard backend.&lt;/strong&gt; When the &lt;code&gt;THINGSBOARD_URL&lt;/code&gt; environment variable is unset, the gateway still runs and accepts uplinks — it just logs them instead of forwarding. Useful for local development, integration testing, or sanity-checking your network server's webhook configuration before the dashboard side is ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-network webhook authentication.&lt;/strong&gt; Each endpoint can require a shared-secret header (&lt;code&gt;X-Gateway-Token&lt;/code&gt;) configured at the LoRaWAN provider side. Constant-time comparison, fail-fast startup mode (refuses to boot if a token is missing in prod), oversize-request rejection — the gateway is built to live on the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small footprint.&lt;/strong&gt; The container uses around 80 MB of RAM at idle and runs on ARM (Raspberry Pi 4 is fine). No external dependencies beyond ThingsBoard itself — no Redis, no Kafka, no separate Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the full stack on your laptop
&lt;/h2&gt;

&lt;p&gt;This is where it gets fun. The repo ships a &lt;code&gt;deploy/compose.yml&lt;/code&gt; that gives you the entire pipeline — ThingsBoard CE, the gateway, and (optionally) a Cloudflare Tunnel for public ingress — in a single &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The services
&lt;/h3&gt;

&lt;p&gt;The compose file defines three services on a shared internal Docker network (&lt;code&gt;tb-network&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thingsboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;thingsboard/tb-postgres:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ThingsBoard CE with embedded PostgreSQL. Exposes MQTT (1883), HTTP transport (7070), and CoAP/LWM2M (5683-5688 UDP) on the host. The HTTP &lt;strong&gt;UI&lt;/strong&gt; port (9090 inside the container) is reached via the gateway/tunnel and not published on the host by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;geda73/lorawan-tb-gateway:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This project. Talks to ThingsBoard internally at &lt;code&gt;http://thingsboard:9090&lt;/code&gt;. No host port exposed by default (uncomment a line if you want to curl it directly).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudflared&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cloudflare/cloudflared:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional. Tunnels public webhook traffic from your Cloudflare-managed domain to the gateway, with TLS terminated at Cloudflare's edge. Skip this if you're just running locally.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ThingsBoard data lives in two named volumes (&lt;code&gt;tb-data&lt;/code&gt;, &lt;code&gt;tb-logs&lt;/code&gt;) so it survives &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quickstart
&lt;/h3&gt;

&lt;p&gt;The repo's &lt;code&gt;deploy/&lt;/code&gt; directory has both &lt;code&gt;compose.yml&lt;/code&gt; and a &lt;code&gt;.env.example&lt;/code&gt;. Clone, copy, fill in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/geda/lorawan-tb-gateway.git
&lt;span class="nb"&gt;cd &lt;/span&gt;lorawan-tb-gateway/deploy
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env

&lt;span class="c"&gt;# Edit .env — at minimum:&lt;/span&gt;
&lt;span class="c"&gt;# TB_USERNAME=tenant@thingsboard.org&lt;/span&gt;
&lt;span class="c"&gt;# TB_PASSWORD=&amp;lt;your choice&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# GATEWAY_SECURITY_SWISSCOM_TOKEN=$(openssl rand -hex 32)&lt;/span&gt;
&lt;span class="c"&gt;# GATEWAY_SECURITY_TTN_TOKEN=$(openssl rand -hex 32)&lt;/span&gt;
&lt;span class="c"&gt;# GATEWAY_SECURITY_REQUIRE_TOKEN=true&lt;/span&gt;

docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first boot takes a minute or two — ThingsBoard initializes its database, the gateway waits for ThingsBoard to come up, and then both settle. You can watch progress with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="c"&gt;# or just the gateway:&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logging in for the first time
&lt;/h3&gt;

&lt;p&gt;If you opted out of the Cloudflare tunnel (or you want to access ThingsBoard locally during setup), uncomment the port mapping in the &lt;code&gt;thingsboard&lt;/code&gt; service to expose &lt;code&gt;9090:9090&lt;/code&gt;, then open &lt;a href="http://localhost:9090" rel="noopener noreferrer"&gt;http://localhost:9090&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tb-postgres&lt;/code&gt; image ships with three pre-created accounts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;Default password&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sysadmin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysadmin@thingsboard.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysadmin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tenant admin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tenant@thingsboard.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tenant&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer@thingsboard.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Change all three before exposing anything to the internet.&lt;/strong&gt; The &lt;code&gt;.env.example&lt;/code&gt; comments call this out — &lt;code&gt;TB_PASSWORD&lt;/code&gt; in the env file is what the &lt;em&gt;gateway&lt;/em&gt; will use to log in as the tenant admin; you also need to update the password from ThingsBoard's UI to match (or rotate it once and update both).&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending fake uplinks (no real LoRaWAN hardware required)
&lt;/h3&gt;

&lt;p&gt;The repo ships two PowerShell scripts in &lt;code&gt;scripts/&lt;/code&gt; that post &lt;strong&gt;real-shape&lt;/strong&gt; payloads to a running gateway. Handy for verifying the wiring before you start moving real devices over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Swisscom LPN shape (DevEUI_uplink envelope, FPort/FCntUp as quoted strings)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/scripts/send-swisscom-uplink.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-BaseUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-DeviceEui&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;70B3D57ED0001234&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PayloadHex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AABBCC&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# TTN v3 shape (end_device_ids + base64 frm_payload + optional decoded_payload)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/scripts/send-ttn-uplink.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-BaseUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-DeviceEui&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;70B3D58FF00000BB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each script takes a &lt;code&gt;-Token&lt;/code&gt; parameter that defaults to the matching &lt;code&gt;GATEWAY_SECURITY_*_TOKEN&lt;/code&gt; environment variable — set it once in your shell, run the scripts bare. The first successful uplink creates the device in ThingsBoard; subsequent uplinks add telemetry rows to it. Watch it land in &lt;strong&gt;Devices → 70B3D57ED0001234 → Latest Telemetry&lt;/strong&gt; in the ThingsBoard UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring real LoRaWAN webhooks
&lt;/h3&gt;

&lt;p&gt;Once the local stack is happy, point your real network servers at the gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swisscom LPN portal&lt;/strong&gt; → your Application Server connection → set the destination URL to &lt;code&gt;https://&amp;lt;your-host&amp;gt;/api/v1/swisscom/uplink&lt;/code&gt;, add a custom header &lt;code&gt;X-Gateway-Token: &amp;lt;value of GATEWAY_SECURITY_SWISSCOM_TOKEN&amp;gt;&lt;/code&gt;, save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTN console&lt;/strong&gt; → Application → Integrations → Webhooks → Add webhook → URL &lt;code&gt;https://&amp;lt;your-host&amp;gt;/api/v1/ttn/uplink&lt;/code&gt;, add a header &lt;code&gt;X-Gateway-Token: &amp;lt;value of GATEWAY_SECURITY_TTN_TOKEN&amp;gt;&lt;/code&gt;, enable "uplink message", save.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're using the Cloudflare Tunnel from the compose stack, your &lt;code&gt;&amp;lt;your-host&amp;gt;&lt;/code&gt; is whatever hostname you configured in Cloudflare Zero Trust. If you're running on a public VM, that's your domain name (terminate TLS in front — Caddy / nginx / Cloudflare Access all work). &lt;strong&gt;Don't expose port 8080 directly to the internet&lt;/strong&gt; — the service is designed to live behind a TLS terminator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tearing it down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down              &lt;span class="c"&gt;# stop containers, keep data&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;           &lt;span class="c"&gt;# nuke everything including the TB database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TB database is in a named volume, so &lt;code&gt;docker compose down&lt;/code&gt; alone leaves your devices, dashboards, and rule chains intact for the next boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few design choices worth knowing
&lt;/h2&gt;

&lt;p&gt;For readers who'll dig into the code, three decisions that aren't obvious from the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ThingsBoard REST SDK.&lt;/strong&gt; The official &lt;code&gt;org.thingsboard:rest-client&lt;/code&gt; is broken under Spring Boot 4 (Jackson 3 vs Jackson 2 incompatibility in its login flow). The gateway calls TB's REST API directly via Spring's modern &lt;code&gt;RestClient&lt;/code&gt; — about 200 lines, all the response shapes modeled as Java records. Shorter than the SDK once you only use what you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT caching with retry-on-401, not refresh-token plumbing.&lt;/strong&gt; TB issues short-lived JWTs. Instead of tracking expiry and refreshing proactively, the gateway caches the JWT, and on a 401 it invalidates and re-logs in &lt;em&gt;once&lt;/em&gt;. If that 401s too, propagate — credentials are wrong, not just expired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ThingsBoard adapter is optional via &lt;code&gt;@ConditionalOnProperty&lt;/code&gt;.&lt;/strong&gt; When &lt;code&gt;THINGSBOARD_URL&lt;/code&gt; is unset, the adapter beans aren't created, the forwarder receives empty &lt;code&gt;Optional&amp;lt;&amp;gt;&lt;/code&gt;s, and uplinks are logged instead of forwarded. The HTTP endpoints still respond &lt;code&gt;202 Accepted&lt;/code&gt;. This is what makes &lt;code&gt;mvn spring-boot:run&lt;/code&gt; on a developer laptop Just Work without a local ThingsBoard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full code is on GitHub — these three are the ones that took the longest to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: it's a webhook receiver, treat it like one
&lt;/h2&gt;

&lt;p&gt;Both network servers post over the public internet. The gateway ships three layers of defense, all configurable via env vars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-network shared secret.&lt;/strong&gt; Each endpoint can require &lt;code&gt;X-Gateway-Token: &amp;lt;value&amp;gt;&lt;/code&gt;, constant-time compared. Configure the matching secret as a custom header on the Swisscom/TTN webhook integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-fast startup.&lt;/strong&gt; Set &lt;code&gt;GATEWAY_SECURITY_REQUIRE_TOKEN=true&lt;/code&gt; and the service refuses to start unless both tokens are configured. Recommended for production — it prevents the "I forgot the env var" fail-open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body-size cap.&lt;/strong&gt; &lt;code&gt;GATEWAY_SECURITY_MAX_BODY_BYTES&lt;/code&gt; (default 32 KB) rejects oversize requests with &lt;code&gt;413 Content Too Large&lt;/code&gt; before Jackson reads the body. Real LoRaWAN uplinks are well under 4 KB; the cap kills the easy DoS vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shared-secret model has a known limitation: it's replayable if leaked. HMAC verification (binding the secret to the request body) is on the roadmap for Swisscom — Actility supports it natively. TTN doesn't sign webhook bodies, so the right answer there is &lt;strong&gt;Cloudflare Access&lt;/strong&gt; (or any zero-trust front-door) in front, which is what my production setup uses anyway and what the bundled &lt;code&gt;compose.yml&lt;/code&gt; is wired for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints worth knowing
&lt;/h2&gt;

&lt;p&gt;To be upfront about what this is &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-tenant.&lt;/strong&gt; One ThingsBoard tenant per gateway instance. For SaaS-style hosting you'd want per-token tenant binding — happy to take a PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uplink-only.&lt;/strong&gt; Telemetry flows up; there's no downlink endpoint yet, so actuator devices need a separate path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No queue.&lt;/strong&gt; Failures during the TB call surface as a failed &lt;code&gt;POST&lt;/code&gt; back to your network server. Both Swisscom and TTN retry, so for short outages it's fine. For longer ones, you'd want a durable buffer in front.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those is a hard requirement for you, the repo's a starting point, not a finished product. The architecture is deliberately set up so adding a third LoRaWAN network is a new DTO, a new mapping method, and a new controller — everything downstream of the common internal &lt;code&gt;UplinkMessage&lt;/code&gt; stays untouched.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/geda/lorawan-tb-gateway" rel="noopener noreferrer"&gt;https://github.com/geda/lorawan-tb-gateway&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Docker image:&lt;/strong&gt; &lt;code&gt;geda73/lorawan-tb-gateway:latest&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;

&lt;p&gt;If you're running ThingsBoard CE and have LoRaWAN devices on Swisscom LPN, The Things Network, or both — this should slot right in. If you're on a third LoRaWAN provider (Helium, ChirpStack, Loriot, Senet…) and want to add support, the extension pattern is documented in the README and the codebase is small enough to fork in an afternoon.&lt;/p&gt;

&lt;p&gt;A star on the repo helps other ThingsBoard CE users find it. Bug reports, "it worked with my LoRaWAN provider" feedback, and pull requests for additional network integrations are all welcome.&lt;/p&gt;

</description>
      <category>lorawan</category>
      <category>java</category>
      <category>lpn</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
