ChirpStack v4 / TTN v3 ↔ ThinkLink Integration Bridge (BizCode 90004)
A generic MQTT forwarding bridge that connects ChirpStack v4 or TTN v3 LoRaWAN devices to the ThinkLink platform bidirectionally, with no changes required on the device side.
1. Overview
This bridge is a generic MQTT forwarding bridge for operators who already run LoRaWAN devices on ChirpStack v4 or TTN v3 (The Things Stack) and want to bring device data into the ThinkLink IoT platform. It consists of four ThinkLink forwarders, each handling one direction for one NS (uplink: device → platform; downlink: platform → device). Uplink messages from both NS types are normalized into ThinkLink's AS standard format at the forwarding layer, then decoded by the platform's thing model and written to telemetry storage. Downlink RPCs issued by the platform are forwarded to the corresponding NS's downlink queue endpoint. Adding new devices requires no script changes — simply pre-create the device entity in ThinkLink and bind it to a thing model.
2. Architecture & Data Flow
2.1 Uplink
┌─────────────────────────────────────────────────────────┐
│ External NS MQTT Broker │
│ ChirpStack: application/{appId}/device/{devEui}/event/up │
│ TTN: v3/{appId}@{tenantId}/devices/{devId}/up │
└────────────────────┬────────────────────────────────────┘
│ Subscribe (source broker)
▼
┌───────────────────────┐
│ ThinkLink Forwarder │
│ forwardScript() │
│ (chirpstack-up or │
│ ttn-up) │
│ reads tenant from │
│ org_params │
│ normalizes to AS fmt │
└───────────┬───────────┘
│ Publish (target broker)
▼
┌────────────────────────────────┐
│ ThinkLink AS MQTT Broker │
│ /v32/{tenant}/as/up/data/{eui} │
└────────────────────────────────┘
│
▼
Platform thing model (payload_parser_type=CHIRPSTACK) decodes
→ telemetry stored / triggers / alerts firedHop-by-hop notes:
- External NS Broker → Forwarder: The forwarder subscribes to the NS uplink topic using wildcards (
+) to match all applications and devices, receiving the raw uplink JSON. - Forwarder processing: Extracts EUI, fPort, base64 payload, and gateway RSSI/SNR/timestamp from the raw message; normalizes them into ThinkLink AS uplink format; reads
org_params['lns.tenant']to construct the target topic. - ThinkLink AS Broker → Platform: The platform consumes the AS MQTT message, matches the device by EUI, invokes the thing model codec (
payload_parser_type=CHIRPSTACK) to decode the payload, and writes telemetry and fires triggers/alerts.
2.2 Downlink
Platform RPC / scheduled task / manual downlink
│
▼
┌────────────────────────────────┐
│ ThinkLink AS MQTT Broker │
│ /v32/{tenant}/as/dn/data/{eui} │
└────────────────────────────────┘
│ Subscribe (source broker = AS)
▼
┌───────────────────────┐
│ ThinkLink Forwarder │
│ forwardScript() │
│ (chirpstack-down or │
│ ttn-down) │
│ extracts eui from │
│ topic trailing segment│
│ builds NS downlink │
│ queue message body │
└───────────┬───────────┘
│ Publish (target = NS broker)
▼
┌──────────────────────────────────────────────────────────┐
│ External NS MQTT Broker │
│ ChirpStack: application/{appId}/device/{devEui}/command/down │
│ TTN: v3/{appId}@{tenantId}/devices/{devId}/down/push │
└──────────────────────────────────────────────────────────┘Hop-by-hop notes:
- Platform → AS Broker: When the platform issues a downlink RPC, it publishes the downlink message to the AS downlink topic, with the device EUI as the trailing segment.
- Forwarder processing: Extracts the EUI from the topic's trailing segment; reads fPort, payload, and confirmed flag from
msg.userdata; builds the NS-specific downlink queue message body; readsappId/tenantIdfromorg_paramsto construct the target topic. - AS Broker → External NS: The NS receives the queued downlink and transmits it to the device at the next available downlink window (Class A) or immediately (Class C).
Single-NS convention (current release): Both
chirpstack-downandttn-downsubscribe to the same AS downlink topic/v32/{tenant}/as/dn/data/+. In this release, one ThinkLink tenant connects to only one NS — enable only the*-downforwarder for the NS in use, and keep the other disabled, to prevent the same downlink from being dispatched by both NS simultaneously. Multi-NS support (per-device NS routing) is planned for a future release.
3. Prerequisites
Before deploying this bridge, confirm the following:
- Devices pre-created in ThinkLink: Every LoRa device must have a device entity in ThinkLink identified by its EUI. The EUI must match the NS side (case-insensitive; the bridge normalizes to lowercase).
- Thing model type is
CHIRPSTACK: The telemetry thing model bound to the device must havepayload_parser_typeset toCHIRPSTACK, with a codec implementing the standard function signatureDecode(fPort, bytes)— ThinkLink uses this to decode the normalized uplink payload. - NS MQTT Broker reachable: The ChirpStack or TTN MQTT broker address, port, and credentials must be known, and the server hosting ThinkLink must be able to reach the broker over the network.
- ThinkLink AS Broker credentials: Obtain the internal AS MQTT broker connection credentials from the platform system configuration (the address is typically an internal hostname or IP).
4. Uplink Integration
4.1 ChirpStack v4 Uplink
Forwarder subscribe topic (on ChirpStack broker):
application/{appId}/device/{devEui}/event/upDeployed with the wildcard pattern application/+/device/+/event/up to match all devices.
Field mapping:
| ThinkLink AS normalized field | ChirpStack v4 raw field | Notes |
|---|---|---|
eui | msg.deviceInfo.devEui | Converted to lowercase |
userdata.port | msg.fPort | Required; frame dropped if missing |
userdata.seqno | msg.fCnt | Frame counter |
userdata.payload | msg.data | base64; required; frame dropped if missing |
gwrx[0].eui | msg.rxInfo[0].gatewayId | Converted to lowercase |
gwrx[0].rssi | msg.rxInfo[0].rssi | dBm |
gwrx[0].lsnr | msg.rxInfo[0].snr | dB |
gwrx[0].time | msg.rxInfo[0].time | ISO 8601 |
4.2 TTN v3 Uplink
Forwarder subscribe topic (on TTN broker):
v3/{appId}@{tenantId}/devices/{devId}/upDeployed with the wildcard pattern v3/+/devices/+/up.
Field mapping:
| ThinkLink AS normalized field | TTN v3 raw field | Notes |
|---|---|---|
eui | msg.end_device_ids.dev_eui | Converted to lowercase |
userdata.port | msg.uplink_message.f_port | Required |
userdata.seqno | msg.uplink_message.f_cnt | Frame counter |
userdata.payload | msg.uplink_message.frm_payload | base64; required |
gwrx[0].eui | msg.uplink_message.rx_metadata[0].gateway_ids.eui | Converted to lowercase |
gwrx[0].rssi | msg.uplink_message.rx_metadata[0].rssi | dBm |
gwrx[0].lsnr | msg.uplink_message.rx_metadata[0].snr | dB |
gwrx[0].time | msg.uplink_message.rx_metadata[0].time | ISO 8601 |
If the NS does not include gateway receive metadata (
rxInfo/rx_metadatais absent or empty),gwrxwill be an empty array[]. This does not affect telemetry storage — the platform AS accepts an emptygwrx.
4.3 Normalized ThinkLink AS Uplink Format
After processing, messages from both NS types are published in the following format:
Publish topic: /v32/{tenant}/as/up/data/{eui} ({eui} in lowercase)
{
"if": "loraWAN",
"type": "data",
"version": "3.0",
"moteeui": "6353012b00021678",
"gwrx": [
{
"eui": "a840411e5f8e7200",
"rssi": -85,
"lsnr": 7.5,
"time": "2026-06-14T08:00:00Z"
}
],
"userdata": {
"adr": false,
"port": 1,
"class": "ClassA",
"seqno": 123,
"payload": "AQIDBA==",
"confirmed": false
}
}Required fields: Both userdata.payload (base64) and userdata.port must be present and non-empty. If either is missing, the forwarder returns null and the frame is silently discarded (not forwarded, no error).
5. Downlink Integration
When the platform issues an RPC or manual downlink, ThinkLink AS broker publishes to:
AS downlink entry topic: /v32/{tenant}/as/dn/data/{eui}
The forwarder reads the following fields from the message:
| Field | Meaning |
|---|---|
msg.userdata.port | LoRa fPort; required |
msg.userdata.payload | base64 downlink payload; required |
msg.userdata.confirmed | Whether a confirmed frame is requested (optional; defaults to false) |
5.1 Forward to ChirpStack v4
Publish topic: application/{appId}/device/{eui}/command/down
appId is read from org_params['lns.chirpstack.appId'].
{
"devEui": "6353012b00021678",
"confirmed": false,
"fPort": 1,
"data": "AQIDBA=="
}5.2 Forward to TTN v3
Publish topic: v3/{appId}@{tenantId}/devices/{devId}/down/push
appId is read from org_params['lns.ttn.appId']; tenantId from org_params['lns.ttn.tenantId'].
{
"downlinks": [
{
"f_port": 1,
"frm_payload": "AQIDBA==",
"priority": "NORMAL",
"confirmed": false
}
]
}TTN downlink device_id mapping: TTN downlink topics require a string devId (not a bare EUI). The forwarder resolves devId as follows:
- First checks
org_params['lns.ttn.deviceMap'][eui]— an operator-maintained explicit mapping. - If the map is empty or the EUI has no entry, falls back to using the EUI itself as
devId(works when the TTN device id matches the EUI, which is a common convention).
6. Configuration (org_params)
The tenant administrator maintains the following five keys under System Settings → Server Configuration → Organization Parameters:
| Key | Purpose | Example |
|---|---|---|
lns.tenant | ThinkLink tenant code; used to construct AS topics /v32/{tenant}/as/... | demo |
lns.chirpstack.appId | ChirpStack application id (integer string) | 1 |
lns.ttn.appId | TTN application id | my-app |
lns.ttn.tenantId | TTN tenant id (The Things Stack multi-tenancy identifier) | ttn |
lns.ttn.deviceMap | JSON object {"eui": "ttn-device-id"}; use {} if TTN device id matches the EUI | {"a1b2c3d4e5f60708":"my-sensor-01"} |
Security: These are operational settings. Do not commit them to any code repository. Broker credentials and API keys are maintained separately in the platform's Broker connection settings and must not be stored in org_params.
7. Deployment Steps
Step 1: Create 3 MQTT Broker Connections
Platform path: System Settings → Server Configuration → Broker Connection Management
1a. ThinkLink AS Broker (internal)
| Parameter | Description |
|---|---|
| Connection type | AS (platform built-in AS broker) |
| Address / Port | See System Settings → Server Configuration → Internal MQTT for the actual address and port |
| Username / Password | Same as above; use the platform's internal MQTT credentials |
| Role | Target for uplink forwarders; source for downlink forwarders |
1b. ChirpStack Broker
| Parameter | Description |
|---|---|
| Connection type | Customize (external broker) |
| Address / Port | From ChirpStack integrations page → MQTT integration → Server (e.g. mqtt://chirpstack.example.com:1883) |
| Username / Password | Credentials configured in ChirpStack MQTT integration |
| TLS | Match the ChirpStack deployment; enable TLS in production |
| Role | Source for chirpstack-up; target for chirpstack-down |
1c. TTN Broker
| Parameter | Description |
|---|---|
| Connection type | Customize (external broker) |
| Address / Port | From TTN Console → Application → Integrations → MQTT (e.g. mqtts://eu1.cloud.thethings.network:8883) |
| Username / Password | TTN MQTT credentials (username format {appId}@{tenantId}, password is an API Key) |
| TLS | TTN cloud service uses TLS by default |
| Role | Source for ttn-up; target for ttn-down |
Step 2: Create 4 Forwarders
Platform path: Advanced Features → Forwarder → Add
Create one forwarder per row below. Paste the full content of the corresponding forwarder script into the script field.
2a. chirpstack-up (ChirpStack uplink)
| Parameter | Value |
|---|---|
| Name | chirpstack-up |
| Source Broker | ChirpStack broker (Step 1b) |
| Subscribe Topic | application/+/device/+/event/up |
| Target Broker | ThinkLink AS broker (Step 1a) |
2b. ttn-up (TTN uplink)
| Parameter | Value |
|---|---|
| Name | ttn-up |
| Source Broker | TTN broker (Step 1c) |
| Subscribe Topic | v3/+/devices/+/up |
| Target Broker | ThinkLink AS broker (Step 1a) |
2c. chirpstack-down (ChirpStack downlink)
| Parameter | Value |
|---|---|
| Name | chirpstack-down |
| Source Broker | ThinkLink AS broker (Step 1a) |
| Subscribe Topic | /v32/{tenant}/as/dn/data/+ (replace {tenant} with the actual tenant code; must match lns.tenant) |
| Target Broker | ChirpStack broker (Step 1b) |
2d. ttn-down (TTN downlink)
| Parameter | Value |
|---|---|
| Name | ttn-down |
| Source Broker | ThinkLink AS broker (Step 1a) |
| Subscribe Topic | /v32/{tenant}/as/dn/data/+ (same replacement as above) |
| Target Broker | TTN broker (Step 1c) |
Single-NS convention:
chirpstack-downandttn-downsubscribe to the same AS downlink topic. Enable only the*-downforwarder for the NS in use; keep the other disabled. Having both enabled simultaneously causes the same downlink to be dispatched to both NS.
Step 3: Fill in org_params
Platform path: System Settings → Server Configuration → Organization Parameters
Maintain the following keys in the target tenant's organization parameters (only fill keys for the NS you are using):
| Key | Description | Example (replace with real values) |
|---|---|---|
lns.tenant | ThinkLink tenant code | demo |
lns.chirpstack.appId | ChirpStack application id | 1 |
lns.ttn.appId | TTN application id | my-app |
lns.ttn.tenantId | TTN tenant id | ttn |
lns.ttn.deviceMap | {"eui":"ttn-device-id"} map; use {} if device id matches EUI | {"a1b2c3d4e5f60708":"my-sensor-01"} |
Step 4: Pre-create Devices in ThinkLink
For each LoRa device to be connected through this bridge:
- Create a device entity in ThinkLink using the device's EUI (case-insensitive; the bridge normalizes to lowercase).
- Bind a telemetry thing model with
payload_parser_typeset toCHIRPSTACKand a codec implementingDecode(fPort, bytes). - Confirm the EUI is stored as lowercase on the platform side, or that the platform's device lookup is case-insensitive.
Step 5: End-to-End Testing
Uplink verification:
- Send an uplink frame from the ChirpStack / TTN console or a simulator (valid fPort + payload).
- In ThinkLink platform → Device Details → Telemetry, confirm data has been stored.
- If telemetry is empty, check the forwarder run log (Advanced Features → Forwarder → Run Log). A
nullreturn means a required field was missing and the frame was silently discarded. Verifylns.tenant, device pre-creation, and thing model type.
Downlink verification:
- Manually issue an RPC or downlink command to the target device from the ThinkLink platform.
- In ChirpStack → Device → LoRaWAN Frames, or TTN Console → Live Data, confirm the downlink was queued (
command/down/down/pushreceived). - If the downlink did not reach the NS, verify that
{tenant}in the downlink forwarder's subscribe topic has been replaced with the real tenant code, and that the relevant org_params (appId,tenantId) are filled in.
8. Troubleshooting
| Symptom | Likely Cause | How to Investigate |
|---|---|---|
| Uplink telemetry not stored | lns.tenant does not match the actual tenant code | Check org_params and forwarder run log |
| Uplink telemetry not stored | Device not pre-created or EUI mismatch | Inspect the device list in ThinkLink |
| Uplink telemetry not stored | Thing model not set to CHIRPSTACK type | Check the thing model bound to the device |
| Downlink not received by NS | {tenant} placeholder not replaced in downlink forwarder's subscribe topic | Check forwarder configuration |
| Downlink not received by NS | appId / tenantId org_params not filled in | Check org_params configuration page |
| TTN downlink topic error | lns.ttn.deviceMap missing and TTN device id differs from EUI | Add the EUI → device id mapping to lns.ttn.deviceMap |
| Both NS receive the same downlink | chirpstack-down and ttn-down are both enabled | Disable the forwarder for the NS not in use (single-NS convention) |