惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
月光博客
月光博客
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
NISL@THU
NISL@THU
爱范儿
爱范儿
S
Securelist
博客园 - 叶小钗
C
CERT Recently Published Vulnerability Notes
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
G
GRAHAM CLULEY
P
Proofpoint News Feed
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recent Announcements
Recent Announcements
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
博客园 - 【当耐特】
H
Help Net Security
F
Fortinet All Blogs
T
The Blog of Author Tim Ferriss

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
ThingsBoard CE doesn't speak LoRaWAN — here's a Spring Boot bridge that fixes it
David Gerber · 2026-05-15 · via DEV Community

One service that unifies Swisscom LPN **and* The Things Network webhooks into a single ThingsBoard CE backend — without paying for the Professional Edition.*

ThingsBoard 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 Community Edition (CE) that you can self-host for free. Device management, rule chains, dashboards, alarms — it's all in CE.

But if you've tried to wire LoRaWAN devices into ThingsBoard CE specifically, you've probably hit the same wall I did: ThingsBoard's LoRaWAN integration ships with the Professional Edition only. 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.

What made my situation slightly worse: I had devices on two different LoRaWAN networks.

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

Same physical radio standard. Same destination dashboard. Two completely different webhook contracts — and ThingsBoard CE can't ingest either of them natively.

So I built an open-source Spring Boot service that sits between both LoRaWAN networks and ThingsBoard CE. One service, two networks, one dashboard.

Repo: https://github.com/geda/lorawan-tb-gateway

What it does

Each LoRaWAN network gets its own HTTP endpoint on the gateway:

  • POST /api/v1/swisscom/uplink — Swisscom LPN's DevEUI_uplink envelope (Actility shape).
  • POST /api/v1/ttn/uplink — The Things Stack v3 webhook payload.

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 single ThingsBoard tenant where devices from both LoRaWAN networks coexist — 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 source field in their telemetry stream.

What you actually get out of the box

Beyond just "forwarding webhooks", a few things make this nicer than rolling your own:

Auto-provisioning. 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.

TTN payload decoding propagated to ThingsBoard. If you've configured a TTN payload formatter on your device (the JavaScript decoder in the TTN console), the resulting decoded_payload object gets flattened into the ThingsBoard telemetry envelope. A device decoding to {"battery": 3.27, "distance": 548, "leak": 1} 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.

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

Optional ThingsBoard backend. When the THINGSBOARD_URL 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.

Per-network webhook authentication. Each endpoint can require a shared-secret header (X-Gateway-Token) 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.

Small footprint. 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.

Running the full stack on your laptop

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

The services

The compose file defines three services on a shared internal Docker network (tb-network):

Service Image What it does
thingsboard thingsboard/tb-postgres:latest Full ThingsBoard CE with embedded PostgreSQL. Exposes MQTT (1883), HTTP transport (7070), and CoAP/LWM2M (5683-5688 UDP) on the host. The HTTP UI port (9090 inside the container) is reached via the gateway/tunnel and not published on the host by default.
gateway geda73/lorawan-tb-gateway:latest This project. Talks to ThingsBoard internally at http://thingsboard:9090. No host port exposed by default (uncomment a line if you want to curl it directly).
cloudflared cloudflare/cloudflared:latest 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.

The ThingsBoard data lives in two named volumes (tb-data, tb-logs) so it survives docker compose down.

Quickstart

The repo's deploy/ directory has both compose.yml and a .env.example. Clone, copy, fill in:

git clone https://github.com/geda/lorawan-tb-gateway.git
cd lorawan-tb-gateway/deploy
cp .env.example .env
chmod 600 .env

# Edit .env — at minimum:
# TB_USERNAME=tenant@thingsboard.org
# TB_PASSWORD=<your choice>
# GATEWAY_SECURITY_SWISSCOM_TOKEN=$(openssl rand -hex 32)
# GATEWAY_SECURITY_TTN_TOKEN=$(openssl rand -hex 32)
# GATEWAY_SECURITY_REQUIRE_TOKEN=true

docker compose up -d

Enter fullscreen mode Exit fullscreen mode

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:

docker compose logs -f
# or just the gateway:
docker compose logs -f gateway

Enter fullscreen mode Exit fullscreen mode

Logging in for the first time

