Skip to content

Scope Channels (bluebird) browser tabs to the signed-in user#3164

Draft
adamleithp wants to merge 2 commits into
mainfrom
posthog-code/per-user-browser-tabs
Draft

Scope Channels (bluebird) browser tabs to the signed-in user#3164
adamleithp wants to merge 2 commits into
mainfrom
posthog-code/per-user-browser-tabs

Conversation

@adamleithp

@adamleithp adamleithp commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

Channels/canvas browser tabs are persisted machine-wide, not per user. Everyone who logs into the app on the same machine shares one tab strip: logging out leaves the previous user's tabs behind, and switching accounts doesn't restore that user's own tabs.

Why

Tabs should follow the login, not the install — logging into an account should bring back that account's tabs, and switching users should restore theirs (raised for PostHog Code / project bluebird).

Changes

  • browser_windows rows now carry an owning account (account_key + cloud_region, the same convention as auth_preferences; migration 0017); tabs inherit scope through their window FK. Load/save in BrowserTabsRepository only touch the active account's rows.
  • BrowserTabsService gets setAccountScope() with a three-state scope: undetermined (app boot before auth resolves, session restoring, identity fetch not yet succeeded — tabs opened then are adopted into the account's snapshot once the scope arrives, so nothing is lost to the swap), signed out (memory-only blank strip by design), and scoped (login loads that account's persisted snapshot and fans it out to all windows).
  • Rows persisted before scoping existed are adopted by the first account that signs in, so existing users keep their tabs after upgrading.
  • AuthState exposes accountKey, with a getAccountScope() helper encoding the three-way scope; the Electron main process wires auth state changes to the tabs service.
  • AuthService gains a session-epoch staleness guard: a background session restore that completes after a logout (or after a newer login) is discarded instead of resurrecting the old session's state and tab scope.
  • setActiveTab ignores tab ids not present in the window, so stale renderer state (router history stamped under a previous account's snapshot) can't leave a dangling activeTabId.

How did you test this?

  • Unit tests: browser-tabs-repository.test.ts (scope isolation across accounts and regions, round-trip, FK-cascade replace, legacy-row adoption), service.test.ts (per-account restore on user switch, adoption of pre-scope tabs with dedupe and focus, signed-out isolation, setActiveTab guard), authIdentity.test.ts (three-way scope resolution), and new auth.test.ts races (restore completing after logout, stale restore vs newer login) — 82 tests passing across the touched suites.
  • pnpm typecheck clean across all 22 packages; Biome clean on changed files.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

Tab strips were stored machine-wide, so every account on a machine shared
one set of tabs and logout left the previous user's tabs behind. Persist
them per (cloudRegion, accountKey) instead: each login restores that
user's tabs, switching accounts swaps snapshots, and signed-out sessions
are memory-only so no account's saved tabs get overwritten. Pre-account
rows are adopted by the first login so existing users keep their tabs.

Generated-By: PostHog Code
Task-Id: b06ce1b6-aef9-4164-8274-0ae6374ca8c1
@trunk-io

trunk-io Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 9882bd0.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. packages/workspace-server/src/services/browser-tabs/service.test.ts, line 1549-1560 (link)

    P2 Prefer parameterised test for emission-count assertions

    The "emits… but not on a repeat" test drives the same setAccountScope→count-check pattern twice (once for no-op on repeat, once for null scope). Per the project's preference for parameterised tests, this could be expressed as a single it.each table with [scope, expectedCount] rows, making it easier to add further edge cases.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "Scope Channels browser tabs to the signe..." | Re-trigger Greptile

@@ -0,0 +1,2 @@
ALTER TABLE `browser_windows` ADD `account_scope` text;--> statement-breakpoint
CREATE INDEX `browser_windows_account_scope_idx` ON `browser_windows` (`account_scope`); No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The file is missing a trailing newline, which can cause issues with some SQL tools and shows as a diff noise marker (\ No newline at end of file) in git diff.

Suggested change
CREATE INDEX `browser_windows_account_scope_idx` ON `browser_windows` (`account_scope`);
CREATE INDEX `browser_windows_account_scope_idx` ON `browser_windows` (`account_scope`);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fixes from review of the initial per-user scoping:

- AuthService now tracks a session epoch: a background session restore
  that finishes after a logout (or after a newer login) is discarded
  instead of resurrecting the old session — previously it would re-apply
  the logged-out user's tab scope and could clobber a fresh login.
- The tabs service treats "scope not determined yet" (app boot before
  auth resolves, offline restore, failed identity fetch) as a distinct
  state from "signed out": tabs opened during that window are adopted
  into the account's snapshot when the scope arrives, instead of being
  silently discarded with the swap.
- setActiveTab ignores tab ids not present in the window, so stale
  renderer state (router history from a previous account) can't leave a
  dangling activeTabId.
- browser_windows now scopes by (account_key, cloud_region) columns,
  matching the auth_preferences convention, instead of a combined
  string; migration 0017 regenerated (unshipped).
- save() relies on the window FK cascade instead of an extra subquery
  DELETE per mutation; repository mock extracted per convention.

Generated-By: PostHog Code
Task-Id: b06ce1b6-aef9-4164-8274-0ae6374ca8c1
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