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
- User logs in with backend credentials (
POST /api/v1/auth/login) useThingsboardAuthhook detects successful auth, fetches TB credentials from backend- Frontend calls
ThingsboardService.init(url)+ThingsboardService.login(username, password) - TB JWT stored in
thingsboardStore(persisted to localStorage) - Frontend uses both
api(backend) andThingsboardService/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:
POST ${baseUrl}/api/auth/loginwith{ username, password }- Extract
tokenfrom responsedata.token - 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 totimeoutms (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.