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

Every org gets a Default workspace auto-created the moment it has an admin user — see Default Workspace Creation below. Workspace access can also be granted to a whole group of users at once via Teams, which layers on top of (and is mutually exclusive with) the direct membership described here.


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.


Default Workspace Creation

Every org gets one workspace created automatically, named literally "Default" (slug default, type Custom), with whatever apps the org currently has access to and the org's new admin user as its WORKSPACE_ADMIN. This isn't an explicit API call — it's a side effect fired from two places:

  1. POST /tenancy/organizations (direct, non-self-signup creation) — right after the admin user is created, for cloud orgs only. on_prem orgs never get one (they have no local admin user or app access to seed it with).
  2. PUT /tenancy/organizations/:orgId/status approving a pending self-signup org — fires only on that pending→active transition, using the org's app access as granted at approval time (which may differ from what was requested at self-signup, if it changed in between).

There is no flag or endpoint to skip it. Creation is best-effort: it goes through the same validation as a manual POST .../workspaces call, so it can fail (e.g. if somehow no app access exists yet) — a failure is logged server-side but does not fail the surrounding org-creation/approval request.


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 org:workspaces: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" }

409 "user already has access to this workspace through a team" if the user already reaches this workspace via a team grant — direct and team-based access are mutually exclusive.


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_..." }


GET /:workspaceId/teams

Lists the teams that have been granted a role on this workspace, alongside the direct member list above — see Teams: Interaction with Direct Workspace Membership for how the two combine.