Skip to main content

ThingsBoard - Zitadel (Bootstrap and User Sync)

Last reviewed: 2026-03-21

This document turns the received manual instructions into a concrete technical flow to link ThingsBoard with Zitadel, make the portal operational, and prepare future automation.

Short summary

The real flow today has four phases:

  1. Create or choose the bootstrap user that will act as the main administrator in ThingsBoard.
  2. Configure the Zitadel OAuth 2.0 provider inside ThingsBoard.
  3. Configure in the portal the base credentials used to call the ThingsBoard API.
  4. Sync users from the portal and let the OAuth popup handle normal user login.

In the current state of the repo:

  • User provisioning in ThingsBoard is already partially automated.
  • Metadata persistence in user-service / Zitadel is already automated.
  • User status verification in ThingsBoard is already automated.
  • The end-user OAuth popup login is already automated.
  • The initial OAuth 2.0 configuration inside ThingsBoard is still manual.
  • Secure handling of portal base credentials can be solved via templates and generated runtime config.

Operational translation of the instructions

The instructions you received:

  • create the main/admin Things user;
  • use that user to enter OAuth settings and configure Zitadel;
  • put that user's information in subsystems.json for API access;
  • then sync the user;

translate into the following concrete flow:

  1. Create or select in Zitadel the human user that will be the main ThingsBoard admin.
  2. Create that user in ThingsBoard as SYS_ADMIN.
  3. Log into ThingsBoard with that user and configure Security -> OAuth 2.0 for Zitadel.
  4. Adjust platform/portal/ui/src/config/subsystems.json.template and regenerate runtime config so the portal can authenticate against the ThingsBoard API.
  5. Use the portal to sync additional users as system or tenant.
  6. Let each user do a normal login in the portal Things app via the OAuth popup.

What the code already does

1. The portal reads base configuration from generated runtime config

Relevant files:

  • platform/portal/ui/src/config/subsystems.json.template
  • deployment/scripts/config-generators/generate-runtime-configs.sh

The things subsystem currently contains:

  • apiBaseUrl
  • adminUsername
  • adminPassword
  • metadataMap

Key finding:

  • apiBaseUrl, adminUsername, and adminPassword are already materialized from env variables when generating subsystems.json.
  • The portal also derives ThingsBoard technical credentials from THINGSBOARD_SYSTEM_USER_SECRET.

This enables automation without storing literal credentials in the versioned runtime JSON.

2. The portal can already sync users to ThingsBoard

Relevant path:

  • platform/portal/ui/src/app/api/admin/subsystems/sync/thingsboard/route.ts

This route already:

  • logs into ThingsBoard using the subsystem base credentials;
  • creates a SYS_ADMIN or TENANT_ADMIN user;
  • activates the user with a random password;
  • stores metadata in user-service using:
    • things_authority
    • things_tenant_id

Current limitation:

  • it does not create CUSTOMER_USER;
  • it assumes the ThingsBoard base configuration already exists and works.

3. The portal can already verify sync status

Relevant path:

  • platform/portal/ui/src/app/api/admin/subsystems/check/route.ts

This route already checks:

  • SYS_ADMIN via metadata;
  • TENANT_ADMIN by querying the tenant API;
  • CUSTOMER_USER by querying customer + tenant.

It also uses the per-tenant technical user pattern:

  • system_{tenantId}@desarrolloselectronicos.com

and currently depends on a hardcoded password in code for that flow. That should not be kept in the final design.

4. End-user OAuth login is already automated

Relevant files:

  • platform/portal/ui/src/components/apps/ThingsClient/ThingsClient.tsx
  • platform/thingsboard/custom/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java
  • platform/thingsboard/custom/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOauth2AuthenticationSuccessHandler.java

Current flow:

  1. The portal opens a ThingsBoard popup with popup=true.
  2. ThingsBoard uses the configured OAuth client and redirects to Zitadel.
  3. On return, the custom handler returns a page that calls postMessage.
  4. The portal receives THINGSBOARD_SYNC_COMPLETE.
  5. The token is stored locally and the Things iframe is ready.

Conclusion:

  • Post-bootstrap SSO login is already solved.
  • The real bottleneck is the initial bootstrap of the OAuth provider and the base admin user.

What is still manual today

Manual step 1: Configure Zitadel OAuth 2.0 inside ThingsBoard

There is no automation implemented in this repo to create:

  • the OAuth 2.0 client in ThingsBoard;
  • domain mapping;
  • mapper/provisioning strategy;
  • Zitadel OIDC endpoints;
  • the provider label shown on the login screen.

Official documentation confirms the manual UI flow in:

  • Security -> OAuth 2.0
  • client creation
  • assigning the client to a domain

Official reference:

Manual step 2: Choose the correct bootstrap user