If you opted out of the Cloudflare tunnel (or you want to access ThingsBoard locally during setup), uncomment the port mapping in the thingsboard service to expose 9090:9090, then open http://localhost:9090.

The tb-postgres image ships with three pre-created accounts:

Role Email Default password
Sysadmin sysadmin@thingsboard.org sysadmin
Tenant admin tenant@thingsboard.org tenant
Customer user customer@thingsboard.org customer

Change all three before exposing anything to the internet. The .env.example comments call this out — TB_PASSWORD in the env file is what the gateway 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).

Sending fake uplinks (no real LoRaWAN hardware required)

The repo ships two PowerShell scripts in scripts/ that post real-shape payloads to a running gateway. Handy for verifying the wiring before you start moving real devices over:

# Swisscom LPN shape (DevEUI_uplink envelope, FPort/FCntUp as quoted strings)
./scripts/send-swisscom-uplink.ps1 -BaseUrl http://localhost:8080 `
    -DeviceEui 70B3D57ED0001234 -PayloadHex AABBCC

# TTN v3 shape (end_device_ids + base64 frm_payload + optional decoded_payload)
./scripts/send-ttn-uplink.ps1 -BaseUrl http://localhost:8080 `
    -DeviceEui 70B3D58FF00000BB

Enter fullscreen mode Exit fullscreen mode

Each script takes a -Token parameter that defaults to the matching GATEWAY_SECURITY_*_TOKEN 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 Devices → 70B3D57ED0001234 → Latest Telemetry in the ThingsBoard UI.

Wiring real LoRaWAN webhooks

Once the local stack is happy, point your real network servers at the gateway:

  • Swisscom LPN portal → your Application Server connection → set the destination URL to https://<your-host>/api/v1/swisscom/uplink, add a custom header X-Gateway-Token: <value of GATEWAY_SECURITY_SWISSCOM_TOKEN>, save.
  • TTN console → Application → Integrations → Webhooks → Add webhook → URL https://<your-host>/api/v1/ttn/uplink, add a header X-Gateway-Token: <value of GATEWAY_SECURITY_TTN_TOKEN>, enable "uplink message", save.

If you're using the Cloudflare Tunnel from the compose stack, your <your-host> 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). Don't expose port 8080 directly to the internet — the service is designed to live behind a TLS terminator.

Tearing it down

docker compose down              # stop containers, keep data
docker compose down -v           # nuke everything including the TB database

Enter fullscreen mode Exit fullscreen mode

The TB database is in a named volume, so docker compose down alone leaves your devices, dashboards, and rule chains intact for the next boot.

A few design choices worth knowing

For readers who'll dig into the code, three decisions that aren't obvious from the README:

  • No ThingsBoard REST SDK. The official org.thingsboard:rest-client 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 RestClient — about 200 lines, all the response shapes modeled as Java records. Shorter than the SDK once you only use what you need.
  • JWT caching with retry-on-401, not refresh-token plumbing. 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 once. If that 401s too, propagate — credentials are wrong, not just expired.
  • The ThingsBoard adapter is optional via @ConditionalOnProperty. When THINGSBOARD_URL is unset, the adapter beans aren't created, the forwarder receives empty Optional<>s, and uplinks are logged instead of forwarded. The HTTP endpoints still respond 202 Accepted. This is what makes mvn spring-boot:run on a developer laptop Just Work without a local ThingsBoard.

The full code is on GitHub — these three are the ones that took the longest to get right.

Security: it's a webhook receiver, treat it like one

Both network servers post over the public internet. The gateway ships three layers of defense, all configurable via env vars:

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

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 Cloudflare Access (or any zero-trust front-door) in front, which is what my production setup uses anyway and what the bundled compose.yml is wired for.

Constraints worth knowing

To be upfront about what this is not:

  • Single-tenant. One ThingsBoard tenant per gateway instance. For SaaS-style hosting you'd want per-token tenant binding — happy to take a PR.
  • Uplink-only. Telemetry flows up; there's no downlink endpoint yet, so actuator devices need a separate path.
  • No queue. Failures during the TB call surface as a failed POST 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.

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 UplinkMessage stays untouched.


Repo: https://github.com/geda/lorawan-tb-gateway
Docker image: geda73/lorawan-tb-gateway:latest
License: MIT

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.

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.