A scalable, independent TCP server and IoT Gateway built in Node.js, specifically designed to interface with Teltonika TFT100 (and similar) IoT devices. It handles real-time telemetry ingestion, remote command execution, and robust connection management as a standalone, plug-and-play module decoupled from any specific host application.
- Dedicated TCP Server: Built-in native TCP socket management to maintain persistent connections with IoT devices.
- RESTful API: Manage connected devices and trigger remote commands safely via a secured REST API (
x-api-key). - Telemetry Ingestion: Decodes high-throughput vehicle metrics directly into Postgres and routes them to webhooks.
- Horizontal Scalability: Fully supports multi-instance deployments using PostgreSQL atomic row-level locks (
FOR UPDATE SKIP LOCKED) to prevent race conditions during command retries. - Command State Tracking: Retains a permanent history of all commands sent to devices (Pending, Completed, Failed) with configurable retries.
- Metadata Roundtripping: Attach arbitrary JSON
metadatato any command. The gateway stores it and returns it inside the webhook when the command completes. - Webhook Notifier: Asynchronously dispatches success, failure, telemetry data, and bulk retry notifications to your core backend via HTTP Webhooks.
The gateway acts as the middleman between your Host Application (Backend) and your physical IoT Devices.
- Host Application triggers commands via the
POST /api/v1/commandendpoint. - Command Pipeline (
api/controllers/command.controller.js) queues commands into the PostgreSQL database (tbl_iot_command_logs). - TCP Broker (
index.js&handlers/socket.handler.js) maintains active device connections and writes binary encoded commands to the active socket. - Packet Handlers (
handlers/packet.handler.js) asynchronously decode inbound binary responses and telemetry streams from devices. - Telemetry Layer (
services/telemetry.service.js) parses incoming location/sensor data and inserts logs into the database. - Notification Layer (
services/notification.service.js) dispatches HTTP webhooks back to the Host Application containing command execution confirmations. - Cron Retries (
jobs/cron.job.js) background jobs automatically retry and eventually fail timed-out commands.
Create a .env file in the root directory:
DATABASE_URL=postgresql://user:password@localhost:5432/iot_db
API_PORT=8091
BROKER_PORT=9000
IOT_API_KEY=your_secure_api_key_here
NOTIFICATION_URL=http://your-backend.com
TELEMETRY_WEBHOOK_ENABLED=true
VALIDATE_COMMAND_RESPONSE=trueThe project uses Prisma purely for schema documentation and migration management. The gateway requires exactly two tables (tbl_iot_command_logs, tbl_iot_telemetry_data).
npx prisma db pushAll endpoints require the x-api-key header for authentication.
GET /api/v1/devices- List all currently connected device IMEIs.GET /api/v1/devices/:imei- Get connection metadata for a specific device.GET /api/v1/stats- Get server performance and connection statistics.POST /api/v1/command- Queue a command for a connected device.- Body:
{ "imei": "350612345678901", "command": "setdigout 1?", "metadata": { "job_id": "123", "action": "START" } }
- Body:
The gateway dispatches HTTP POST requests to NOTIFICATION_URL.
Dispatched when a device confirms a command.
POST /webhooks/iot/v1/command/confirm
{
"identified": true,
"imei": "350612345678901",
"command": "setdigout 1?",
"response": "DOUT1:1",
"metadata": { "job_id": "123", "action": "START" }
}Dispatched when a command permanently fails after exhausting all retries.
POST /webhooks/iot/v1/command/bulk_failure
{
"entries": [
{
"command_log_id": "1",
"imei": "350612345678901",
"command": "setdigout 1?",
"metadata": { "job_id": "123", "action": "START" }
}
]
}Dispatched when new data is received (if TELEMETRY_WEBHOOK_ENABLED=true).
POST /webhooks/iot/v1/telemetry
{
"imei": "350612345678901",
"recordCount": 5,
"latestTimestamp": "2023-10-01T12:00:00Z",
"latestGps": { "latitude": 40.7128, "longitude": -74.0060, "speed": 45 },
"latestAttributes": { "internal_battery_percent": 85 }
}The following sections provide a detailed step-by-step analysis of the asynchronous data flows between your Backend, the Gateway, and the IoT Device.
- Host App Issues Command: Your backend decides an IoT actions needs to happen (e.g., unlocking a vehicle). It calls the Gateway's
POST /api/v1/commandendpoint, providing theimei, the raw stringcommand, and an arbitrary JSONmetadataobject (e.g.,{"workflow_id":"W-100"}). - Gateway Validates Structure: The REST API controller verifies the device is actively tracking a TCP socket with the matching
imei. - Database Ledger: The
iot.service.jsinserts aPENDINGcommand log intotbl_iot_command_logswith a futureestimated_timeout_atvalue. It saves themetadataJSON blob exactly as passed. - Binary Encoding: The command string is passed to the Device Manager, encoded into a specific protocol (e.g., Teltonika Codec 12), and flushed to the active TCP Socket buffer. Current execution on the REST API ends here, returning an immediate
200 OK("Command sent"). - Asynchronous Wait: The Gateway goes idle, maintaining the connection.
- Device Response: The physical IoT hardware executes the requested action and responds with a binary TCP payload containing a confirmation string.
- Decoding & Matching: The Gateway's
packet.handler.jsdetects the Codec 12 response, decodes it into a string, and triggersiot.service.jsto look for the oldestPENDINGlog for thatimei. - Finalization & Webhooks: The log is marked
COMPLETED. The Gateway reads the originalmetadataobject from the database row and fires a webhook toNOTIFICATION_URLcontaining BOTH the hardware's confirmation string and the originalmetadata. Your backend receives this hook and advances its own business logic logic (e.g. marking the workflow complete).
Because IoT ecosystems are distributed and run over unstable 2G/4G networks, the Gateway is designed to be resilient in split-brain paradigms.
- Trigger: The broker tries to write a command to a socket, but the device is actually offline (half-open connection).
- Handling: The REST API successfully queued the
PENDINGcommand, but the device never responds over the TCP socket. AfterIOT_COMMAND_TIMEOUTseconds, the Background Cron Job queries the database for timed-out commands. It increments the generic retry counter by 1. The broker attempts to write the binary packet again over the actively tracked socket. This repeatsDEFAULT_MAX_IOT_COMMAND_RETRYtimes. - Outcome: If the device never comes online or the socket ultimately drops, the Cron Job eventually marks the database row as
FAILEDand dispatches thebulk_failurewebhook to the Host App.
- Trigger: A device successfully executes a command and the Gateway attempts to fire the completion webhook (
NOTIFICATION_URL), but the Host App's API is temporarily unreachable (e.g., 502 Bad Gateway). - Handling: The Gateway marks the command
COMPLETEDin the database immediately after receiving the TCP bytes. The webhook attempt is fired but fails loudly in the Gateway's internal logs (axios.postexception). - Outcome: Because the Gateway does not implement complex webhook retry queues (it assumes Host APIs are highly available), the Host App misses the webhook. However, Host Apps can periodically reconcile state manually by directly querying
tbl_iot_command_logsvia standard PostgreSQL queries or Prisma, searching for pending jobs in their own tables that align withCOMPLETEDcommands in the Gateway database.
- Trigger: The Host App fires multiple commands to the same IMEI in rapid succession.
- Handling — Same Command (Duplicates): If the same command string is fired 5 times (e.g., "Start Vehicle" x5), the
sendCommandroutine marks all olderPENDINGentries with the same command string asFAILED(superseded). Only the latest one staysPENDING. The device may execute duplicates, but confirmation matching only resolves the most recent entry. - Handling — Different Commands (Concurrent): If different command strings are fired (e.g., "Start Vehicle", then "getinfo"), each command gets its own independent
PENDINGentry. They coexist concurrently. When a response arrives, the Gateway matches it to the correctPENDINGlog by validating the response against each command's expected response pattern (defined inIOT_COMMANDS). For custom/raw commands not defined inIOT_COMMANDS, the Gateway falls back to oldest-first (FIFO) matching. - Outcome: Duplicate commands are de-duplicated to prevent stale webhook noise, while genuinely different commands execute and confirm independently.
- Trigger: The device receives the command, executes it physically (the vehicle unlocks), but drives into a tunnel and loses 4G before sending the TCP response back.
- Handling: From the Gateway's perspective, the device never executed the command. The Cron Job eventually triggers a retry. The Gateway sends the "Start Vehicle" binary packet again once the device reconnects.
- Outcome: Teltonika devices handle redundant commands natively (responding with "Already set to 1"). The Gateway parses these secondary confirmation strings equally and marks the command
COMPLETED, firing the success webhook.