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:
- Create or choose the bootstrap user that will act as the main administrator in ThingsBoard.
- Configure the Zitadel OAuth 2.0 provider inside ThingsBoard.
- Configure in the portal the base credentials used to call the ThingsBoard API.
- 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.jsonfor API access; - then sync the user;
translate into the following concrete flow:
- Create or select in Zitadel the human user that will be the main ThingsBoard admin.
- Create that user in ThingsBoard as
SYS_ADMIN. - Log into ThingsBoard with that user and configure
Security -> OAuth 2.0for Zitadel. - Adjust
platform/portal/ui/src/config/subsystems.json.templateand regenerate runtime config so the portal can authenticate against the ThingsBoard API. - Use the portal to sync additional users as
systemortenant. - 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.templatedeployment/scripts/config-generators/generate-runtime-configs.sh
The things subsystem currently contains:
apiBaseUrladminUsernameadminPasswordmetadataMap
Key finding:
apiBaseUrl,adminUsername, andadminPasswordare already materialized from env variables when generatingsubsystems.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_ADMINorTENANT_ADMINuser; - activates the user with a random password;
- stores metadata in
user-serviceusing:things_authoritythings_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_ADMINvia metadata;TENANT_ADMINby querying the tenant API;CUSTOMER_USERby 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.tsxplatform/thingsboard/custom/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.javaplatform/thingsboard/custom/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOauth2AuthenticationSuccessHandler.java
Current flow:
- The portal opens a ThingsBoard popup with
popup=true. - ThingsBoard uses the configured OAuth client and redirects to Zitadel.
- On return, the custom handler returns a page that calls
postMessage. - The portal receives
THINGSBOARD_SYNC_COMPLETE. - 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_ADMINin 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
-
Create the bootstrap user in ThingsBoard as
SYS_ADMIN. This can be done by reusing the existing sync route withuserType=system. -
Create
TENANT_ADMINfrom the portal. This already exists and only requires selecting a tenant. -
Save synchronization metadata. Already exists via
UserService.saveIntegrationData(...). -
Verify existence and status. Already exists via
/api/admin/subsystems/check. -
Post-bootstrap end-user popup login. Already exists in
ThingsClient.tsx. -
Render
subsystems.jsonfrom environment variables. The current code partially supports this; it needs to be standardized in templates or scripts.
Automatable with a small refactor
- Generate
subsystems.jsonfrom 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"
}
}
]
}
-
Bootstrap script for the main admin user. The script can:
- validate ThingsBoard is responding;
- validate the user exists in Zitadel;
- invoke the
SYS_ADMINsync flow; - verify metadata was persisted.
-
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.jsonresolves variables correctly.
- validate login at
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.
Recommended flow to document (and later script)
Phase 1: Minimal manual bootstrap
- Install ThingsBoard and ensure it is operational.
- Choose the bootstrap user in Zitadel.
- Create that user in ThingsBoard as
SYS_ADMIN. - Log in with that user and open
Security -> OAuth 2.0. - Configure the Zitadel client.
- Assign it to the correct domain.
Phase 2: Safe portal automation
- Keep
subsystems.jsonreferencing environment variables. - Generate those variables from templates or environment secrets.
- Validate
/api/admin/subsystems/thingsboard/tenantsworks. - Validate
/api/admin/subsystems/checkworks.
Phase 3: Operational user synchronization
- From Portal Admin, sync the user as
SYS_ADMINorTENANT_ADMIN. - Confirm the following were persisted:
things_authoritythings_tenant_id(if applicable)things_customer_id(if applicable in the future)
- 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_ADMINin ThingsBoard; - ensure its metadata is synced;
- validate the portal can use it as the base account.
Inputs:
zitadel_user_idTHINGSBOARD_PORTAL_SYSADMIN_USERTHINGSBOARD_PORTAL_SYSADMIN_PASSTHINGSBOARD_API_BASE_URL
Script 2: Render Things subsystem configuration
Goal:
- generate
platform/portal/ui/src/config/subsystems.jsonfrom 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
Related documentation recommendations
This flow should live alongside:
- the Things functional guide;
- the Admin Portal guide;
- the existing technical audit that flags
subsystems.jsonas a container for operational secrets.
References
- ThingsBoard OAuth 2.0 support: https://thingsboard.io/docs/user-guide/oauth-2-support/
- ThingsBoard REST API reference: https://thingsboard.io/docs/reference/rest-api/