Skip to content

Web clients scope by the URL org; drop cookie-based org switching#1000

Merged
RhysSullivan merged 1 commit into
mainfrom
claude/stateless-org-client
Jun 13, 2026
Merged

Web clients scope by the URL org; drop cookie-based org switching#1000
RhysSullivan merged 1 commit into
mainfrom
claude/stateless-org-client

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

Stacked on #999 (which is stacked on #974).

What

The client half of stateless URL-scoped orgs. Both API clients (core + account) read the org slug from window.location at request time and send it as the x-executor-organization header, so every request is scoped to the org its own tab's URL names. With requests URL-scoped, the cookie-based switch machinery becomes dead weight and is deleted:

  • switchOrganization endpoint + handler + atom — gone. The org switcher and create-org dialog just navigate to /<slug>; the session already authenticates the user to all their orgs, so there's nothing to switch server-side.
  • ForeignOrgSlug — gone. A slug the caller can't access now resolves to a null org via /account/me, and the shell 404s — no switch-into-org dance, no cookie rotation.
  • OrgSlugGate — reduced to its one remaining job: canonicalize a bare URL onto the active slug; a slug already in the URL is the scope, so it renders through.

The slug is read from window.location (not a React-synced mirror) deliberately: an earlier attempt to mirror it into a module global during render caused an SSR/hydration mismatch and a stuck onboarding redirect. Reading at request time in the browser-only transformClient avoids that entirely.

Why

WorkOS pins one org into the shared wos-session cookie, and the whole browser shares one cookie jar — so under the old model "active organization" was a browser-global. Two tabs couldn't be in two orgs at once: a switch in either tab (or the gate's switch-to-honor-the-URL) silently re-scoped the other tab's session. The URL is now the scope, so the tabs are independent.

Evidence

Cloud e2e (real browser), all green:

  • org-multitab-cookie — flipped from reproducing the cookie-steal corruption to asserting per-tab independence. Two tabs in one BrowserContext (one cookie jar) on different orgs; reads the real outgoing x-executor-organization header off each tab's requests. Tab 2 opening org A no longer re-scopes tab 1 — tab 1's requests stay pinned to org B.
  • org-slug-foreign — opening another of your orgs by its slug works with the session cookie never rewritten (no switch); proves the URL alone is the scope.
  • org-switcher, org-slug-routing — still pass (switcher now navigates by URL; unknown/unauthorized slug → 404).

Gates: typecheck 39/39, lint clean, format clean, react 153 tests, cloud 125 tests.

A recording of the multi-tab independence run is available locally (session.mp4); I can embed it here on request.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

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
executor-marketing 600d7e1 Commit Preview URL

Branch Preview URL
Jun 13 2026, 05:49 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 600d7e1 Jun 13 2026, 05:50 PM

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1000

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1000

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1000

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1000

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1000

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1000

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1000

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1000

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1000

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1000

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1000

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1000

executor

npm i https://pkg.pr.new/executor@1000

commit: 560aa7d

@RhysSullivan RhysSullivan force-pushed the claude/stateless-org-server branch from 8bf8a7f to 17798ec Compare June 13, 2026 17:40
@RhysSullivan RhysSullivan force-pushed the claude/stateless-org-client branch from d78ec37 to 560aa7d Compare June 13, 2026 17:40
@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@RhysSullivan RhysSullivan force-pushed the claude/stateless-org-server branch from 17798ec to be7f2df Compare June 13, 2026 17:45
@RhysSullivan RhysSullivan changed the base branch from claude/stateless-org-server to main June 13, 2026 17:45
The console URL's org slug is now the request scope on the client too: both
API clients (core + account) read the slug from window.location at request
time and send it as the x-executor-organization header, so every tab's
requests are scoped to the org its own URL names — independent of the single
shared session cookie.

With requests URL-scoped, the cookie-based switch machinery is dead weight:
- delete the switchOrganization endpoint/handler/atom; the org switcher and
  create-org just navigate to /<slug> (the session already authenticates the
  user to all their orgs, so there is nothing to switch server-side).
- delete ForeignOrgSlug; a slug the caller can't access now resolves to a
  null org via /account/me and the shell 404s, no switch-into-org dance.
- simplify OrgSlugGate to canonicalize a bare URL onto the active slug and
  otherwise render through (a slug in the URL is already the scope).

Flips the multi-tab scenario from reproducing the cookie-steal corruption to
asserting per-tab independence, and reframes the foreign-slug scenario around
URL scoping (no session switch; the cookie is never rewritten).
@RhysSullivan RhysSullivan force-pushed the claude/stateless-org-client branch from 560aa7d to 600d7e1 Compare June 13, 2026 17:46
@RhysSullivan RhysSullivan merged commit 1f9bfe0 into main Jun 13, 2026
9 checks passed
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