Summary
Creating a workspace isn't atomic and the "add owner as member" error is discarded. If that second write fails, the workspace exists with no members and the owner is locked out of it.
Where
apps/web... backend: WorkspaceService.Create (apps/api/internal/service/workspace.go):
s.ws.Create(w)
_ = s.ws.AddMember(m) // separate statement, error ignored, no transaction
Since IsMember gates all access, a failed AddMember leaves a workspace the creator can't see or clean up through the UI. ProjectStore.CreateWithCreatorMember already does the equivalent in a single transaction, so there's a pattern to follow.
There's a related shape in handler/auth.go (createDefaultWorkspace, at least logged) and the invite auto-accept path uses a plain Create for AddMember that can also hit a soft-deleted membership row.
Fix
Wrap the workspace insert and the owner membership insert in one transaction (mirror CreateWithCreatorMember).
Summary
Creating a workspace isn't atomic and the "add owner as member" error is discarded. If that second write fails, the workspace exists with no members and the owner is locked out of it.
Where
apps/web... backend:WorkspaceService.Create(apps/api/internal/service/workspace.go):Since
IsMembergates all access, a failedAddMemberleaves a workspace the creator can't see or clean up through the UI.ProjectStore.CreateWithCreatorMemberalready does the equivalent in a single transaction, so there's a pattern to follow.There's a related shape in
handler/auth.go(createDefaultWorkspace, at least logged) and the invite auto-accept path uses a plainCreateforAddMemberthat can also hit a soft-deleted membership row.Fix
Wrap the workspace insert and the owner membership insert in one transaction (mirror
CreateWithCreatorMember).