Skip to content

204 improve org implementation#205

Merged
InfinityBowman merged 12 commits into
devfrom
204-improve-org-implementation
Jan 1, 2026
Merged

204 improve org implementation#205
InfinityBowman merged 12 commits into
devfrom
204-improve-org-implementation

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Jan 1, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Real-time project membership synchronization
    • Organization selection when creating projects in multi-organization accounts
    • Enhanced projects dashboard with dedicated panel
  • Improvements

    • Simplified project-focused navigation paths
    • Removed workspace switcher from navigation bar
    • Updated dashboard layout with improved project and appraisals sections

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 1, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This pull request transitions the application from organization-scoped to project-scoped routing and architecture. It removes org-context dependencies, updates API endpoints from /orgs/:orgSlug/projects/:projectId to /projects/:projectId, refactors hooks to remove orgId parameters, and introduces real-time membership synchronization and new project-focused UI components.

Changes

Cohort / File(s) Summary
Route & Navigation Updates
packages/web/src/Routes.jsx, packages/web/src/primitives/useRecentsNav.js
Replaced org-scoped project routes (/orgs/:orgSlug/projects/:projectId*) with project-scoped routes (/projects/:projectId*). Added backward compatibility for legacy org paths by normalizing them to new project-scoped equivalents. Updated import of ProjectView from org barrel to project-specific path.
Dashboard & Layout Components
packages/web/src/Layout.jsx, packages/web/src/components/Dashboard.jsx, packages/web/src/components/Navbar.jsx
Mounted RealtimeMembershipSync component in Layout. Replaced org-based redirect/dashboard logic in Dashboard with conditional ProjectsPanel and LocalAppraisalsPanel rendering. Removed workspace switcher and org context from Navbar; changed sign-out redirect to /dashboard instead of /signin.
Removed Organization Page Components
packages/web/src/components/org/OrgProjectsPage.jsx, packages/web/src/components/org/OrgRedirect.jsx, packages/web/src/components/org/index.js
Deleted org-specific pages (OrgProjectsPage, OrgRedirect) and removed their exports from barrel. Retained CreateOrgPage.
New Project-Focused Components
packages/web/src/components/RealtimeMembershipSync.jsx, packages/web/src/components/project/ProjectsPanel.jsx, packages/web/src/components/project/CreateProjectForm.jsx
Added RealtimeMembershipSync to handle real-time project membership invalidation. Introduced ProjectsPanel for user projects grid with create/delete/navigation. Extended CreateProjectForm with org selection UI and auto-select logic.
Project Context & Wrappers
packages/web/src/components/project/ProjectContext.jsx, packages/web/src/components/project/ProjectView.jsx, packages/web/src/components/checklist/ChecklistYjsWrapper.jsx, packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx
Removed orgSlug from ProjectContext value; changed path helpers to project-scoped only. Updated ProjectView and checklist wrappers to use useProjectOrgId instead of org context. Changed useProject calls to accept projectId only (removed orgId parameter). Simplified access-denied redirects to /dashboard.
Sidebar Navigation
packages/web/src/components/sidebar/Sidebar.jsx, packages/web/src/components/sidebar/ProjectTreeItem.jsx, packages/web/src/components/sidebar/ChecklistTreeItem.jsx, packages/web/src/components/sidebar/StudyTreeItem.jsx
Replaced useOrgContext and useOrgProjectList with useMyProjectsList. Removed org-scoped path construction and orgSlug prop passing. Changed ProjectTreeItem useProjectData call signature to accept projectId only. Updated labels from "Workspace" to "Projects".
Completed/Reconciliation Tabs
packages/web/src/components/project/completed-tab/CompletedTab.jsx, packages/web/src/components/project/completed-tab/PreviousReviewersView.jsx, packages/web/src/components/project/overview-tab/OverviewTab.jsx
Removed orgId/orgSlug extraction and usage. Updated useProject and useProjectData calls to omit orgId. Simplified self-member removal navigation to /dashboard without org slug logic.
Project Hooks & Primitives
packages/web/src/primitives/useProject/index.js, packages/web/src/primitives/useProject/connection.js, packages/web/src/primitives/useProjectData.js
Removed orgId parameter from useProject, useProjectData, and createConnectionManager signatures. Changed WebSocket endpoint from /api/orgs/{orgId}/project-doc to /api/project-doc. Removed orgId validation and routing logic.
User & Project Query Hooks
packages/web/src/primitives/useMyProjectsList.js, packages/web/src/primitives/useOrgs.js, packages/web/src/primitives/useProjectOrgId.js, packages/web/src/primitives/useProjectList.js, packages/web/src/primitives/useOrgProjectList.js
Added useMyProjectsList to fetch current user's projects. Added useOrgs hook for user organizations. Introduced useProjectOrgId to resolve orgId from projectId via project store or cache. Added refetchOnMount to useProjectList. Added isInitialLoading to useOrgProjectList.
Query Configuration & Keys
packages/web/src/lib/queryClient.js, packages/web/src/lib/queryKeys.js, packages/web/src/api/better-auth-store.js, packages/web/src/lib/bfcache-handler.js
Added development-mode cache disabling (staleTime/gcTime=0). Updated queryKeys documentation comments for projects. Modified auth store to prefer cached user when loading online. Extended bfcache handler to invalidate both all-projects and per-user project caches.
Durable Objects & Project Doc
packages/workers/src/lib/project-doc-id.js, packages/workers/src/durable-objects/ProjectDoc.js, packages/workers/src/index.js
Changed ProjectDoc naming from orgId:projectId to project:projectId. Updated createConnectionManager, getProjectDocName, and getProjectDocStub to accept projectId only. Removed org membership validation from ProjectDoc; now enforces project-membership-only access. Updated worker router to project-scoped endpoints.
User Session Durable Object
packages/workers/src/durable-objects/UserSession.js, packages/workers/src/durable-objects/__tests__/UserSession.test.js
Removed session CRUD operations (getSession, createSession, updateSession, deleteSession, alarm). Added upfront authentication for WebSocket paths with sessionUserId validation. Simplified to 405 response for non-supported methods. Updated tests to assert limited HTTP handling.
Project Sync & Routes
packages/workers/src/lib/project-sync.js, packages/workers/src/routes/orgs/projects.js, packages/workers/src/routes/projects.js, packages/workers/src/routes/avatars.js
Removed orgId parameter from syncMemberToDO and syncProjectToDO. Updated all getProjectDocStub calls to omit orgId. Expanded sync payloads to include user metadata (name, email, displayName, image).
Invitation & Membership Flows
packages/workers/src/routes/orgs/invitations.js, packages/workers/src/routes/orgs/members.js, packages/workers/src/routes/invitations.js, packages/workers/src/config/validation.js, packages/workers/src/db/schema.js
Added grantOrgMembership field to projectInvitations schema and validation. Changed invitation flow to always grant project membership but optionally grant org membership based on flag. Updated membership notifications from "project-invite" to "project-membership-added/removed/updated" with orgId included. Extended sync payloads with user metadata.
User & Admin Routes
packages/workers/src/routes/users.js, packages/workers/src/routes/admin/users.js
Added GET /api/users/me/projects endpoint to fetch current user's projects with orgId. Included orgId in user projects endpoint results. Updated admin user deletion to use project-scoped syncMemberToDO calls.
Database Migration & Test Helpers
packages/workers/migrations/0003_worried_skreet.sql, packages/workers/migrations/meta/0003_snapshot.json, packages/workers/migrations/meta/_journal.json, packages/workers/src/__tests__/helpers.js
Added migration for grantOrgMembership column on projectInvitations. Added schema snapshot. Updated test helpers getProjectDocName and clearProjectDOs to use project-scoped naming.
Utility Updates
packages/workers/src/lib/entitlements.js
Enhanced isSubscriptionActive to handle both Date objects and Unix timestamps.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Notif as Notification Stream
    participant RMS as RealtimeMembershipSync
    participant QC as TanStack Query Client
    participant Cache as Query Cache

    User->>Notif: Project membership changes
    Notif->>RMS: Emit membership-added/removed/updated
    
    rect rgb(200, 220, 255)
        Note over RMS,Cache: Cache Invalidation Flow
        RMS->>QC: invalidateQueries(queryKeys.projects.all)
        QC->>Cache: Clear all projects
        
        RMS->>QC: invalidateQueries(queryKeys.projects.list(userId))
        QC->>Cache: Clear user's project list
        
        alt orgId present in event
            RMS->>QC: invalidateQueries(queryKeys.projects.byOrg(orgId))
            QC->>Cache: Clear org-scoped projects
        end
    end
    
    Cache-->>User: Triggers refetch on next access
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • add tanstack query #193: Modifies the same TanStack query infrastructure and related imports/hooks (adding queryClient/queryKeys and migrating components to use tanstack-solid-query), creating overlapping change surface.
  • 119 project add member without account #174: Both PRs touch the same project membership notification/sync surface (worker routes emit project-membership-added/removed/updated events that RealtimeMembershipSync listens for and cache invalidates).
  • 59 yjs websocket refactor #60: Both PRs modify the same project connection code (packages/web/src/primitives/useProject/connection.js), including createConnectionManager signature and WebSocket endpoint switching.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title '204 improve org implementation' is vague and does not accurately reflect the actual scope of changes. The PR significantly refactors the application from organization-scoped to project-scoped architecture, removes workspace UI features, introduces real-time synchronization, and restructures multiple API endpoints—far beyond improving org implementation. Update the title to reflect the main refactoring, such as 'Migrate from org-scoped to project-scoped architecture' or 'Refactor application to project-centric model with real-time sync'.
