Workspaces
A workspace is a sub-unit inside an organization — the day-to-day container that scopes membership, app access, spaces ("locations"), and channels for a specific team or venue. An org typically holds several workspaces (e.g. one per cinema branch); a workspace never spans organizations.
Handles:
- Workspace CRUD — create, list, get, update, delete
- Workspace app access — which apps this workspace's members can use (separate from the org-level grant)
- Workspace membership — add/remove members, change their workspace role
- Space and channel assignment — attach existing locations/channels to a workspace
Architecture
graph LR
AdminPortal["Org / Workspace Admin"]
CoreAPI["CoreAPI (Tenancy — workspaces)"]
CoreDB["CoreAPI DB\n(workspaces, workspace_members,\nworkspace_app_access, workspace_locations,\nworkspace_channels)"]
AdminPortal -->|POST/PATCH/DELETE workspaces| CoreAPI
AdminPortal -->|members, app-access, locations, channels| CoreAPI
CoreAPI --> CoreDB
Workspaces are entirely CoreAPI-local — unlike organization creation, there's no AuthAPI round-trip. Workspace membership is a separate concept from org membership: a user must already be an org member to be added to one of its workspaces, but org membership alone doesn't grant workspace access.
Data Model
erDiagram
Workspace {
string id
string organizationId
string name
string workspaceSlug
string description
string workspaceType
string createdBy
}
WorkspaceMember {
string workspaceId
string userId
string role
}
WorkspaceAppAccess {
string workspaceId
string appId
}
WorkspaceLocation {
string workspaceId
string locationId
}
WorkspaceChannel {
string workspaceId
string channelId
}
Organization ||--o{ Workspace : contains
Workspace ||--o{ WorkspaceMember : has
Workspace ||--o{ WorkspaceAppAccess : "app access"
Workspace ||--o{ WorkspaceLocation : "spaces"
Workspace ||--o{ WorkspaceChannel : channels
Workspace roles are WORKSPACE_ADMIN > WORKSPACE_MEMBER > VIEWER (in that priority order). Deleting a workspace cascades across all five tables above plus workspace_settings, in a single transaction.
Endpoints
All routes below are mounted under /api/v1/tenancy/organizations/:orgId/workspaces and require a valid session (ProtectedRS256). Endpoints that mutate state additionally require the WORKSPACE_ADMIN role in the target workspace, via RequireWorkspaceRole — with one exception: an ORG_OWNER or ORG_ADMIN of the workspace's parent org always passes this check, regardless of their own workspace membership.
GET /
Lists every workspace in the org, newest first, each with its app IDs, member count, space count, and channel count.
Response 200
[
{
"id": "ws_...", "name": "KLCC Branch", "workspaceSlug": "klcc-branch",
"description": "", "workspaceType": "Custom",
"appIds": ["ticketing"], "memberCount": 4, "spaceCount": 2, "channelCount": 1
}
]
POST / — requires org permission workspace:create
Creates a workspace and, in one transaction, grants its app access and adds an initial WORKSPACE_ADMIN member.
nameand at least oneappIdsentry are required.workspaceSlugis optional — if omitted, it's derived fromname(lowercased, non-alphanumerics collapsed to single dashes). An explicit slug is still normalized the same way. Slugs and names (case-insensitive) must each be unique within the org — a collision returns409.workspaceTypedefaults to"Custom".adminUserIdis optional and defaults to the caller. If set explicitly, that user must already be a member of the org (400otherwise). The caller themself must also be an org member (403if not) and must have a localusersrow (401if not).
Request
{
"name": "KLCC Branch",
"description": "Flagship branch",
"workspaceType": "Cinema",
"appIds": ["ticketing", "signage"],
"adminUserId": "usr_..."
}
Response 201
{ "id": "ws_...", "name": "KLCC Branch", "workspaceSlug": "klcc-branch", "appIds": ["ticketing", "signage"], "adminUserId": "usr_..." }
GET /:workspaceId
Fetches a single workspace by UUID, scoped to orgId. 404 if it doesn't belong to that org.
PATCH /:workspaceId — requires WORKSPACE_ADMIN
Updates name, description, and/or workspaceType; any field left empty is left unchanged. At least one field must be non-empty. A new name is checked for uniqueness within the org (excluding this workspace) and returns 409 on collision.
Request
{ "description": "Flagship branch — renovated 2026" }
PUT /:workspaceId/app-access — requires WORKSPACE_ADMIN
Replaces the workspace's full app access list. At least one app is required; unknown app IDs return 400.
Request
{ "appIds": ["ticketing", "signage", "menu-board"] }
DELETE /:workspaceId — requires WORKSPACE_ADMIN
Deletes the workspace and all related rows (members, app access, channels, spaces, settings). Returns 204 with no body.
GET /:workspaceId/members
Paginated member list (page, limit query params, default 1/20, max limit 100), ordered by join time.
Response 200
{
"items": [ { "userId": "usr_...", "name": "Ada Lovelace", "email": "ada@acme.example.com", "role": "WORKSPACE_ADMIN" } ],
"total": 1, "page": 1, "limit": 20, "hasNext": false
}
POST /:workspaceId/members — requires WORKSPACE_ADMIN
Adds a user to the workspace. role defaults to WORKSPACE_MEMBER if omitted. A no-op if the user is already a member.
Request
{ "userId": "usr_...", "role": "VIEWER" }
PATCH /:workspaceId/members/:userId — requires WORKSPACE_ADMIN
Changes a member's workspace role. Callers cannot change their own role (403).
Request
{ "newRole": "WORKSPACE_ADMIN" }
DELETE /:workspaceId/members/:userId — requires WORKSPACE_ADMIN
Removes a member from the workspace. Callers cannot remove themselves (403). 404 if the target wasn't a member.
POST /:workspaceId/locations — requires WORKSPACE_ADMIN
Assigns an existing location ("space") to the workspace. A no-op if already assigned.
Request
{ "locationId": "loc_..." }
POST /:workspaceId/channels — requires WORKSPACE_ADMIN
Assigns an existing channel to the workspace. A no-op if already assigned.
Request
{ "channelId": "chn_..." }