You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When organization.list fails but the auth endpoints are healthy, a returning window renders nothing at all — no message, no retry, indistinguishable from a hang. CollectionsProvider wraps the whole authenticated tree and returns null until it can resolve the window's org, and it deliberately waits for the membership list before deciding. That wait is correct; the blank screen it produces during an outage is not.
This is not the offline case. An offline launch is already handled: the session is in-memory only, so get-session fails and _authenticated/layout.tsx:244 shows "You're offline" with a Retry. The bug needs /api/auth/* to succeed while /api/trpc does not.
Steps
Open the app once and let a window persist its org to the window registry.
Make /api/trpc fail while /api/auth/* keeps serving (bad tRPC deploy, rate limit, or a DB fault in the organizations ⋈ members join — better-auth's cookieCache at packages/auth/src/server.ts:135-138 lets sessions answer without touching those tables).
Relaunch.
Expected: an error state explaining the app can't reach the server, with a way to retry. Actual: an empty window.
Implementation notes
Files
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx:112 — if (windowOrgId != null && organizations == null) return; blocks initialization. The comment above it reasons "the window is showing nothing yet either way", which holds for a few hundred milliseconds and not for an outage.
.../CollectionsProvider.tsx:206 — if (!contextValue) return null; blanks the entire authenticated subtree (_authenticated/layout.tsx:289-336), including <Outlet />, the sidebar and the window-drag regions.
apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx:48 — hostServiceQueryRetry governs cloud queries too, despite the name. It only retries when error.data == null, so a 500 through tRPC gets zero retries; only transport failures retry (3x, ~4.2s total).
Approach
Keep the guard exactly as it is — it exists so a dead registry org can't pin the window to an org whose every read 403s, and that invariant is worth more than the blank screen costs. Instead, distinguish "still loading" from "gave up" at the render site: when the org is unresolved andorganization.list is in error, render an error state with a Retry wired to the query's refetch() rather than null.
_authenticated/layout.tsx:204-259 already has both panels to copy — the offline card and the 15s "Still restoring your session" escape hatch built on useDelayElapsed (:99-102). Neither is applied to org resolution today.
Gotchas
Do not fall back to the registry org on error.initializedRef (:98/:116) is one-shot: adopting an unverified org id on a transient failure means that when the query later succeeds and reveals the id is dead, nothing re-evaluates. That pins the window permanently and is strictly worse than the blank, which does recover.
The blank is not permanent today. refetchOnWindowFocus/refetchOnReconnect are at their defaults and organizations is in the effect's dependency list, so it self-heals on focus or reconnect. The harm is the silence, not the persistence.
There are no component tests for CollectionsProvider, and the app has exactly one React component test overall (components/Redirect/Redirect.test.tsx). Extracting the resolution into a pure function next to evictInactiveOrgs.ts would match the five existing co-located unit tests and let the decision table be tested without a DOM.
Context
When
organization.listfails but the auth endpoints are healthy, a returning window renders nothing at all — no message, no retry, indistinguishable from a hang.CollectionsProviderwraps the whole authenticated tree and returnsnulluntil it can resolve the window's org, and it deliberately waits for the membership list before deciding. That wait is correct; the blank screen it produces during an outage is not.This is not the offline case. An offline launch is already handled: the session is in-memory only, so
get-sessionfails and_authenticated/layout.tsx:244shows "You're offline" with a Retry. The bug needs/api/auth/*to succeed while/api/trpcdoes not.Steps
/api/trpcfail while/api/auth/*keeps serving (bad tRPC deploy, rate limit, or a DB fault in theorganizations ⋈ membersjoin — better-auth'scookieCacheatpackages/auth/src/server.ts:135-138lets sessions answer without touching those tables).Expected: an error state explaining the app can't reach the server, with a way to retry.
Actual: an empty window.
Implementation notes
Files
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx:112—if (windowOrgId != null && organizations == null) return;blocks initialization. The comment above it reasons "the window is showing nothing yet either way", which holds for a few hundred milliseconds and not for an outage..../CollectionsProvider.tsx:206—if (!contextValue) return null;blanks the entire authenticated subtree (_authenticated/layout.tsx:289-336), including<Outlet />, the sidebar and the window-drag regions.apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx:48—hostServiceQueryRetrygoverns cloud queries too, despite the name. It only retries whenerror.data == null, so a 500 through tRPC gets zero retries; only transport failures retry (3x, ~4.2s total).Approach
Keep the guard exactly as it is — it exists so a dead registry org can't pin the window to an org whose every read 403s, and that invariant is worth more than the blank screen costs. Instead, distinguish "still loading" from "gave up" at the render site: when the org is unresolved and
organization.listis in error, render an error state with a Retry wired to the query'srefetch()rather thannull._authenticated/layout.tsx:204-259already has both panels to copy — the offline card and the 15s "Still restoring your session" escape hatch built onuseDelayElapsed(:99-102). Neither is applied to org resolution today.Gotchas
initializedRef(:98/:116) is one-shot: adopting an unverified org id on a transient failure means that when the query later succeeds and reveals the id is dead, nothing re-evaluates. That pins the window permanently and is strictly worse than the blank, which does recover.refetchOnWindowFocus/refetchOnReconnectare at their defaults andorganizationsis in the effect's dependency list, so it self-heals on focus or reconnect. The harm is the silence, not the persistence.CollectionsProvider, and the app has exactly one React component test overall (components/Redirect/Redirect.test.tsx). Extracting the resolution into a pure function next toevictInactiveOrgs.tswould match the five existing co-located unit tests and let the decision table be tested without a DOM.b9d851f93), which added per-window org context.