Docstring Coverage ⚠️ Warning Docstring coverage is 70.45% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jan 1, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates 0ae5368 Commit Preview URL Jan 01 2026, 11:19 PM

@InfinityBowman
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 1, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/workers/src/durable-objects/UserSession.js (1)

28-31: Consider adding X-Internal-Request header verification for /notify endpoint.

Per coding guidelines, internal sync endpoints should not be exposed publicly without X-Internal-Request verification. The /notify endpoint accepts notification payloads and could be abused if called externally. Consider adding header verification:

Proposed fix
     // Handle internal notification requests (from other workers/DOs)
-    if (path.endsWith('/notify') && request.method === 'POST') {
+    if (path.endsWith('/notify') && request.method === 'POST') {
+      // Verify internal request
+      if (request.headers.get('X-Internal-Request') !== 'true') {
+        return new Response(JSON.stringify({ error: 'Forbidden' }), {
+          status: 403,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+        });
+      }
       return await this.handleNotification(request, corsHeaders);
     }
packages/workers/src/index.js (1)

351-362: Stale error message in legacy handler.

The legacy /api/project/:projectId handler still references the old org-scoped path that itself now returns 410. Update the message to point to the new project-scoped endpoint.

Proposed fix
 const legacyProjectDocHandler = c =>
   c.json(
     {
       error: 'ENDPOINT_MOVED',
-      message: 'This endpoint has been moved. Use /api/orgs/:orgId/project-doc/:projectId instead.',
+      message: 'This endpoint has been moved. Use /api/project-doc/:projectId instead.',
       statusCode: 410,
     },
     410,
   );
packages/workers/src/routes/orgs/invitations.js (2)

432-442: Avoid exposing emails in client-facing error responses.

The error metadata includes both userEmail and invitationEmail, which are returned to the client. While useful for debugging, this could expose PII (the invitation email) to unauthorized parties. Consider limiting client-facing error details.

Suggested fix
     if (normalizedUserEmail !== normalizedInvitationEmail) {
       console.error(
         `[Invitation] Email mismatch: user email="${currentUser.email}", invitation email="${invitation.email}"`,
       );
       const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
         reason: 'email_mismatch',
-        userEmail: currentUser.email,
-        invitationEmail: invitation.email,
       });
       return c.json(error, error.statusCode);
     }

