Skip to main content

Workspaces & Tenants

A workspace is a shared container for projects, devices, groups, dashboards and workflows. A tenant is a sub-organization inside a workspace, used to segment devices and users — typically one tenant per end customer.

See Roles and permissions for what each role may do.

Workspaces

Base path: /api/workspaces — every route requires authentication, either an API key or a signed-in session.

MethodPathPurpose
GET/api/workspacesList the caller's workspaces
GET/api/workspaces/:workspaceIdGet one workspace with resolved members
POST/api/workspacesCreate a workspace
PUT/api/workspaces/:workspaceIdRename a workspace
DELETE/api/workspaces/:workspaceIdDelete a workspace and all its content
POST/api/workspaces/:workspaceId/usersAdd an existing user directly
PUT/api/workspaces/:workspaceId/users/:userId/roleChange a member's role
DELETE/api/workspaces/:workspaceId/users/:userIdRemove a member
POST/api/workspaces/:workspaceId/invitesInvite a user by email
GET/api/workspaces/:workspaceId/invitesList pending invites
DELETE/api/workspaces/:workspaceId/invites/:inviteIdRevoke an invite
GET/api/workspaces/:workspaceId/variablesRead workspace variables
PUT/api/workspaces/:workspaceId/variablesReplace workspace variables
GET/api/workspaces/:workspaceId/tenant-variable-templatesRead tenant variable templates
PUT/api/workspaces/:workspaceId/tenant-variable-templatesReplace tenant variable templates
GET/api/workspaces/:workspaceId/audit-logsRead the audit trail

List workspaces

GET /api/workspaces

Returns a plain array (no pagination envelope) of every workspace the caller belongs to — as owner, as a member, or through tenant membership only:

[
{
"_id": "664a1f2b8c9d0e0011223344",
"name": "Acme Facilities",
"owner": "8a3f…",
"users": [ { "user": "9b4e…", "role": "member" } ],
"currentUserRole": "owner",
"isTenantOnlyMember": false
}
]

currentUserRole is owner, admin, operator, member, or null for a tenant-only member. For tenant-only members, users, variables and tenantVariableTemplates are omitted and isTenantOnlyMember is true.

Get one workspace

GET /api/workspaces/:workspaceId

Same shape as above, except that users is resolved against the identity provider so each entry carries an email:

{
"_id": "664a1f2b8c9d0e0011223344",
"name": "Acme Facilities",
"users": [
{ "user": "9b4e…", "email": "tech@acme.example", "role": "member" }
],
"currentUserRole": "admin",
"isTenantOnlyMember": false
}

Returns 403 {"message":"Access denied"} when the caller has no access.

note

If the workspace id does not exist, this route falls through to the list behaviour and returns the array of the caller's workspaces rather than a 404.

Create and rename

POST /api/workspaces
PUT /api/workspaces/:workspaceId
{ "name": "Acme Facilities" }

name is required (400 {"message":"Name is required"} otherwise). The caller becomes the owner of a newly created workspace. Renaming requires owner or admin rights.

Delete a workspace

DELETE /api/workspaces/:workspaceId
{ "confirmName": "Acme Facilities" }

confirmName must match the workspace name exactly. Only the owner or an admin may delete.

Destructive

Deleting a workspace also deletes every tenant, invite, device, device group, dashboard, SCADA screen, workflow, build job, extension, audit log, project and firmware belonging to it. There is no undo.

Returns {"message":"Workspace deleted"}.

Members

POST   /api/workspaces/:workspaceId/users
PUT /api/workspaces/:workspaceId/users/:userId/role
DELETE /api/workspaces/:workspaceId/users/:userId

Adding a member requires that the user already has an AppBlocks account:

{ "email": "tech@acme.example", "role": "member" }

role is one of member, operator, admin and defaults to member. An unrecognized role silently falls back to member. If the user is already a member the response is 200 {"message":"user already in workspace"}; if they are the owner, 400. To onboard someone who has no account yet, use an invite instead.

Changing a role requires a valid role value, otherwise 400 {"message":"Invalid role"}. All three routes require owner or admin rights and return the updated workspace.

Workspace invites

POST   /api/workspaces/:workspaceId/invites
GET /api/workspaces/:workspaceId/invites
DELETE /api/workspaces/:workspaceId/invites/:inviteId
{ "email": "newuser@acme.example", "role": "member" }

Creating an invite deletes any earlier pending invite for the same email and workspace, then generates a token valid for 7 days and emails a link (<APP_URL>/invite/<token>) when transactional email is configured.

{
"message": "Invitation sent",
"invite": { "email": "newuser@acme.example", "role": "member", "expiresAt": "…" }
}

Listing invites first prunes expired ones, then returns an array of pending invites with email, role, status, expiresAt, createdAt and token. All three routes require owner or admin rights.

