From 023949f1e0b8535bb060c8b590bfacb0b47fca34 Mon Sep 17 00:00:00 2001
From: Jason Woltje
Date: Sat, 28 Feb 2026 09:04:15 -0600
Subject: [PATCH 1/2] fix(api,web): separate workspace context from auth
session (#534)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
BetterAuth session responses contain only identity fields — workspace
context (workspaceId, currentWorkspaceId) was never returned, causing
"Workspace ID is required" on every guarded endpoint after login.
Add GET /api/workspaces endpoint (AuthGuard only, no WorkspaceGuard)
that returns user workspace memberships with auto-provisioning for
new users. Frontend auth-context now fetches workspaces after session
check and persists the default to localStorage. Race condition in
auto-provisioning is guarded by re-querying inside the transaction.
Co-Authored-By: Claude Opus 4.6
---
apps/api/src/app.module.ts | 2 +
apps/api/src/auth/auth.controller.spec.ts | 19 +-
apps/api/src/auth/auth.controller.ts | 13 +-
apps/api/src/workspaces/dto/index.ts | 1 +
.../workspaces/dto/workspace-response.dto.ts | 12 +
apps/api/src/workspaces/index.ts | 3 +
.../workspaces/workspaces.controller.spec.ts | 75 ++++++
.../src/workspaces/workspaces.controller.ts | 28 +++
apps/api/src/workspaces/workspaces.module.ts | 13 +
.../src/workspaces/workspaces.service.spec.ts | 229 ++++++++++++++++++
apps/api/src/workspaces/workspaces.service.ts | 97 ++++++++
.../src/app/(authenticated)/profile/page.tsx | 19 +-
apps/web/src/components/chat/Chat.tsx | 5 +-
apps/web/src/components/layout/AppSidebar.tsx | 2 +-
apps/web/src/lib/api/index.ts | 1 +
apps/web/src/lib/api/workspaces.ts | 26 ++
apps/web/src/lib/auth/auth-context.test.tsx | 92 +++++--
apps/web/src/lib/auth/auth-context.tsx | 17 +-
packages/shared/src/types/auth.types.ts | 7 +-
19 files changed, 596 insertions(+), 65 deletions(-)
create mode 100644 apps/api/src/workspaces/dto/index.ts
create mode 100644 apps/api/src/workspaces/dto/workspace-response.dto.ts
create mode 100644 apps/api/src/workspaces/index.ts
create mode 100644 apps/api/src/workspaces/workspaces.controller.spec.ts
create mode 100644 apps/api/src/workspaces/workspaces.controller.ts
create mode 100644 apps/api/src/workspaces/workspaces.module.ts
create mode 100644 apps/api/src/workspaces/workspaces.service.spec.ts
create mode 100644 apps/api/src/workspaces/workspaces.service.ts
create mode 100644 apps/web/src/lib/api/workspaces.ts
diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts
index 79810b8..994d191 100644
--- a/apps/api/src/app.module.ts
+++ b/apps/api/src/app.module.ts
@@ -42,6 +42,7 @@ import { SpeechModule } from "./speech/speech.module";
import { DashboardModule } from "./dashboard/dashboard.module";
import { TerminalModule } from "./terminal/terminal.module";
import { PersonalitiesModule } from "./personalities/personalities.module";
+import { WorkspacesModule } from "./workspaces/workspaces.module";
import { RlsContextInterceptor } from "./common/interceptors/rls-context.interceptor";
@Module({
@@ -107,6 +108,7 @@ import { RlsContextInterceptor } from "./common/interceptors/rls-context.interce
DashboardModule,
TerminalModule,
PersonalitiesModule,
+ WorkspacesModule,
],
controllers: [AppController, CsrfController],
providers: [
diff --git a/apps/api/src/auth/auth.controller.spec.ts b/apps/api/src/auth/auth.controller.spec.ts
index 8f54a32..3740338 100644
--- a/apps/api/src/auth/auth.controller.spec.ts
+++ b/apps/api/src/auth/auth.controller.spec.ts
@@ -361,16 +361,13 @@ describe("AuthController", () => {
});
describe("getProfile", () => {
- it("should return complete user profile with workspace fields", () => {
+ it("should return complete user profile with identity fields", () => {
const mockUser: AuthUser = {
id: "user-123",
email: "test@example.com",
name: "Test User",
image: "https://example.com/avatar.jpg",
emailVerified: true,
- workspaceId: "workspace-123",
- currentWorkspaceId: "workspace-456",
- workspaceRole: "admin",
};
const result = controller.getProfile(mockUser);
@@ -381,13 +378,10 @@ describe("AuthController", () => {
name: mockUser.name,
image: mockUser.image,
emailVerified: mockUser.emailVerified,
- workspaceId: mockUser.workspaceId,
- currentWorkspaceId: mockUser.currentWorkspaceId,
- workspaceRole: mockUser.workspaceRole,
});
});
- it("should return user profile with optional fields undefined", () => {
+ it("should return user profile with only required fields", () => {
const mockUser: AuthUser = {
id: "user-123",
email: "test@example.com",
@@ -400,12 +394,11 @@ describe("AuthController", () => {
id: mockUser.id,
email: mockUser.email,
name: mockUser.name,
- image: undefined,
- emailVerified: undefined,
- workspaceId: undefined,
- currentWorkspaceId: undefined,
- workspaceRole: undefined,
});
+ // Workspace fields are not included — served by GET /api/workspaces
+ expect(result).not.toHaveProperty("workspaceId");
+ expect(result).not.toHaveProperty("currentWorkspaceId");
+ expect(result).not.toHaveProperty("workspaceRole");
});
});
diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts
index b8802f7..f0bd96b 100644
--- a/apps/api/src/auth/auth.controller.ts
+++ b/apps/api/src/auth/auth.controller.ts
@@ -72,15 +72,10 @@ export class AuthController {
if (user.emailVerified !== undefined) {
profile.emailVerified = user.emailVerified;
}
- if (user.workspaceId !== undefined) {
- profile.workspaceId = user.workspaceId;
- }
- if (user.currentWorkspaceId !== undefined) {
- profile.currentWorkspaceId = user.currentWorkspaceId;
- }
- if (user.workspaceRole !== undefined) {
- profile.workspaceRole = user.workspaceRole;
- }
+
+ // Workspace context is served by GET /api/workspaces, not the auth profile.
+ // The deprecated workspaceId/currentWorkspaceId/workspaceRole fields on
+ // AuthUser are never populated by BetterAuth and are omitted here.
return profile;
}
diff --git a/apps/api/src/workspaces/dto/index.ts b/apps/api/src/workspaces/dto/index.ts
new file mode 100644
index 0000000..32bb49a
--- /dev/null
+++ b/apps/api/src/workspaces/dto/index.ts
@@ -0,0 +1 @@
+export { WorkspaceResponseDto } from "./workspace-response.dto";
diff --git a/apps/api/src/workspaces/dto/workspace-response.dto.ts b/apps/api/src/workspaces/dto/workspace-response.dto.ts
new file mode 100644
index 0000000..b89d294
--- /dev/null
+++ b/apps/api/src/workspaces/dto/workspace-response.dto.ts
@@ -0,0 +1,12 @@
+import type { WorkspaceMemberRole } from "@prisma/client";
+
+/**
+ * Response DTO for a workspace the authenticated user belongs to.
+ */
+export class WorkspaceResponseDto {
+ id!: string;
+ name!: string;
+ ownerId!: string;
+ role!: WorkspaceMemberRole;
+ createdAt!: Date;
+}
diff --git a/apps/api/src/workspaces/index.ts b/apps/api/src/workspaces/index.ts
new file mode 100644
index 0000000..402915b
--- /dev/null
+++ b/apps/api/src/workspaces/index.ts
@@ -0,0 +1,3 @@
+export { WorkspacesModule } from "./workspaces.module";
+export { WorkspacesService } from "./workspaces.service";
+export { WorkspacesController } from "./workspaces.controller";
diff --git a/apps/api/src/workspaces/workspaces.controller.spec.ts b/apps/api/src/workspaces/workspaces.controller.spec.ts
new file mode 100644
index 0000000..31a04ee
--- /dev/null
+++ b/apps/api/src/workspaces/workspaces.controller.spec.ts
@@ -0,0 +1,75 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { Test, TestingModule } from "@nestjs/testing";
+import { WorkspacesController } from "./workspaces.controller";
+import { WorkspacesService } from "./workspaces.service";
+import { AuthGuard } from "../auth/guards/auth.guard";
+import { WorkspaceMemberRole } from "@prisma/client";
+import type { AuthUser } from "@mosaic/shared";
+
+describe("WorkspacesController", () => {
+ let controller: WorkspacesController;
+ let service: WorkspacesService;
+
+ const mockWorkspacesService = {
+ getUserWorkspaces: vi.fn(),
+ };
+
+ const mockUser: AuthUser = {
+ id: "user-1",
+ email: "test@example.com",
+ name: "Test User",
+ };
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [WorkspacesController],
+ providers: [
+ {
+ provide: WorkspacesService,
+ useValue: mockWorkspacesService,
+ },
+ ],
+ })
+ .overrideGuard(AuthGuard)
+ .useValue({ canActivate: () => true })
+ .compile();
+
+ controller = module.get(WorkspacesController);
+ service = module.get(WorkspacesService);
+
+ vi.clearAllMocks();
+ });
+
+ describe("GET /api/workspaces", () => {
+ it("should call service with authenticated user id", async () => {
+ mockWorkspacesService.getUserWorkspaces.mockResolvedValueOnce([]);
+
+ await controller.getUserWorkspaces(mockUser);
+
+ expect(service.getUserWorkspaces).toHaveBeenCalledWith("user-1");
+ });
+
+ it("should return workspace list from service", async () => {
+ const mockWorkspaces = [
+ {
+ id: "ws-1",
+ name: "My Workspace",
+ ownerId: "user-1",
+ role: WorkspaceMemberRole.OWNER,
+ createdAt: new Date("2026-01-01"),
+ },
+ ];
+ mockWorkspacesService.getUserWorkspaces.mockResolvedValueOnce(mockWorkspaces);
+
+ const result = await controller.getUserWorkspaces(mockUser);
+
+ expect(result).toEqual(mockWorkspaces);
+ });
+
+ it("should propagate service errors", async () => {
+ mockWorkspacesService.getUserWorkspaces.mockRejectedValueOnce(new Error("Database error"));
+
+ await expect(controller.getUserWorkspaces(mockUser)).rejects.toThrow("Database error");
+ });
+ });
+});
diff --git a/apps/api/src/workspaces/workspaces.controller.ts b/apps/api/src/workspaces/workspaces.controller.ts
new file mode 100644
index 0000000..d23f825
--- /dev/null
+++ b/apps/api/src/workspaces/workspaces.controller.ts
@@ -0,0 +1,28 @@
+import { Controller, Get, UseGuards } from "@nestjs/common";
+import { WorkspacesService } from "./workspaces.service";
+import { AuthGuard } from "../auth/guards/auth.guard";
+import { CurrentUser } from "../auth/decorators/current-user.decorator";
+import type { AuthUser } from "@mosaic/shared";
+import type { WorkspaceResponseDto } from "./dto";
+
+/**
+ * User-scoped workspace operations.
+ *
+ * Intentionally does NOT use WorkspaceGuard — these routes operate across all
+ * workspaces the user belongs to, not within a single workspace context.
+ */
+@Controller("workspaces")
+@UseGuards(AuthGuard)
+export class WorkspacesController {
+ constructor(private readonly workspacesService: WorkspacesService) {}
+
+ /**
+ * GET /api/workspaces
+ * Returns workspaces the authenticated user is a member of.
+ * Auto-provisions a default workspace if the user has none.
+ */
+ @Get()
+ async getUserWorkspaces(@CurrentUser() user: AuthUser): Promise {
+ return this.workspacesService.getUserWorkspaces(user.id);
+ }
+}
diff --git a/apps/api/src/workspaces/workspaces.module.ts b/apps/api/src/workspaces/workspaces.module.ts
new file mode 100644
index 0000000..e9157fa
--- /dev/null
+++ b/apps/api/src/workspaces/workspaces.module.ts
@@ -0,0 +1,13 @@
+import { Module } from "@nestjs/common";
+import { WorkspacesController } from "./workspaces.controller";
+import { WorkspacesService } from "./workspaces.service";
+import { PrismaModule } from "../prisma/prisma.module";
+import { AuthModule } from "../auth/auth.module";
+
+@Module({
+ imports: [PrismaModule, AuthModule],
+ controllers: [WorkspacesController],
+ providers: [WorkspacesService],
+ exports: [WorkspacesService],
+})
+export class WorkspacesModule {}
diff --git a/apps/api/src/workspaces/workspaces.service.spec.ts b/apps/api/src/workspaces/workspaces.service.spec.ts
new file mode 100644
index 0000000..2c87b0c
--- /dev/null
+++ b/apps/api/src/workspaces/workspaces.service.spec.ts
@@ -0,0 +1,229 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { Test, TestingModule } from "@nestjs/testing";
+import { WorkspacesService } from "./workspaces.service";
+import { PrismaService } from "../prisma/prisma.service";
+import { WorkspaceMemberRole } from "@prisma/client";
+
+describe("WorkspacesService", () => {
+ let service: WorkspacesService;
+
+ const mockUserId = "550e8400-e29b-41d4-a716-446655440001";
+ const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440002";
+
+ const mockWorkspace = {
+ id: mockWorkspaceId,
+ name: "Test Workspace",
+ ownerId: mockUserId,
+ settings: {},
+ matrixRoomId: null,
+ createdAt: new Date("2026-01-01"),
+ updatedAt: new Date("2026-01-01"),
+ };
+
+ const mockMembership = {
+ workspaceId: mockWorkspaceId,
+ userId: mockUserId,
+ role: WorkspaceMemberRole.OWNER,
+ joinedAt: new Date("2026-01-01"),
+ workspace: {
+ id: mockWorkspaceId,
+ name: "Test Workspace",
+ ownerId: mockUserId,
+ createdAt: new Date("2026-01-01"),
+ },
+ };
+
+ const mockPrismaService = {
+ workspaceMember: {
+ findMany: vi.fn(),
+ create: vi.fn(),
+ },
+ workspace: {
+ create: vi.fn(),
+ },
+ $transaction: vi.fn(),
+ };
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ WorkspacesService,
+ {
+ provide: PrismaService,
+ useValue: mockPrismaService,
+ },
+ ],
+ }).compile();
+
+ service = module.get(WorkspacesService);
+
+ vi.clearAllMocks();
+ });
+
+ describe("getUserWorkspaces", () => {
+ it("should return all workspaces user is a member of", async () => {
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([mockMembership]);
+
+ const result = await service.getUserWorkspaces(mockUserId);
+
+ expect(result).toEqual([
+ {
+ id: mockWorkspaceId,
+ name: "Test Workspace",
+ ownerId: mockUserId,
+ role: WorkspaceMemberRole.OWNER,
+ createdAt: mockMembership.workspace.createdAt,
+ },
+ ]);
+ expect(mockPrismaService.workspaceMember.findMany).toHaveBeenCalledWith({
+ where: { userId: mockUserId },
+ include: {
+ workspace: {
+ select: { id: true, name: true, ownerId: true, createdAt: true },
+ },
+ },
+ orderBy: { joinedAt: "asc" },
+ });
+ });
+
+ it("should return multiple workspaces ordered by joinedAt", async () => {
+ const secondWorkspace = {
+ ...mockMembership,
+ workspaceId: "ws-2",
+ role: WorkspaceMemberRole.MEMBER,
+ joinedAt: new Date("2026-02-01"),
+ workspace: {
+ id: "ws-2",
+ name: "Second Workspace",
+ ownerId: "other-user",
+ createdAt: new Date("2026-02-01"),
+ },
+ };
+
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([
+ mockMembership,
+ secondWorkspace,
+ ]);
+
+ const result = await service.getUserWorkspaces(mockUserId);
+
+ expect(result).toHaveLength(2);
+ expect(result[0].id).toBe(mockWorkspaceId);
+ expect(result[1].id).toBe("ws-2");
+ expect(result[1].role).toBe(WorkspaceMemberRole.MEMBER);
+ });
+
+ it("should auto-provision a default workspace when user has no memberships", async () => {
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);
+ mockPrismaService.$transaction.mockImplementationOnce(
+ async (fn: (tx: typeof mockPrismaService) => Promise) => {
+ const txMock = {
+ workspaceMember: {
+ findFirst: vi.fn().mockResolvedValueOnce(null),
+ create: vi.fn().mockResolvedValueOnce({}),
+ },
+ workspace: {
+ create: vi.fn().mockResolvedValueOnce(mockWorkspace),
+ },
+ };
+ return fn(txMock as unknown as typeof mockPrismaService);
+ }
+ );
+
+ const result = await service.getUserWorkspaces(mockUserId);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe("Test Workspace");
+ expect(result[0].role).toBe(WorkspaceMemberRole.OWNER);
+ expect(mockPrismaService.$transaction).toHaveBeenCalledTimes(1);
+ });
+
+ it("should return existing workspace if one was created between initial check and transaction", async () => {
+ // Simulates a race condition: initial findMany returns [], but inside the
+ // transaction another request already created a workspace.
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);
+ mockPrismaService.$transaction.mockImplementationOnce(
+ async (fn: (tx: typeof mockPrismaService) => Promise) => {
+ const txMock = {
+ workspaceMember: {
+ findFirst: vi.fn().mockResolvedValueOnce(mockMembership),
+ },
+ workspace: {
+ create: vi.fn(),
+ },
+ };
+ return fn(txMock as unknown as typeof mockPrismaService);
+ }
+ );
+
+ const result = await service.getUserWorkspaces(mockUserId);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe(mockWorkspaceId);
+ expect(result[0].name).toBe("Test Workspace");
+ });
+
+ it("should create workspace with correct data during auto-provisioning", async () => {
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);
+
+ let capturedWorkspaceData: unknown;
+ let capturedMemberData: unknown;
+
+ mockPrismaService.$transaction.mockImplementationOnce(
+ async (fn: (tx: typeof mockPrismaService) => Promise) => {
+ const txMock = {
+ workspaceMember: {
+ findFirst: vi.fn().mockResolvedValueOnce(null),
+ create: vi.fn().mockImplementation((args: unknown) => {
+ capturedMemberData = args;
+ return {};
+ }),
+ },
+ workspace: {
+ create: vi.fn().mockImplementation((args: unknown) => {
+ capturedWorkspaceData = args;
+ return mockWorkspace;
+ }),
+ },
+ };
+ return fn(txMock as unknown as typeof mockPrismaService);
+ }
+ );
+
+ await service.getUserWorkspaces(mockUserId);
+
+ expect(capturedWorkspaceData).toEqual({
+ data: {
+ name: "My Workspace",
+ ownerId: mockUserId,
+ settings: {},
+ },
+ });
+ expect(capturedMemberData).toEqual({
+ data: {
+ workspaceId: mockWorkspaceId,
+ userId: mockUserId,
+ role: WorkspaceMemberRole.OWNER,
+ },
+ });
+ });
+
+ it("should not auto-provision when user already has workspaces", async () => {
+ mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([mockMembership]);
+
+ await service.getUserWorkspaces(mockUserId);
+
+ expect(mockPrismaService.$transaction).not.toHaveBeenCalled();
+ });
+
+ it("should propagate database errors", async () => {
+ mockPrismaService.workspaceMember.findMany.mockRejectedValueOnce(
+ new Error("Database connection failed")
+ );
+
+ await expect(service.getUserWorkspaces(mockUserId)).rejects.toThrow(
+ "Database connection failed"
+ );
+ });
+ });
+});
diff --git a/apps/api/src/workspaces/workspaces.service.ts b/apps/api/src/workspaces/workspaces.service.ts
new file mode 100644
index 0000000..247932e
--- /dev/null
+++ b/apps/api/src/workspaces/workspaces.service.ts
@@ -0,0 +1,97 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { WorkspaceMemberRole } from "@prisma/client";
+import { PrismaService } from "../prisma/prisma.service";
+import type { WorkspaceResponseDto } from "./dto";
+
+@Injectable()
+export class WorkspacesService {
+ private readonly logger = new Logger(WorkspacesService.name);
+
+ constructor(private readonly prisma: PrismaService) {}
+
+ /**
+ * Get all workspaces the user is a member of.
+ *
+ * Auto-provisioning: if the user has no workspace memberships (e.g. fresh
+ * signup via BetterAuth), a default workspace is created atomically and
+ * returned. This is the only call site for workspace bootstrapping.
+ */
+ async getUserWorkspaces(userId: string): Promise {
+ const memberships = await this.prisma.workspaceMember.findMany({
+ where: { userId },
+ include: {
+ workspace: {
+ select: { id: true, name: true, ownerId: true, createdAt: true },
+ },
+ },
+ orderBy: { joinedAt: "asc" },
+ });
+
+ if (memberships.length > 0) {
+ return memberships.map((m) => ({
+ id: m.workspace.id,
+ name: m.workspace.name,
+ ownerId: m.workspace.ownerId,
+ role: m.role,
+ createdAt: m.workspace.createdAt,
+ }));
+ }
+
+ // Auto-provision a default workspace for new users.
+ // Re-query inside the transaction to guard against concurrent requests
+ // both seeing zero memberships and creating duplicate workspaces.
+ this.logger.log(`Auto-provisioning default workspace for user ${userId}`);
+
+ const workspace = await this.prisma.$transaction(async (tx) => {
+ const existing = await tx.workspaceMember.findFirst({
+ where: { userId },
+ include: {
+ workspace: {
+ select: { id: true, name: true, ownerId: true, createdAt: true },
+ },
+ },
+ });
+ if (existing) {
+ return { ...existing.workspace, alreadyExisted: true as const };
+ }
+
+ const created = await tx.workspace.create({
+ data: {
+ name: "My Workspace",
+ ownerId: userId,
+ settings: {},
+ },
+ });
+ await tx.workspaceMember.create({
+ data: {
+ workspaceId: created.id,
+ userId,
+ role: WorkspaceMemberRole.OWNER,
+ },
+ });
+ return { ...created, alreadyExisted: false as const };
+ });
+
+ if (workspace.alreadyExisted) {
+ return [
+ {
+ id: workspace.id,
+ name: workspace.name,
+ ownerId: workspace.ownerId,
+ role: WorkspaceMemberRole.OWNER,
+ createdAt: workspace.createdAt,
+ },
+ ];
+ }
+
+ return [
+ {
+ id: workspace.id,
+ name: workspace.name,
+ ownerId: workspace.ownerId,
+ role: WorkspaceMemberRole.OWNER,
+ createdAt: workspace.createdAt,
+ },
+ ];
+ }
+}
diff --git a/apps/web/src/app/(authenticated)/profile/page.tsx b/apps/web/src/app/(authenticated)/profile/page.tsx
index f60d696..9ae73fc 100644
--- a/apps/web/src/app/(authenticated)/profile/page.tsx
+++ b/apps/web/src/app/(authenticated)/profile/page.tsx
@@ -265,23 +265,8 @@ export default function ProfilePage(): ReactElement {
)}
- {user?.workspaceRole && (
-
- {user.workspaceRole}
-
- )}
+ {/* Workspace role badge — placeholder until workspace context API
+ provides role data via GET /api/workspaces */}
diff --git a/apps/web/src/components/chat/Chat.tsx b/apps/web/src/components/chat/Chat.tsx
index 7934614..a06a538 100644
--- a/apps/web/src/components/chat/Chat.tsx
+++ b/apps/web/src/components/chat/Chat.tsx
@@ -5,6 +5,7 @@ import { useAuth } from "@/lib/auth/auth-context";
import { useChat } from "@/hooks/useChat";
import { useOrchestratorCommands } from "@/hooks/useOrchestratorCommands";
import { useWebSocket } from "@/hooks/useWebSocket";
+import { useWorkspaceId } from "@/lib/hooks";
import { MessageList } from "./MessageList";
import { ChatInput, type ModelId, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS } from "./ChatInput";
import { ChatEmptyState } from "./ChatEmptyState";
@@ -89,10 +90,10 @@ export const Chat = forwardRef(function Chat(
...(initialProjectId !== undefined && { projectId: initialProjectId }),
});
- // Use the actual workspace ID for the WebSocket room subscription.
+ // Read workspace ID from localStorage (set by auth-context after session check).
// Cookie-based auth (withCredentials) handles authentication, so no explicit
// token is needed here — pass an empty string as the token placeholder.
- const workspaceId = user?.currentWorkspaceId ?? user?.workspaceId ?? "";
+ const workspaceId = useWorkspaceId() ?? "";
const { isConnected: isWsConnected } = useWebSocket(workspaceId, "", {});
const { isCommand, executeCommand } = useOrchestratorCommands();
diff --git a/apps/web/src/components/layout/AppSidebar.tsx b/apps/web/src/components/layout/AppSidebar.tsx
index 8be56ff..b678795 100644
--- a/apps/web/src/components/layout/AppSidebar.tsx
+++ b/apps/web/src/components/layout/AppSidebar.tsx
@@ -464,7 +464,7 @@ function UserCard({ collapsed }: UserCardProps): React.JSX.Element {
const displayName = user?.name ?? "User";
const initials = getInitials(displayName);
- const role = user?.workspaceRole ?? "Member";
+ const role = "Member";
return (