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
| Concern | Location |
|---|---|
| Redirect to create flow | gaicc-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 gate | Same file: users with onboardingStep < 3 are sent to onboarding routes first |
UI
| Item | Location |
|---|---|
| Full-page forced create | gaicc-app/Clients/src/presentation/pages/WorkspaceRequired.tsx — renders CreateWorkspaceModal with allowDismiss={false}, onClose → navigate("/dashboard", { replace: true }) (runs after successful create via modal onClose) |
| Modal | gaicc-app/Clients/src/presentation/components/CreateWorkspaceModal.tsx |
| Sidebar entry | gaicc-app/Clients/src/presentation/components/Sidebar.tsx — optional modal with allowDismiss default true |
CreateWorkspaceModal behavior
useCreateWorkspacemutation fromauth.queries.ts;useSubscriptionGate(usePlanUsage) — if!gate.canCreate, opensUpgradePrompt(reason:no_subscription|trial_expired,resource="workspaces").- Submit validates non-empty name and displaySlug;
slugifyon name until user edits handle. - Multipart payload:
name,slug, optionalfile(logo). allowDismiss: when true, Escape (unless pending), overlay close, Cancel, and header X can dismiss; when false,onInteractOutside/onEscapeKeyDowncallpreventDefault, close button hidden, no Cancel.
Client HTTP
| Concern | Location |
|---|---|
| API call | gaicc-app/Clients/src/infrastructure/repositories/auth.repository.ts — createWorkspace(dto) → POST /api/workspaces with FormData (name, slug, optional file) |
| DTO | gaicc-app/Clients/src/domain/types/auth.types.ts — CreateWorkspaceDto |
| Mutation | gaicc-app/Clients/src/application/queries/auth.queries.ts — useCreateWorkspace: onSuccess → setAuth with new token, user, workspaces; queryClient.clear() |
API
| Method | Path | Auth |
|---|---|---|
POST | /api/workspaces | authenticateJWT |
Middleware order — gaicc-app/Servers/src/routes/workspace.routes.ts
authenticateJWTassertWorkspaceBootstrapOrTenant— if JWT hasorganizationId, continue; if not, allow only whenOrganizationMembercount for user is zero (bootstrap after losing all memberships); else 401Organization context requiredenforceMutationBillingComplianceWorkspacePost— if noorganizationIdon request, skip tenant billing check (first workspace); else delegate to normal mutation billing complianceenforcePlanLimitWorkspacePost("workspaces")— same "no org on JWT → skip" pattern for plan limitsuploadImage— optionalfilefieldcreateWorkspacecontroller
Controller — gaicc-app/Servers/src/controllers/workspace.controller.ts
- Zod:
nameandslugtrimmed, min length 1. - Validation failure: 400
{ message: "Validation failed", errors }. - Optional logo: S3 key under
logos/{userId}/...(note: segment usesuserId, not org id, ingenerateS3Key). - Success: 201 +
buildWorkspaceAuthResponseresult (same shape family as auth login/register).
Service — gaicc-app/Servers/src/services/workspace.service.ts — createWorkspace
slugifyon slug; 409 if any organization already has that slug globally.- Transaction: create
Organization,OrganizationMember(Admin), set useractiveOrganizationId,copyManualEnterprisePlanToNewOrgfrom previous active org when applicable. - Returns
buildWorkspaceAuthResponse(JWT + user + workspaces list).
Mount — gaicc-app/Servers/src/index.ts: app.use("/api/workspaces", workspaceRoutes).
Testing notes (TestSprite / external)
- Bootstrap JWT: user with zero memberships can POST without
organizationIdon the token path allowed byassertWorkspaceBootstrapOrTenant. - 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.