Skip to content

fix(auth): eliminate multi-tab org context bleed#2507

Open
viktormarinho wants to merge 1 commit intomainfrom
fix/multi-tab-org-context
Open

fix(auth): eliminate multi-tab org context bleed#2507
viktormarinho wants to merge 1 commit intomainfrom
fix/multi-tab-org-context

Conversation

@viktormarinho
Copy link
Contributor

@viktormarinho viktormarinho commented Feb 26, 2026

Summary

  • Root cause: shell-layout.tsx called setActive() on every org navigation, writing activeOrganizationId to the shared server-side session. The last tab to navigate won, causing every other open tab to fetch data from the wrong org.
  • Two separate request surfaces needed fixing: MeshContext (MCP routes) and Better Auth's own org-management routes.

Changes

Server — MeshContext / MCP routes

  • context-factory.ts: when x-org-id header is present on a browser session request, resolve org directly from DB with membership verification instead of reading session.activeOrganizationId. The MCP client already sends x-org-id on every call, so all MCP tool operations are now per-request rather than per-session.

Client — Better Auth org-management routes

  • org-store.ts (new): module-level per-tab org ID store. Each browser tab has its own JS execution context so this is naturally isolated.
  • auth-client.ts: fetchOptions.onRequest injects organizationId on every Better Auth organization route call — as a query param for GET requests (listMembers, listRoles, getFullOrganization) and in the request body for POST requests (inviteMember, removeMember, createRole, updateRole, deleteRole). Better Auth already accepts an explicit organizationId that overrides the session fallback on all these endpoints.
  • shell-layout.tsx: replaced setActive() with getFullOrganization({ query: { organizationSlug } }). Pure read — zero session mutation. Populates the org store so all subsequent calls are scoped correctly.
  • organizations-home.tsx, inbox.tsx: update org store on explicit intentional org switches so the store stays current.

Testing

  • bun run check — all workspaces pass
  • bun test apps/mesh/src/core/context-factory.test.ts — 11/11 pass
  • Manual: open two tabs on different orgs, confirm each tab shows data from its own org

🤖 Generated with Claude Code


Summary by cubic

Fixes multi-tab org context bleed by removing session mutations on nav and scoping org per request/per tab. Each browser tab now stays in its own org without clobbering others.

  • Bug Fixes
    • Server: prefer x-org-id to resolve org from DB with membership check; fall back to session if absent. MCP requests are now per-request, not session-scoped.
    • Client: add per-tab org store; auth-client injects organizationId on Better Auth org routes (GET query, POST body), skipping set-active.
    • shell-layout: replace setActive() with getFullOrganization() and seed the org store.
    • organizations-home/inbox: update the org store on explicit org switches.
    • Tests: unit tests pass; verified two tabs can operate in different orgs.

Written for commit f67f5be. Summary will update on new commits.

Two browser tabs open on different orgs would clobber each other's
session because shell-layout called setActive() on every navigation,
writing activeOrganizationId to the shared server-side session.

Fix across both request surfaces:

Server (MeshContext / MCP routes):
- context-factory.ts: when x-org-id header is present on a browser
  session request, resolve org directly from DB with membership
  verification instead of reading session.activeOrganizationId.
  The MCP client already sends x-org-id on every call, so all MCP
  tool operations are now per-request rather than per-session.

Client (Better Auth org-management routes):
- org-store.ts: module-level (per-tab) store for the current org ID.
  Each browser tab has its own JS execution context so this is
  naturally isolated.
- auth-client.ts: fetchOptions.onRequest injects organizationId on
  every Better Auth organization route call — as a query param for
  GET requests (listMembers, listRoles, getFullOrganization) and in
  the request body for POST requests (inviteMember, removeMember,
  createRole, updateRole, deleteRole). Better Auth accepts an
  explicit organizationId that overrides the session fallback.
- shell-layout.tsx: replaced setActive() with getFullOrganization()
  using the org slug from the URL. Pure read — zero session mutation.
  Also populates the org store so subsequent calls are scoped to the
  correct tab org from the start.
- organizations-home.tsx, inbox.tsx: update org store when the user
  explicitly switches orgs so the store stays current.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.119.1-alpha.1
🎉 Patch 2.119.1
❤️ Minor 2.120.0
🚀 Major 3.0.0

Current version: 2.119.0

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/layouts/shell-layout.tsx">

<violation number="1" location="apps/mesh/src/web/layouts/shell-layout.tsx:200">
P1: Bug: `setCurrentOrgId` inside `queryFn` won't fire on cache hits, causing stale org context when revisiting an org.

With `staleTime: Infinity` + `refetchOnMount: false`, navigating org A → org B → back to org A returns cached data for A without executing `queryFn`. The org store keeps B's ID, so all subsequent auth-client calls target the wrong org.

Move the store sync out of `queryFn` into a `useEffect` that runs whenever the query data changes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


// Populate the per-tab org store so the auth client injects organizationId
// on all subsequent Better Auth org-management calls from this tab.
setCurrentOrgId(data?.id ?? null);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 26, 2026

Choose a reason for hiding this comment

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

P1: Bug: setCurrentOrgId inside queryFn won't fire on cache hits, causing stale org context when revisiting an org.

With staleTime: Infinity + refetchOnMount: false, navigating org A → org B → back to org A returns cached data for A without executing queryFn. The org store keeps B's ID, so all subsequent auth-client calls target the wrong org.

Move the store sync out of queryFn into a useEffect that runs whenever the query data changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/layouts/shell-layout.tsx, line 200:

<comment>Bug: `setCurrentOrgId` inside `queryFn` won't fire on cache hits, causing stale org context when revisiting an org.

With `staleTime: Infinity` + `refetchOnMount: false`, navigating org A → org B → back to org A returns cached data for A without executing `queryFn`. The org store keeps B's ID, so all subsequent auth-client calls target the wrong org.

Move the store sync out of `queryFn` into a `useEffect` that runs whenever the query data changes.</comment>

<file context>
@@ -187,10 +188,17 @@ function ShellLayoutContent() {
 
+      // Populate the per-tab org store so the auth client injects organizationId
+      // on all subsequent Better Auth org-management calls from this tab.
+      setCurrentOrgId(data?.id ?? null);
+
       return {
</file context>
Fix with Cubic

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.

1 participant