Skip to content

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.

  • name and at least one appIds entry are required.
  • workspaceSlug is optional — if omitted, it's derived from name (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 returns 409.
  • workspaceType defaults to "Custom".
  • adminUserId is optional and defaults to the caller. If set explicitly, that user must already be a member of the org (400 otherwise). The caller themself must also be an org member (403 if not) and must have a local users row (401 if 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_..." }