Workspace variables

GET /api/workspaces/:workspaceId/variables
PUT /api/workspaces/:workspaceId/variables

Workspace variables are shared key/value strings available to workspace content. Reading requires membership; writing requires owner or admin rights.

{ "variables": { "MQTT_HOST": "broker.acme.example", "SITE": "riga" } }

PUT replaces the whole map and echoes back the saved result. A missing or non-object variables property returns 400.

Tenant variable templates

GET /api/workspaces/:workspaceId/tenant-variable-templates
PUT /api/workspaces/:workspaceId/tenant-variable-templates

Templates declare which variables each tenant is expected to define:

{
"templates": [
{ "key": "SITE_NAME", "description": "Customer site label", "defaultValue": "" }
]
}

Keys are sanitized on save — every character outside A-Z a-z 0-9 _ is stripped. Reading requires workspace edit rights or tenant membership; writing requires owner or admin rights. A non-array templates returns 400.

Audit logs

GET /api/workspaces/:workspaceId/audit-logs
Query parameterDescription
tenantReturn only that tenant's entries. Without it, only entries not tied to a tenant are returned
limitPage size, default 50, clamped to 1–200
skipOffset, default 0
{
"logs": [
{
"_id": "66c1…",
"user": "8a3f…",
"userEmail": "admin@acme.example",
"type": "firmware",
"workspace": "664a1f2b…",
"tenant": null,
"meta": {
"deviceIds": ["665f3c1e…"],
"versionId": "664a1f9c…",
"projectId": "664a1f2b…",
"versionCode": "13"
},
"createdAt": "2026-08-17T05:40:00.000Z"
}
],
"total": 512,
"deviceNames": { "665f3c1e…": "Boiler room controller" }
}

Entry types written by the platform are firmware, variable, tables, command, group_setting and workflow. Reading the workspace-wide trail requires owner or admin rights; reading a tenant's trail additionally allows that tenant's admins.

Writing audit entries

POST /cgg/logs/:type

:type must be firmware or group_setting; anything else returns 400.

{
"deviceIds": ["665f3c1e…"],
"deviceGroupId": "664a20fa…",
"workspaceId": "664a1f2b…",
"versionId": "664a1f9c…",
"projectId": "664a1f2b…",
"versionCode": "13"
}

For group_setting, send setting and value instead of the version fields. The target workspace is resolved from workspaceId, or from the referenced group or device, and access is verified — writing into a workspace you cannot access returns 403. Returns the created audit log document.

Most platform actions (variable writes, table changes, commands, firmware assignments, workflow enable/disable) are logged automatically; this endpoint is for clients that perform such actions through other means.

Tenants

Base path: /api/workspaces/:workspaceId/tenants — every route requires an authenticated session and workspace access.

Each request loads the tenant, verifies it belongs to :workspaceId, and resolves an effective role: workspace owners and admins act as owner, workspace operators as operator, workspace members as member, otherwise the caller's tenant role (admin or member) applies. A caller with none of these gets 403 {"message":"No access to this tenant"}.

MethodPathRequired rolePurpose
GET/tenantsany workspace accessList tenants
POST/tenantsworkspace owner/operatorCreate a tenant
GET/tenants/:tenantIdany tenant accessGet a tenant
PUT/tenants/:tenantIdowner/operator/tenant adminRename a tenant
DELETE/tenants/:tenantIdworkspace owner/operatorDelete a tenant
GET/tenants/:tenantId/variablesany tenant accessRead tenant variables
PUT/tenants/:tenantId/variablesowner/operator/tenant adminReplace tenant variables
POST/tenants/:tenantId/usersowner/operator/tenant adminAdd a user
DELETE/tenants/:tenantId/users/:userIdowner/operator/tenant adminRemove a user
PUT/tenants/:tenantId/users/:userId/roleowner/operator/tenant adminChange a user's role
POST/tenants/:tenantId/invitesowner/operator/tenant adminInvite by email
GET/tenants/:tenantId/invitesowner/operator/tenant adminList pending invites
DELETE/tenants/:tenantId/invites/:inviteIdowner/operator/tenant adminRevoke an invite

List and read tenants

GET /api/workspaces/:workspaceId/tenants
GET /api/workspaces/:workspaceId/tenants/:tenantId

Workspace owners and operators see every tenant in the workspace; everyone else sees only the tenants they belong to. The list response is a plain array.

For a single tenant, users is resolved to include emails when the caller is a workspace admin or the tenant's admin:

{
"_id": "664a20338c9d0e0011223399",
"name": "Acme North",
"workspace": "664a1f2b8c9d0e0011223344",
"users": [
{ "user": "9b4e…", "email": "ops@acme-north.example", "role": "admin" }
],
"variables": { "SITE_NAME": "North plant" }
}