Cosmo Docs
Server

Workspaces Module

Workspace CRUD and access control

The Workspaces module manages project containers within teams. Source: packages/server/src/modules/workspaces/.

REST Endpoints

All endpoints are prefixed with /api/v1/workspaces and require ClerkAuthGuard.

MethodPathDescription
GET/workspacesList user's accessible workspaces
POST/workspacesCreate a workspace
GET/workspaces/:idGet a single workspace
PATCH/workspaces/:idUpdate name or description
DELETE/workspaces/:idDelete workspace and its contents
POST/workspaces/:id/moveMove workspace to a different team

Query params for GET /workspaces:

ParamDescription
teamIdFilter workspaces by team

Workspace Service (services/workspaces.service.ts)

create(orgId, userId, dto)

Creates a new workspace. If no teamId is provided, the workspace is created in the user's personal team (auto-provisioned on first membership sync).

Input (CreateWorkspaceDto):

FieldRequiredDescription
nameYesDisplay name
teamIdNoTarget team (defaults to personal)
descriptionNoOptional description

A URL-safe slug is generated from the name (with deduplication if taken).

listForUser(orgId, userId, teamId?)

Returns all workspaces the user can access:

  1. Team-accessible workspaces — the teams where the user is a member (optionally filtered by teamId).
  2. Share-accessible workspaces — a WorkspaceShare grant for a workspace outside those teams (skipped when filtering by teamId).

These run as two separate repository queries rather than one combined OR, specifically so each row can be tagged accessType: 'team' | 'share' — a single combined query can't tell the caller which path matched. A workspace reachable both ways counts as 'team'.

getWorkspace(orgId, workspaceId, userId)

Returns a single workspace after verifying the user has access. Primary path: team membership. Fallback: a WorkspaceShare grant for this specific workspace (see the Sharing module). Throws 404 Not Found (not 403, to avoid leaking existence) if neither applies.

The returned object carries accessType: 'team' | 'share' so callers can tell which path granted access — used by the client to decide whether to show mutation controls, and by assertWriteAccess below to decide whether to allow them.

assertWriteAccess(orgId, workspaceId, userId)

Like getWorkspace, but throws 403 Forbidden when access came only from a WorkspaceShare grant. Workspace shares are read-only: a member who reaches a workspace only via a share can view and download its artifacts, but not create, upload, rename, move, or delete them — only real team membership can. Used by:

  • Every artifact-mutation route in the Artifacts module (writeIndex, writeContent, deleteContent, and the artifact-storage deleteWorkspace); read routes (readIndex, readMeta, readContent, contentExists) keep using plain getWorkspace and stay accessible to share-only members.
  • WorkspaceSyncController.putWorkspaceMemory (writing workspace memory files) — getWorkspaceMemory/listWorkspaceMemory are reads and stay on getWorkspace.
  • SharingService.shareArtifact/revokeArtifactShare (via assertArtifactWorkspaceWriteAccess) — granting or revoking an ArtifactShare hands the recipient full access to that artifact, so it's a mutation like any other; a read-only share member must not be able to use it as a side channel. listArtifactShares is a read and keeps the original assertArtifactWorkspaceAccess/getWorkspace gate.

ArtifactService.purgeExpired() (core, triggered as a side effect of list()/listTrashed()/listAllTrashed()) also performs writes through this same access boundary, but is best-effort background housekeeping — it catches and swallows any error from its deleteContent/writeIndex calls rather than letting a share-only caller's plain listing fail because of it.

updateWorkspace(orgId, workspaceId, userId, dto)

Updates name and/or description. Regenerates the slug if name changes. Access check: user must be a member of the workspace's team.

deleteWorkspace(orgId, workspaceId, userId)

Cascades deletion to all workspace resources: tasks, storage objects, artifact folders. Access check: user must be a member.

moveWorkspace(orgId, workspaceId, userId, targetTeamId)

Moves the workspace to a different team within the same org. Verifies the user is a member of both the source and target teams.

Workspace Schema

Workspaces are scoped under a Team. There is no orgId column — the org is accessed via team.orgId.

{
  id: string;
  teamId: string;
  name: string;
  slug: string;
  description?: string;
  archived: boolean;
  createdBy: string;   // userId of the creator
  createdAt: Date;
  updatedAt: Date;
}