Thingsboard Integration Design

Date: 2026-04-20
Status: Draft

Overview

Integrate Thingsboard (TB) API into the frontend to enable device management, RPC commands, attribute read/write, and telemetry reading — alongside the existing backend API. Users authenticate once with the backend, then the frontend auto-connects to Thingsboard using per-user credentials fetched from the backend.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Frontend                                  │
│  ┌──────────────┐    ┌─────────────────────┐    ┌─────────────┐ │
│  │ authStore    │───▶│ useThingsboardAuth  │───▶│thingsboard │ │
│  │ (Zustand)    │    │ (hook)              │    │Store        │ │
│  └──────────────┘    └─────────────────────┘    └──────┬──────┘ │
│                                                        │        │
│         ┌──────────────────────────────────────────────┘        │
│         ▼                                                       │
│  ┌──────────────────────┐    ┌─────────────────────────────┐  │
│  │ ThingsboardService   │───▶│ TBDevice[] (per device)    │  │
│  │ (singleton, HTTP)    │    │ - get(), update(), delete() │  │
│  │ - login()            │    │ - rpc()                    │  │
│  │ - logout()           │    │ - getAttributes()          │  │
│  │ - init()             │    │ - getTelemetry()           │  │
│  └──────────────────────┘    │ - saveAttributes()         │  │
│                              └─────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ TB HTTP API
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Thingsboard                                 │
│  /api/v1/plugins/rpc/{id}  /api/v1/device/{id}  /api/v1/v1/... │
└─────────────────────────────────────────────────────────────────┘

Flow

  1. User logs in with backend credentials (POST /api/v1/auth/login)
  2. useThingsboardAuth hook detects successful auth, fetches TB credentials from backend
  3. Frontend calls ThingsboardService.init(url) + ThingsboardService.login(username, password)
  4. TB JWT stored in thingsboardStore (persisted to localStorage)
  5. Frontend uses both api (backend) and ThingsboardService/TBDevice (Thingsboard) simultaneously

Backend — TB Credentials Endpoint

GET /api/v1/thingsboard/credentials

Returns Thingsboard credentials for the authenticated user.

Auth: JWT required (Bearer token).

Response (200):

{
  thingsboardUrl: string;  // e.g., "http://localhost:9090/api/v1"
  username: string;        // TB username for this user
  password: string;        // TB password for this user
}

Errors:

  • 401 — Unauthorized (no or invalid JWT)
  • 404 — No TB credentials configured for this user

Implementation

New NestJS module: thingsboard/ with controller, service, and module. Depends on UsersModule to look up per-user TB credentials.

Thingsboard API Reference

Endpoints Used

Operation Endpoint Method
Login /api/auth/login POST
Get Device /api/device/{deviceId} GET
Delete Device /api/device/{deviceId} DELETE
Save Device (create/update) /api/device POST
Get Attributes /api/plugins/telemetry/{deviceId}/values/attributes GET
Get Latest Telemetry /api/plugins/telemetry/{deviceId}/values/timeseries GET
Get Telemetry Range /api/plugins/telemetry/{deviceId}/values/timeseries GET
Save Attributes /api/plugins/telemetry/{deviceId}/{scope} POST
RPC One-way /api/plugins/rpc/oneway/{deviceId} POST
RPC Two-way /api/plugins/rpc/twoway/{deviceId} POST

Auth Header

Thingsboard uses X-Authorization: Bearer ${token} (non-standard header name).

Frontend — Thingsboard Store (src/stores/thingsboard.store.ts)

interface ThingsboardState {
  url: string | null;
  token: string | null;
  isConnected: boolean;
  init(url: string, token: string): void;
  setConnected(connected: boolean): void;
  clear(): void;
}

Persisted to localStorage key thingsboard-storage.

Frontend — ThingsboardService Module (thingsboard.service.ts)

Singleton module-level service.

class ThingsboardService {
  // Setup
  static init(baseUrl: string): void;
  static login(username: string, password: string): Promise<void>;
  static logout(): Promise<void>;
  static getToken(): string | null;
  static isConnected(): boolean;
  static getClient(): AxiosInstance;