494-524: Race condition: duplicate org membership possible without check inside batch.

The member table has no unique constraint on (organizationId, userId). The check for existing membership (lines 495-499) runs outside the batch while the insert (lines 503-511) runs inside. Between the check and batch execution, concurrent requests could create duplicate memberships.

Move the existence check inside the batch, or add a unique constraint on (organizationId, userId) in the schema, or use an upsert pattern with onConflictDoNothing() on the insert.

🧹 Nitpick comments (12)
packages/web/src/primitives/useProject/connection.js (1)

63-70: Consider adding projectId validation for defensive coding.

The guard at line 64 validates ydoc and checks isLocalProject(), but doesn't validate that projectId is a non-empty string. If projectId is undefined/null, the WebsocketProvider would receive an invalid room parameter.

This is likely validated upstream, but adding a guard here would make the function more robust.

Proposed defensive check
 function connect() {
-  if (!ydoc || isLocalProject()) return;
+  if (!projectId || !ydoc || isLocalProject()) return;
packages/web/src/primitives/useRecentsNav.js (1)

59-92: Consider optimizing regex matching strategy.

The current implementation matches the path multiple times with increasingly specific patterns (project → study → checklist). Each regex match performs a full scan of the path string. A more efficient approach would be to match the most specific patterns first and fall back to less specific ones, avoiding redundant work.

Additionally, the current regex patterns don't handle optional trailing slashes (e.g., /projects/123/ vs /projects/123), which could result in duplicate entries in recents for the same logical page.

Proposed optimization
function parsePathToRecentItem(path) {
+  // Normalize trailing slashes
+  const normalizedPath = path.replace(/\/$/, '');
+  
-  // Match project pages: /projects/:projectId
-  const projectMatch = path.match(/^\/projects\/([^/]+)/);
-  if (projectMatch) {
-    const [, projectId] = projectMatch;
-    // Check if it's a study page: /projects/:projectId/studies/:studyId
-    const studyMatch = path.match(/^\/projects\/([^/]+)\/studies\/([^/]+)/);
-    if (studyMatch) {
-      const [, , studyId] = studyMatch;
-      // Check if it's a checklist page
-      const checklistMatch = path.match(
-        /^\/projects\/([^/]+)\/studies\/([^/]+)\/checklists\/([^/]+)/,
-      );
-      if (checklistMatch) {
-        return {
-          type: 'checklist',
-          path,
-          projectId,
-          studyId,
-          checklistId: checklistMatch[3],
-        };
-      }
-      return {
-        type: 'study',
-        path,
-        projectId,
-        studyId,
-      };
-    }
-    return {
-      type: 'project',
-      path,
-      projectId,
-    };
-  }
+  // Match most specific patterns first (checklist → study → project)
+  let match = normalizedPath.match(/^\/projects\/([^/]+)\/studies\/([^/]+)\/checklists\/([^/]+)$/);
+  if (match) {
+    return {
+      type: 'checklist',
+      path: normalizedPath,
+      projectId: match[1],
+      studyId: match[2],
+      checklistId: match[3],
+    };
+  }
+
+  match = normalizedPath.match(/^\/projects\/([^/]+)\/studies\/([^/]+)$/);
+  if (match) {
+    return {
+      type: 'study',
+      path: normalizedPath,
+      projectId: match[1],
+      studyId: match[2],
+    };
+  }
+
+  match = normalizedPath.match(/^\/projects\/([^/]+)$/);
+  if (match) {
+    return {
+      type: 'project',
+      path: normalizedPath,
+      projectId: match[1],
+    };
+  }

Apply similar normalization to the legacy path handling section.

packages/web/src/primitives/useOrgProjectList.js (2)

47-53: Consider adding refetchOnMount: 'always' for consistency.

Based on the pattern in packages/web/src/primitives/useMyProjectsList.js (lines 38-50), consider adding refetchOnMount: 'always' to ensure the project list stays fresh when membership changes occur while the app is closed or when navigating back to this view.

Proposed enhancement
    return {
      queryKey: queryKeys.projects.byOrg(currentOrgId),
      queryFn: () => fetchOrgProjects(currentOrgId),
      enabled: enabledOption !== false && !!currentOrgId,
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 10, // 10 minutes
+     refetchOnMount: 'always', // Always refetch on mount to catch membership changes that occurred while app was closed
    };

59-59: LGTM! Consider adding to useMyProjectsList for consistency.

The isInitialLoading implementation correctly distinguishes between initial loading (no data yet) and subsequent refetches. This is a useful addition for managing different loading UI states.

For consistency, consider adding the same property to packages/web/src/primitives/useMyProjectsList.js.

packages/web/src/lib/queryClient.js (1)

196-209: Consider the implications of disabling cache in development.

While disabling the cache in development (staleTime: 0, gcTime: 0) provides predictable fresh data, it creates a behavioral difference between development and production that could hide caching-related issues:

  • Cache invalidation logic won't be exercised during development
  • Race conditions or stale data bugs may only surface in production
  • Developers won't experience the actual user cache behavior

This is a common pattern with clear benefits (faster dev feedback, no confusion from stale data), but be aware that production-specific caching bugs may slip through.

packages/workers/src/routes/admin/users.js (1)

482-490: Minor cleanup opportunity: orgId is fetched but unused.

The query fetches orgId from the joined projects table, but it's no longer used after the syncMemberToDO signature change. Consider removing it to reduce query overhead, unless it's intentionally kept for future use or logging.

Proposed cleanup
     // Fetch all projects the user is a member of before any deletions (with orgId)
     const userProjects = await db
       .select({
         projectId: projectMembers.projectId,
-        orgId: projects.orgId,
       })
       .from(projectMembers)
-      .innerJoin(projects, eq(projectMembers.projectId, projects.id))
       .where(eq(projectMembers.userId, userId));
packages/web/src/api/better-auth-store.js (1)

217-225: Consider parallelizing the query invalidations.

Both invalidateQueries calls are independent and could be executed in parallel using Promise.all for slightly better performance.

Proposed refactor
-            await queryClient.invalidateQueries({
-              queryKey: queryKeys.projects.all,
-            });
-            // Also invalidate legacy query key for backward compatibility
-            await queryClient.invalidateQueries({
-              queryKey: queryKeys.projects.list(currentUser.id),
-            });
+            await Promise.all([
+              queryClient.invalidateQueries({
+                queryKey: queryKeys.projects.all,
+              }),
+              // Also invalidate legacy query key for backward compatibility
+              queryClient.invalidateQueries({
+                queryKey: queryKeys.projects.list(currentUser.id),
+              }),
+            ]);
packages/workers/src/routes/avatars.js (1)

34-47: Remove unused orgId from the database query.

The query still selects orgId from the database (line 38), but it's no longer used after removing it from the loop destructuring (line 45). Consider removing orgId from the SELECT clause for clarity and minor performance improvement.

Proposed refactor
-    // Get all projects the user is a member of (with orgId for DO addressing)
+    // Get all projects the user is a member of
     const memberships = await db
       .select({
         projectId: projectMembers.projectId,
-        orgId: projects.orgId,
       })
       .from(projectMembers)
-      .innerJoin(projects, eq(projectMembers.projectId, projects.id))
       .where(eq(projectMembers.userId, userId));

Note: If projects.orgId is no longer needed, the join with projects table may also be unnecessary unless it serves as a filter (to ensure the project exists).

packages/workers/src/routes/users.js (1)

134-167: New /me/projects endpoint duplicates /:userId/projects logic.

This endpoint provides the same functionality as /:userId/projects when called with the current user's ID. Consider extracting the shared query logic into a helper function to reduce duplication.

Suggested refactor to reduce duplication
// Helper function for fetching user projects
async function fetchUserProjects(db, userId) {
  return db
    .select({
      id: projects.id,
      name: projects.name,
      description: projects.description,
      orgId: projects.orgId,
      role: projectMembers.role,
      createdAt: projects.createdAt,
      updatedAt: projects.updatedAt,
    })
    .from(projects)
    .innerJoin(projectMembers, eq(projects.id, projectMembers.projectId))
    .where(eq(projectMembers.userId, userId))
    .orderBy(desc(projects.updatedAt));
}

// Then use in both endpoints:
// /me/projects
const results = await fetchUserProjects(db, authUser.id);

// /:userId/projects  
const results = await fetchUserProjects(db, userId);
packages/web/src/primitives/useProjectOrgId.js (1)

19-41: Consider accepting a getter function for defensive reactivity.

The hook currently accepts projectId as a direct parameter, but if it's ever passed a reactive accessor (vs. a static string), changes won't be tracked—SolidJS memos only track reactive primitives accessed during execution. While all current callers pass values at component body level (which re-execute appropriately when route params/props change), accepting a getter function would make the hook more robust:

-export function useProjectOrgId(projectId) {
+export function useProjectOrgId(projectIdAccessor) {
   const queryClient = useQueryClient();
 
   return createMemo(() => {
+    const projectId = typeof projectIdAccessor === 'function' ? projectIdAccessor() : projectIdAccessor;
     if (!projectId) return null;

Alternatively, document that projectId must be a static value, not a reactive accessor.

packages/web/src/primitives/useMyProjectsList.js (1)

53-61: Consider separating background refetch state from initial loading.

The current isLoading includes isFetching, which will be true during background refetches even when stale data is available. This may cause the UI to show loading spinners during silent background refreshes.

If consumers need to distinguish between initial load and background refresh, consider:

Possible refinement
 return {
   projects: () => query.data || [],
-  isLoading: () => query.isLoading || query.isFetching,
+  isLoading: () => query.isLoading,
+  isFetching: () => query.isFetching,
   isInitialLoading: () => query.isLoading && !query.data,
   isError: () => query.isError,
   error: () => query.error,
   refetch: () => query.refetch(),
   query,
 };

This would let consumers show a subtle refresh indicator (via isFetching) without replacing content with a loading skeleton.

packages/web/src/components/project/ProjectsPanel.jsx (1)

107-125: Use handleFetchError for fetch error handling.

Per coding guidelines, frontend fetch calls should use handleFetchError from @/lib/error-utils.js instead of a dynamic import of handleError. This provides consistent error handling with optional toast display.

Suggested refactor

Add the import at the top of the file:

import { handleFetchError } from '@/lib/error-utils.js';

Then update the error handling:

     try {
       const response = await fetch(
         `${API_BASE}/api/orgs/${project.orgId}/projects/${targetProjectId}`,
         {
           method: 'DELETE',
           credentials: 'include',
         },
       );
       if (!response.ok) {
-        const data = await response.json().catch(() => ({}));
-        throw new Error(data.error || 'Failed to delete project');
+        await handleFetchError(response, { showToast: true, toastTitle: 'Delete Failed' });
+        return;
       }

       queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
       showToast.success('Project Deleted', 'The project has been deleted successfully');
-    } catch (err) {
-      const { handleError } = await import('@/lib/error-utils.js');
-      await handleError(err, { toastTitle: 'Delete Failed' });
+    } catch (err) {
+      showToast.error('Delete Failed', err.message || 'An unexpected error occurred');
     }

Comment on lines 56 to 65
createEffect(() => {
const pid = params.projectId;
const oid = orgId();
if (pid && oid) {
projectActionsStore._setActiveProject(pid, oid);
if (pid) {
if (oid) {
projectActionsStore._setActiveProject(pid, oid);
}
connect();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine ProjectView.jsx to understand the context
find . -name "ProjectView.jsx" -type f | head -5

Repository: InfinityBowman/corates

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Search for useProjectOrgId hook definition
rg -l "useProjectOrgId" --type=js --type=jsx --type=ts --type=tsx

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find projectActionsStore and understand setActiveProject
rg -l "projectActionsStore" --type=js --type=jsx --type=ts --type=tsx | head -10

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for the connect function and how it's used
rg -n "const connect|function connect|export.*connect" packages/web/src --type=js --type=jsx --type=ts --type=tsx -A 5

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Read ProjectView.jsx directly to see the context
cat -n ./packages/web/src/components/project/ProjectView.jsx | head -100

Repository: InfinityBowman/corates

Length of output: 4737


🏁 Script executed:

#!/bin/bash
# Search for useProjectOrgId definition without strict type filtering
rg "export.*useProjectOrgId|const useProjectOrgId|function useProjectOrgId" -A 20

Repository: InfinityBowman/corates

Length of output: 1760


🏁 Script executed:

#!/bin/bash
# Search for projectActionsStore definition and _setActiveProject
rg "projectActionsStore" -A 5 -B 5 | head -150

Repository: InfinityBowman/corates

Length of output: 12521


🏁 Script executed:

#!/bin/bash
# Find connect function that's used in ProjectView
rg "connect()" -B 5 -A 5 packages/web/src | head -100

Repository: InfinityBowman/corates

Length of output: 6920


🏁 Script executed:

#!/bin/bash
# Find projectActionsStore implementation to understand how _setActiveProject and _setConnection work
cat -n packages/web/src/stores/projectActionsStore/index.js | head -200

Repository: InfinityBowman/corates

Length of output: 6825


🏁 Script executed:

#!/bin/bash
# Look for how getActiveConnection is implemented
rg -n "getActiveConnection|_setActiveProject|_setConnection" packages/web/src/stores/projectActionsStore/index.js -A 3

Repository: InfinityBowman/corates

Length of output: 1314


🏁 Script executed:

#!/bin/bash
# Check if there are operations that don't require activeProject to be set
rg -n "getActiveConnection\(\)" packages/web/src/stores/projectActionsStore -A 10 | head -80

Repository: InfinityBowman/corates

Length of output: 6611


🏁 Script executed:

#!/bin/bash
# Check useProjectOrgId more thoroughly to understand if it's async
cat -n packages/web/src/primitives/useProjectOrgId.js

Repository: InfinityBowman/corates

Length of output: 1559


🏁 Script executed:

#!/bin/bash
# Check if connect() is async and what it does
rg -n "export.*connect|const connect" packages/web/src/primitives/useProject/index.js -A 20 | head -100

Repository: InfinityBowman/corates

Length of output: 2013


🏁 Script executed:

#!/bin/bash
# Understand when _setConnection is called during connect()
rg -n "_setConnection" packages/web/src/primitives/useProject/index.js -B 5 -A 5

Repository: InfinityBowman/corates

Length of output: 744


🏁 Script executed:

#!/bin/bash
# Check if there are any immediate operations attempted on ProjectView mount
rg -n "projectActionsStore\.(study|pdf|project|checklist|member|reconciliation)\." packages/web/src/components/project/ProjectView.jsx -B 2 -A 2

Repository: InfinityBowman/corates

Length of output: 1654


🏁 Script executed:

#!/bin/bash
# Look at the pending effects more carefully to see what they depend on
sed -n '88,160p' packages/web/src/components/project/ProjectView.jsx

Repository: InfinityBowman/corates

Length of output: 3051


🏁 Script executed:

#!/bin/bash
# Check what getActiveProjectId error message says and if it's thrown
rg -n "getActiveProjectId()" packages/web/src/stores/projectActionsStore/index.js -B 2 -A 8

Repository: InfinityBowman/corates

Length of output: 2172


🏁 Script executed:

#!/bin/bash
# Verify if operations in ProjectView actually call getActiveProjectId before orgId is set
sed -n '105,200p' packages/web/src/components/project/ProjectView.jsx

Repository: InfinityBowman/corates

Length of output: 3805


🏁 Script executed:

#!/bin/bash
# Check if createStudyActions uses getActiveOrgId and what happens if it's null
rg -n "getActiveOrgId" packages/web/src/stores/projectActionsStore/studies.js -B 2 -A 5

Repository: InfinityBowman/corates

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Check the exact flow - what does study.create do when getActiveOrgId is called
sed -n '230,270p' packages/web/src/stores/projectActionsStore/studies.js

Repository: InfinityBowman/corates

Length of output: 1263


Set active project regardless of orgId availability.

The project connection is established via connect() (line 63) before _setActiveProject is called (line 61), which is gated on orgId availability (line 60). Since useProjectOrgId is a synchronous memo that may return null if orgId is not yet synced from Y.js or available in the query cache, the activeProjectId remains null if orgId fails to load.

Operations that depend on activeProjectId will throw "No active project - are you inside a ProjectView?" when invoked:

  • User-triggered operations like projectActionsStore.project.rename() and projectActionsStore.project.updateDescription() (lines 281-282) have no guards
  • These call getActiveConnection()getActiveProjectId() which throws if activeProjectId is null

Since the WebSocket connection is project-scoped and doesn't require orgId (as noted in the comment on line 46), call _setActiveProject(pid, oid) unconditionally when pid exists, even if oid is null. This ensures activeProjectId is set and operations don't fail with a misleading error message.

🤖 Prompt for AI Agents
In packages/web/src/components/project/ProjectView.jsx around lines 56 to 65,
currently _setActiveProject(pid, oid) is only called when oid is truthy which
can leave activeProjectId null; change the logic to call
projectActionsStore._setActiveProject(pid, oid) unconditionally whenever pid
exists (even if oid is null) and then call connect(); remove or bypass the oid
guard so the active project is always set before or alongside establishing the
WebSocket connection.

Comment on lines +12 to +63
/**
* Internal component that handles notifications for a specific userId
* This is recreated when userId changes, ensuring the hook uses the correct userId
*/
function MembershipSyncForUser(props) {
// Create notification handler that invalidates queries
const handleNotification = notification => {
const notificationType = notification.type;

// Handle project membership change events
if (
notificationType === 'project-membership-added' ||
notificationType === 'project-membership-removed' ||
notificationType === 'project-membership-updated'
) {
// Invalidate all project list queries
queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });

// Also invalidate legacy query keys for backward compatibility
if (props.userId) {
queryClient.invalidateQueries({
queryKey: queryKeys.projects.list(props.userId),
});
}

// If orgId is provided, also invalidate org-scoped project list
if (notification.orgId) {
queryClient.invalidateQueries({
queryKey: queryKeys.projects.byOrg(notification.orgId),
});
}
}
};

const notifications = useNotifications(props.userId, {
onNotification: handleNotification,
});

// Connect when component mounts
createEffect(() => {
if (props.userId) {
notifications.connect();
}
});

// Cleanup on unmount
onCleanup(() => {
notifications.disconnect();
});

return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the useNotifications hook
fd -t f "useNotifications" --extension js --extension jsx --extension ts --extension tsx

Repository: InfinityBowman/corates

Length of output: 113


🏁 Script executed:

# Examine the RealtimeMembershipSync component to see the full context
fd -t f "RealtimeMembershipSync" --extension jsx --extension js

Repository: InfinityBowman/corates

Length of output: 120


🏁 Script executed:

# Search for useNotifications hook definition
rg "function useNotifications|const useNotifications|export.*useNotifications" -A 5

Repository: InfinityBowman/corates

Length of output: 662


🏁 Script executed:

cat -n packages/web/src/primitives/useNotifications.js

Repository: InfinityBowman/corates

Length of output: 5928


🏁 Script executed:

cat -n packages/web/src/components/RealtimeMembershipSync.jsx

Repository: InfinityBowman/corates

Length of output: 2932


Component will reuse stale WebSocket connection when a different user logs in.

The issue is critical and confirmed: When a user logs out and a different user logs in, the MembershipSyncForUser component updates its props but does not remount. Since useNotifications captures userId at initialization (line 46) and the hook doesn't support reactive userId updates, it never reconnects to the new user's WebSocket endpoint. The existing connection persists with the old userId, causing membership event cache invalidations to go to the wrong user's cache.

The Show component with callback pattern (lines 71-72) re-evaluates when the condition changes but does not remount the child component when the value changes from one truthy userId to another. The key={userId()} attribute on line 72 has no effect in SolidJS—it's just a regular prop and doesn't trigger remounting.

To fix this, use Show's keyed prop to force remounting:

Recommended fix using Show keyed prop
 export default function RealtimeMembershipSync() {
   const { user, isLoggedIn } = useBetterAuth();
 
   return (
-    <Show when={isLoggedIn() && user()?.id}>
+    <Show when={isLoggedIn() && user()?.id} keyed>
-      {userId => <MembershipSyncForUser userId={userId()} key={userId()} />}
+      {userId => <MembershipSyncForUser userId={userId} />}
     </Show>
   );
 }

The keyed prop forces Show to remount its children whenever the value changes, ensuring MembershipSyncForUser reinitializes with the correct userId and creates a fresh WebSocket connection.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/web/src/components/RealtimeMembershipSync.jsx around lines 12 to 63,
MembershipSyncForUser can reuse a stale WebSocket when a different user logs in
because the parent Show wrapper does not remount the child on userId change;
update the parent Show to use the keyed prop (keyed={userId()}) instead of
relying on a regular prop so the child is remounted whenever userId changes,
ensuring useNotifications is reinitialized and a fresh connection is created for
the new user.

// This ensures useNotifications is called with the correct userId
return (
<Show when={isLoggedIn() && user()?.id}>
{userId => <MembershipSyncForUser userId={userId()} key={userId()} />}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove ineffective key attribute.

SolidJS doesn't use the key attribute for forcing component remounting like React does. The key attribute has no special meaning here and should be removed.

Proposed fix
-      {userId => <MembershipSyncForUser userId={userId()} key={userId()} />}
+      {userId => <MembershipSyncForUser userId={userId()} />}

If you need to force remounting when userId changes, use the keyed prop on the Show component instead (see previous comment).

🤖 Prompt for AI Agents
In packages/web/src/components/RealtimeMembershipSync.jsx around line 72, the
JSX is passing a React-style key prop to a SolidJS component: "{userId =>
<MembershipSyncForUser userId={userId()} key={userId()} />}", which is
ineffective in Solid and should be removed; delete the key attribute from the
MembershipSyncForUser element, and if you need to force remount when userId
changes use Solid's keyed behavior (e.g., use keyed prop on Show or a keyed
list) instead of a key attribute.

@InfinityBowman InfinityBowman merged commit 9f5b0e1 into dev Jan 1, 2026
3 of 4 checks passed
@InfinityBowman InfinityBowman deleted the 204-improve-org-implementation branch January 1, 2026 23:25
This was referenced Jan 8, 2026
This was referenced Jan 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants