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.
| Method | Path | Description |
|---|---|---|
| GET | /workspaces | List user's accessible workspaces |
| POST | /workspaces | Create a workspace |
| GET | /workspaces/:id | Get a single workspace |
| PATCH | /workspaces/:id | Update name or description |
| DELETE | /workspaces/:id | Delete workspace and its contents |
| POST | /workspaces/:id/move | Move workspace to a different team |
Query params for GET /workspaces:
| Param | Description |
|---|---|
teamId | Filter 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):
| Field | Required | Description |
|---|---|---|
name | Yes | Display name |
teamId | No | Target team (defaults to personal) |
description | No | Optional 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:
- Team-accessible workspaces — the teams where the user is a member (optionally filtered by
teamId). - Share-accessible workspaces — a
WorkspaceSharegrant for a workspace outside those teams (skipped when filtering byteamId).
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-storagedeleteWorkspace); read routes (readIndex,readMeta,readContent,contentExists) keep using plaingetWorkspaceand stay accessible to share-only members. WorkspaceSyncController.putWorkspaceMemory(writing workspace memory files) —getWorkspaceMemory/listWorkspaceMemoryare reads and stay ongetWorkspace.SharingService.shareArtifact/revokeArtifactShare(viaassertArtifactWorkspaceWriteAccess) — granting or revoking anArtifactSharehands 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.listArtifactSharesis a read and keeps the originalassertArtifactWorkspaceAccess/getWorkspacegate.
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 noorgIdcolumn — the org is accessed viateam.orgId.
{
id: string;
teamId: string;
name: string;
slug: string;
description?: string;
archived: boolean;
createdBy: string; // userId of the creator
createdAt: Date;
updatedAt: Date;
}