fix(onboarding): stop the completed-onboarding guard reading a shared cache on the server - #181
Open
bookingseo wants to merge 1 commit into
Open
Conversation
… cache on the server `beforeLoad` runs on the server for the initial document request, and `queryClient` lives in module scope, which a worker isolate reuses across requests. `["onboardingAnswers"]` carries no user id and the default staleTime is 5 minutes, so `ensureQueryData` on the server can resolve with the *previous* request's answers and decide this visitor's redirect from another account's `completedAt` — sending someone who has not onboarded to `/`, or showing the wizard to someone who finished it. Measured on a dev worker rather than argued: the hook logs `isServer=true`, a module counter increments across separate document requests, and a value written into the shared client during one request is read back by the next. Nothing rehydrates that cache into the browser either — there is no `dehydrate` or `HydrationBoundary` anywhere — so the server-side read buys nothing even when it happens to be correct. Marking the route `ssr: false` keeps the guard in the browser, where the cache is per-tab. That matches what `/_project/p/$projectId` already does, and for the same reason: the page renders inside `ClientOnly`, so SSR only produced empty chrome. It also stops a failing server-side read from turning the whole document into a 500 — locally, `/onboarding` went from 500 to 200. The test pins the rule repo-wide: no route lifecycle hook may touch the shared query client unless the route is client-only. Verified in both directions — commenting out the new `ssr: false` reddens it.
HasimEmre
pushed a commit
to HasimEmre/open-seo
that referenced
this pull request
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
/_authenticated/onboarding/gates its redirect on a shared cache read that runs on the server:beforeLoadruns on the server for the initial document request.queryClientlives in module scope (src/client/tanstack-db/queryClient.ts), which a worker isolate reuses across requests.["onboardingAnswers"]carries no user id, and the defaultstaleTimeis 5 minutes.So within that window, a signed-in visitor's
ensureQueryDatacan resolve with the previous request's answers and decide their redirect from another account'scompletedAt— bouncing someone who has not onboarded to/, or showing the wizard to someone who already finished.Measured, not argued
On a dev worker running this branch's base, with a probe temporarily added to that
beforeLoad:Three separate document requests.
isServer=true— the hook runs on the server. The module counter increments across requests — module scope survives them. And a value written into the sharedqueryClientduring one request is read back by the next, which is exactly the mechanismensureQueryDatarides.Control: hooks under
/_project/p/$projectIdproduce no server-side hits at all, because that subtree is alreadyssr: false.Nothing rehydrates this cache into the browser either — there is no
dehydrateorHydrationBoundaryin the repo — so the server-side read buys nothing even when it happens to be correct.The fix
ssr: falseon the route. The guard then runs only in the browser, where the cache is per-tab.This is what
/_project/p/$projectId/route.tsxalready does, with the same rationale ("SSR would only render empty chrome") — and it applies here for the same reason: the app tree renders insideClientOnly, so this route never had server-rendered content to lose. Client navigation is unchanged;beforeLoadstill blocks before the component renders.Side effect worth having: a failing server-side read no longer turns the whole document into a 500. Locally,
/onboarding?step=0went 500 → 200 on this base.The test
src/client/lib/route-lifecycle-query-client.test.tspins the rule repo-wide: no routebeforeLoad/loadermay reach the shared query client unless the route declaresssr: false. Today that is one route — this one.Verified in both directions: commenting out the new
ssr: falsereddens it. The exemption reads the declared option after stripping comments, so a commented-outssr: false,cannot buy an exemption — an earlier version of the check was fooled by exactly that, and only re-running the control against the real route tree caught it.Happy to drop the test if a repo-wide structural check is more than you want here; the one-line fix stands on its own.
Checks
tsc --noEmitclean,oxlint0 warnings / 0 errors,vitest run736 passed. Prettier clean on both changed files.