DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our marketing pages never read your session, so the header guesses from a cookie with useSyncExternalStore

Fetch the Nakodo home page with curl and the header offers you "Sign in" and "Start free". Open the same URL in a browser where you are signed in and the header offers you "Open the app". Same HTML.

$ curl -s https://nakodo.app/ | grep -o "Sign in\|Open the app" | sort | uniq -c
   4 Sign in
Enter fullscreen mode Exit fullscreen mode

The server never knew. It is not allowed to know, and the whole reason the site is fast is that it does not ask.

Why the server is kept out of it

Nakodo's public pages are the home page, pricing, the guides, the glossary and about seventy others. Every one of them shows the same thing to everybody, so the proxy that refreshes the Supabase session on each request skips them entirely:

matcher: ["/((?!_next/static|_next/image|alcy/|api/cron|...|(?:guides|blog|youtubers|tiktokers|instagrammers|alternatives)(?:/|...)).*)"]
Enter fullscreen mode Exit fullscreen mode

That one unreadable pattern is worth it. A page that reads cookies cannot be a cache hit shared between visitors. A guide page on this site comes back with x-vercel-cache: HIT and an age in the tens of thousands of seconds; it was rendered once and has been handed out ever since.

The cost of that is one piece of UI: the button in the top right. A signed-in visitor who lands on a guide from a search result and sees "Sign in" has been told, by the product, that it has forgotten them.

The hint

const SESSION_COOKIE = /(?:^|;\s*)sb-[^=;]+-auth-token(?:\.\d+)?=/;

const noop = () => () => {};
const hasSession = () => SESSION_COOKIE.test(document.cookie);

function useSignedIn() {
  return useSyncExternalStore(noop, hasSession, () => false);
}
Enter fullscreen mode Exit fullscreen mode

Supabase stores the session in a cookie named sb-<project ref>-auth-token, readable from JavaScript, and splits it into .0 and .1 when it grows past the per-cookie size limit. Hence [^=;]+ for the project ref and the optional (?:\.\d+)? for the chunk suffix. Matching only the exact unsuffixed name is a bug that shows up months later for whichever users happen to have a large JWT.

Three arguments to useSyncExternalStore, and each one is doing something:

noop as the subscribe function. There is no event for "a cookie changed", so there is nothing to subscribe to. Returning a no-op unsubscribe means React reads the snapshot on render and never again until something else re-renders the tree. That is correct here: the session does not change while you read a blog post, and signing in or out is a full page load.

hasSession as the client snapshot. Read at render time, in the browser, where document exists.

() => false as the server snapshot. This is the important one. It is used on the server and again during hydration, so the HTML React builds on the client's first pass is identical to the HTML that came down the wire, and only the commit after hydration swaps in the real answer. Returning the real value here would be a hydration mismatch on every signed-in visit.

Could this be useState plus a useEffect? Yes, and it would be four lines longer and would describe the same behaviour less honestly. The thing being modelled genuinely is an external store: a value that lives outside React, that React cannot subscribe to, and that has a different answer before hydration than after. useSyncExternalStore has a parameter for each of those facts.

It is a hint and nothing else

The comment above it says so, and this matters more than the hook does:

// It is only a hint for which button to show; the proxy and the app check
// the session for real.
Enter fullscreen mode Exit fullscreen mode

Anybody can set that cookie to the string x in a console and get an "Open the app" button. Clicking it hits the proxy, which asks Supabase for real claims, finds none, and redirects to the login page. The regex is allowed to be wrong because the only thing it decides is a word and a destination.

That line between an optimistic client-side guess and the real check is one you have to draw out loud in a comment. The next person to touch this file is one if (useSignedIn()) away from using it to hide something that should be hidden properly.

The other export in the file, and a Suspense rule

export function DeletedNotice() {
  if (!useSearchParams().has("deleted")) return null;
  return <p role="status">Your account and its data have been deleted.</p>;
}
Enter fullscreen mode Exit fullscreen mode

After deleting an account, Settings sends the browser to /?deleted=1 and the home page says the deletion happened. The comment next to it is the part worth carrying away:

// Render inside <Suspense>: reading search params opts out of static HTML.
Enter fullscreen mode Exit fullscreen mode

In the App Router, useSearchParams in a client component makes the whole route render dynamically unless it is wrapped in a Suspense boundary. Forget the boundary and this one three line notice, shown to each user exactly once in their life, un-statics the busiest page on the site.

It is a small thing that generalises: on a static marketing page, every per-visitor detail you add has to be either a client-side guess or a boundary, and if it is neither it quietly takes the page off the cache. The home page is where people meet what the product does, and it should be the fastest page on the site, not the only uncached one.

Top comments (0)