  // Device factory
  static device(tbDeviceId: string): TBDevice;

  // Error normalization
  private static normalizeError(error: unknown): never;
}

Login flow:

  1. POST ${baseUrl}/api/auth/login with { username, password }
  2. Extract token from response data.token
  3. Store in thingsboardStore

Internal HTTP client:

  • Base URL: ThingsboardService.init() sets it
  • Auth header: X-Authorization: Bearer ${token}
  • Response interceptor: normalize errors
  • 401 interceptor: attempt re-login once, then clear store

Frontend — TBDevice Class (tb-device.class.ts)

Created via ThingsboardService.device(tbDeviceId). Shares the HTTP client from ThingsboardService.

class TBDevice {
  constructor(tbDeviceId: string) { ... }

  async get(): Promise<TBDeviceInfo>;
  async update(payload: Partial<TBDeviceInfo>): Promise<TBDeviceInfo>;
  async delete(): Promise<void>;

  async getAttributes(keys?: string[]): Promise<Record<string, any>>;
  async saveAttributes(attributes: Record<string, any>): Promise<void>;

  async getTelemetry(
    keys?: string[],
    fromTs?: number,
    toTs?: number
  ): Promise<TelemetryData[]>;
  async getLatestTelemetry(keys?: string[]): Promise<Record<string, any>>;

  async rpc(
    method: string,
    params?: Record<string, any>,
    options?: { waitForResponse?: boolean; timeout?: number }
  ): Promise<any>;
}

RPC Details:

  • If waitForResponse: false: POST /api/v1/plugins/rpc/oneway/{deviceId} with body {"method":"${method}","params":${params}}
  • If waitForResponse: true: POST /api/v1/plugins/rpc/twoway/{deviceId} with same body, wait up to timeout ms (default 5000)
  • Request body is a JSON string per TB spec

Error normalization:
All methods throw in format { message: string, statusCode: number, cause?: unknown }.

Frontend — Auth Hook (useThingsboardAuth.ts)

export const useThingsboardAuth = () => {
  const { isAuthenticated } = useAuthStore();
  const thingsboardStore = useThingsboardStore();

  useEffect(() => {
    if (isAuthenticated && !thingsboardStore.isConnected) {
      // Fetch TB credentials from backend
      const { thingsboardUrl, username, password } = await fetchTBcredentials();
      ThingsboardService.init(thingsboardUrl);
      await ThingsboardService.login(username, password);
      thingsboardStore.setConnected(true);
    }
  }, [isAuthenticated]);

  // Optionally: clear on logout
  useEffect(() => {
    if (!isAuthenticated) {
      ThingsboardService.logout();
      thingsboardStore.clear();
    }
  }, [isAuthenticated]);
};

Dependency: useThingsboardAuth() must be called inside a React context provider that wraps authStore.

Environment Variables

Backend (.env.example):

TB_API_URL=http://localhost:9090/api/v1

Frontend (.env.example):

VITE_TB_URL=http://localhost:9090/api/v1

Files to Create/Modify

Backend

File Action
backend/src/thingsboard/thingsboard.controller.ts New
backend/src/thingsboard/thingsboard.service.ts New
backend/src/thingsboard/thingsboard.module.ts New
backend/src/thingsboard/dto/get-tb-credentials.dto.ts New

Frontend

File Action
frontend/src/stores/thingsboard.store.ts New
frontend/src/services/thingsboard.service.ts New
frontend/src/services/thingsboard/tb-device.class.ts New
frontend/src/hooks/useThingsboardAuth.ts New
frontend/src/services/thingsboard/index.ts New (barrel export)
frontend/.env.example Add VITE_TB_URL

Spec Self-Review

  • All “TBD”/“TODO” placeholders resolved
  • No internal contradictions
  • Architecture matches feature descriptions
  • Scope is single enough for one implementation plan
  • No ambiguous requirements left unresolved

Next Step

Invoke writing-plans skill to create the implementation plan.