Before any scripts, there is a functional decision:

  • which Zitadel user will be the main SYS_ADMIN in ThingsBoard;
  • whether that user is human, technical, or mixed;
  • whether the portal will operate with that same user or with a dedicated service user.

Today this decision is not encoded anywhere; it is simply assumed that there is a user the portal can use as base credentials for the API.

What can be automated now

Automatable right now

  1. Create the bootstrap user in ThingsBoard as SYS_ADMIN. This can be done by reusing the existing sync route with userType=system.

  2. Create TENANT_ADMIN from the portal. This already exists and only requires selecting a tenant.

  3. Save synchronization metadata. Already exists via UserService.saveIntegrationData(...).

  4. Verify existence and status. Already exists via /api/admin/subsystems/check.

  5. Post-bootstrap end-user popup login. Already exists in ThingsClient.tsx.

  6. Render subsystems.json from environment variables. The current code partially supports this; it needs to be standardized in templates or scripts.

Automatable with a small refactor

  1. Generate subsystems.json from templates. Instead of editing JSON by hand, it should look like this:
{
"subsystems": [
{
"id": "things",
"name": "Thingsboard",
"type": "thingsboard",
"apiBaseUrl": "${THINGSBOARD_API_BASE_URL}",
"adminUsername": "THINGSBOARD_PORTAL_SYSADMIN_USER",
"adminPassword": "THINGSBOARD_PORTAL_SYSADMIN_PASS",
"metadataMap": {
"tenantId": "things_tenant_id",
"customerId": "things_customer_id",
"authority": "things_authority"
}
}
]
}
  1. Bootstrap script for the main admin user. The script can:

    • validate ThingsBoard is responding;
    • validate the user exists in Zitadel;
    • invoke the SYS_ADMIN sync flow;
    • verify metadata was persisted.
  2. Validation script for the Things subsystem. The script can:

    • validate login at /api/auth/login;
    • validate /api/tenants;
    • validate the portal can list tenants;
    • validate subsystems.json resolves variables correctly.

Automatable only if we confirm ThingsBoard API capabilities

Automating the internal OAuth configuration in ThingsBoard is only feasible if, on 4.2.1, we confirm one of the following:

  • REST endpoints exposed in Swagger for OAuth client/domain;
  • security settings import/export that includes OAuth;
  • stable access to OAuth entities via internal API.

As of today, there is no evidence in this repo that such automation exists. To avoid introducing a fragile script, this step should remain manual until we review the instance Swagger at runtime.

Phase 1: Minimal manual bootstrap

  1. Install ThingsBoard and ensure it is operational.
  2. Choose the bootstrap user in Zitadel.
  3. Create that user in ThingsBoard as SYS_ADMIN.
  4. Log in with that user and open Security -> OAuth 2.0.
  5. Configure the Zitadel client.
  6. Assign it to the correct domain.

Phase 2: Safe portal automation

  1. Keep subsystems.json referencing environment variables.
  2. Generate those variables from templates or environment secrets.
  3. Validate /api/admin/subsystems/thingsboard/tenants works.
  4. Validate /api/admin/subsystems/check works.

Phase 3: Operational user synchronization

  1. From Portal Admin, sync the user as SYS_ADMIN or TENANT_ADMIN.
  2. Confirm the following were persisted:
    • things_authority
    • things_tenant_id (if applicable)
    • things_customer_id (if applicable in the future)
  3. Log in from the portal Things app to complete the normal OAuth popup flow.

Proposed future automation

Reasonable scripts to implement later:

Script 1: Bootstrap the main admin

Goal:

  • create or verify the bootstrap SYS_ADMIN in ThingsBoard;
  • ensure its metadata is synced;
  • validate the portal can use it as the base account.

Inputs:

  • zitadel_user_id
  • THINGSBOARD_PORTAL_SYSADMIN_USER
  • THINGSBOARD_PORTAL_SYSADMIN_PASS
  • THINGSBOARD_API_BASE_URL

Script 2: Render Things subsystem configuration

Goal:

  • generate platform/portal/ui/src/config/subsystems.json from a template;
  • avoid storing plain-text credentials in the repo;
  • validate the minimum required placeholders.

Script 3: Functional sync healthcheck

Goal:

  • test login to ThingsBoard;
  • list tenants;
  • create or validate a test TENANT_ADMIN;
  • verify metadata stored in user-service.

Script 4: Secrets and hardcodes audit

Goal:

  • remove fixed passwords/usernames from code;
  • detect any credential still stored in:
    • subsystems.json
    • Next.js routes
    • hardcoded per-tenant technical user values

This flow should live alongside:

  • the Things functional guide;
  • the Admin Portal guide;
  • the existing technical audit that flags subsystems.json as a container for operational secrets.

References