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.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/workspaces | List the caller's workspaces |
| GET | /api/workspaces/:workspaceId | Get one workspace with resolved members |
| POST | /api/workspaces | Create a workspace |
| PUT | /api/workspaces/:workspaceId | Rename a workspace |
| DELETE | /api/workspaces/:workspaceId | Delete a workspace and all its content |
| POST | /api/workspaces/:workspaceId/users | Add an existing user directly |
| PUT | /api/workspaces/:workspaceId/users/:userId/role | Change a member's role |
| DELETE | /api/workspaces/:workspaceId/users/:userId | Remove a member |
| POST | /api/workspaces/:workspaceId/invites | Invite a user by email |
| GET | /api/workspaces/:workspaceId/invites | List pending invites |
| DELETE | /api/workspaces/:workspaceId/invites/:inviteId | Revoke an invite |
| GET | /api/workspaces/:workspaceId/variables | Read workspace variables |
| PUT | /api/workspaces/:workspaceId/variables | Replace workspace variables |
| GET | /api/workspaces/:workspaceId/tenant-variable-templates | Read tenant variable templates |
| PUT | /api/workspaces/:workspaceId/tenant-variable-templates | Replace tenant variable templates |
| GET | /api/workspaces/:workspaceId/audit-logs | Read 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.
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.
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 parameter | Description |
|---|---|
tenant | Return only that tenant's entries. Without it, only entries not tied to a tenant are returned |
limit | Page size, default 50, clamped to 1–200 |
skip | Offset, 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"}.
| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | /tenants | any workspace access | List tenants |
| POST | /tenants | workspace owner/operator | Create a tenant |
| GET | /tenants/:tenantId | any tenant access | Get a tenant |
| PUT | /tenants/:tenantId | owner/operator/tenant admin | Rename a tenant |
| DELETE | /tenants/:tenantId | workspace owner/operator | Delete a tenant |
| GET | /tenants/:tenantId/variables | any tenant access | Read tenant variables |
| PUT | /tenants/:tenantId/variables | owner/operator/tenant admin | Replace tenant variables |
| POST | /tenants/:tenantId/users | owner/operator/tenant admin | Add a user |
| DELETE | /tenants/:tenantId/users/:userId | owner/operator/tenant admin | Remove a user |
| PUT | /tenants/:tenantId/users/:userId/role | owner/operator/tenant admin | Change a user's role |
| POST | /tenants/:tenantId/invites | owner/operator/tenant admin | Invite by email |
| GET | /tenants/:tenantId/invites | owner/operator/tenant admin | List pending invites |
| DELETE | /tenants/:tenantId/invites/:inviteId | owner/operator/tenant admin | Revoke 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" }
}