Skip to main content

Developer reference: Create workspace (additional organization)

This document covers creating a new Organization for an authenticated user via POST /api/workspaces, the /create-workspace route, and the CreateWorkspaceModal UI. It does not cover onboarding workspace setup (PATCH /api/auth/onboarding/workspace) or switching workspace (POST /api/auth/switch-workspace).

Repository layout

  • Web client: gaicc-app/Clients
  • API server: gaicc-app/Servers

When the app requires a workspace

ConcernLocation
Redirect to create flowgaicc-app/Clients/src/presentation/components/RequireAuth.tsx — if workspaces.length === 0 and path is not /create-workspace, Navigate to /create-workspace; if user has workspaces and path is /create-workspace, redirect to /dashboard
Onboarding gateSame file: users with onboardingStep < 3 are sent to onboarding routes first

UI

ItemLocation
Full-page forced creategaicc-app/Clients/src/presentation/pages/WorkspaceRequired.tsx — renders CreateWorkspaceModal with allowDismiss={false}, onClosenavigate("/dashboard", { replace: true }) (runs after successful create via modal onClose)
Modalgaicc-app/Clients/src/presentation/components/CreateWorkspaceModal.tsx
Sidebar entrygaicc-app/Clients/src/presentation/components/Sidebar.tsx — optional modal with allowDismiss default true

CreateWorkspaceModal behavior

  • useCreateWorkspace mutation from auth.queries.ts; useSubscriptionGate (usePlanUsage) — if !gate.canCreate, opens UpgradePrompt (reason: no_subscription | trial_expired, resource="workspaces").
  • Submit validates non-empty name and displaySlug; slugify on name until user edits handle.
  • Multipart payload: name, slug, optional file (logo).
  • allowDismiss: when true, Escape (unless pending), overlay close, Cancel, and header X can dismiss; when false, onInteractOutside / onEscapeKeyDown call preventDefault, close button hidden, no Cancel.

Client HTTP

ConcernLocation
API callgaicc-app/Clients/src/infrastructure/repositories/auth.repository.tscreateWorkspace(dto)POST /api/workspaces with FormData (name, slug, optional file)
DTOgaicc-app/Clients/src/domain/types/auth.types.tsCreateWorkspaceDto
Mutationgaicc-app/Clients/src/application/queries/auth.queries.tsuseCreateWorkspace: onSuccesssetAuth with new token, user, workspaces; queryClient.clear()

API

MethodPathAuth
POST/api/workspacesauthenticateJWT

Middleware ordergaicc-app/Servers/src/routes/workspace.routes.ts

  1. authenticateJWT
  2. assertWorkspaceBootstrapOrTenant — if JWT has organizationId, continue; if not, allow only when OrganizationMember count for user is zero (bootstrap after losing all memberships); else 401 Organization context required
  3. enforceMutationBillingComplianceWorkspacePost — if no organizationId on request, skip tenant billing check (first workspace); else delegate to normal mutation billing compliance
  4. enforcePlanLimitWorkspacePost("workspaces") — same "no org on JWT → skip" pattern for plan limits
  5. uploadImage — optional file field
  6. createWorkspace controller

Controllergaicc-app/Servers/src/controllers/workspace.controller.ts

  • Zod: name and slug trimmed, min length 1.
  • Validation failure: 400 { message: "Validation failed", errors }.
  • Optional logo: S3 key under logos/{userId}/... (note: segment uses userId, not org id, in generateS3Key).
  • Success: 201 + buildWorkspaceAuthResponse result (same shape family as auth login/register).

Servicegaicc-app/Servers/src/services/workspace.service.tscreateWorkspace

  • slugify on slug; 409 if any organization already has that slug globally.
  • Transaction: create Organization, OrganizationMember (Admin), set user activeOrganizationId, copyManualEnterprisePlanToNewOrg from previous active org when applicable.
  • Returns buildWorkspaceAuthResponse (JWT + user + workspaces list).

Mountgaicc-app/Servers/src/index.ts: app.use("/api/workspaces", workspaceRoutes).


Testing notes (TestSprite / external)

  • Bootstrap JWT: user with zero memberships can POST without organizationId on the token path allowed by assertWorkspaceBootstrapOrTenant.
  • Slug conflict: second POST with same slug → 409.
  • Subscription gate is client-side before submit; server still enforces plan/billing middleware when JWT carries an org.
  • Configure cases in the TestSprite web portal; this file is the implementation reference.