This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Deprecated. This write-up documents a feature that's no longer part of the app. It's kept here for the history and the reasoning.
Dev notes
A user reported seeing the feature hub when opening the site from Facebook Messenger on mobile, even though they had never logged in. Two independent root causes — one about caching, one about session validation — that produced the same visible symptom.
The site has two top-level views: a public landing page for unauthenticated visitors and a feature hub for logged-in users. The root page.tsx calls auth0.getSession() on the server to decide which one to render. The user opened a shared link inside Facebook Messenger and landed on the hub instead of the landing page. The header showed their name and "no email on file" — a confusing logged-in state where no identity information was actually available.
The symptom has two distinct causes that can each produce it independently.
The root page had no export const dynamic = "force-dynamic". Next.js determines whether a page is dynamic by detecting calls to cookies, headers, or similar request-time APIs. The session check goes through auth0.getSession(), which calls cookies() from next/headers internally — but this detection happens inside the Auth0 library, not directly in the page file. If Next.js missed the indirect call, the page could be treated as statically renderable.
A statically rendered page gets cached at the Vercel edge. If a logged-in user visited first and the hub HTML was cached, that same response would be served to any subsequent visitor — including someone with no session at all opening the link from Messenger. The in-app browser gets the hub HTML, hydrates it, and the user sees a logged-in interface with no real session behind it. Any attempt to use an auth-gated feature would then fail or redirect to login.
The fix is one line: export const dynamic = "force-dynamic" at the top of src/app/page.tsx. This is an explicit opt-out from any caching — Next.js will always render the page fresh per request, so each visitor gets a response built from their own session cookie.
The second cause applies even when caching is not involved. The middleware in src/proxy.ts calls auth0.middleware(request) for the /auth/* OIDC routes and for protected routes like /calendar and /settings. The root / route had no such handling — it fell straight through to a plain NextResponse.next().
This matters because auth0.getSession() reads the encrypted session cookie and trusts it — it does not make a network call to Auth0 to validate the underlying tokens. If a user logged in some time ago and their Auth0 refresh token has since expired, the local cookie still exists, still decrypts successfully, and getSession() returns the session. The page renders the hub. But when the user tries to use any feature — clicking through to the calendar or opening settings — the protected route calls auth0.middleware(), the token refresh fails, the cookie is cleared, and they get redirected to login. The hub looked real but was a ghost.
Facebook Messenger is where this surfaces because its in-app browser on iOS uses SFSafariViewController, which shares the cookie jar with Safari. If the user ever logged in on Safari — even weeks ago — that session cookie is still there. On Android, Messenger opens links in Chrome Custom Tabs, which shares cookies with Chrome. Either way, an old cookie that passed getSession() without being validated against Auth0 produced a hub that appeared real but could not do anything.
The fix: in proxy.ts, when the root route has a session cookie, run auth0.middleware(request) before letting the page render. If the underlying token is still valid, the middleware refreshes it and the hub renders correctly. If the token is expired and cannot be refreshed, the middleware clears the cookie — and page.tsx sees no session and renders the landing page instead.
A separate but related issue: users who authenticated via a social provider that does not share email (Facebook OAuth, Apple Sign-In) would land on the hub with no email visible in the header. The email line was conditionally rendered — it only appeared when userEmail was truthy. For these users the header showed their name but nothing else, which looked broken rather than just private.
The fix renders the email line unconditionally, falling back to "no email on file" when the profile has none. The user can see that the field exists and is empty, rather than wondering why the header feels incomplete.
Nothing here was written wrong. The root page read the session and rendered accordingly, which is correct; it simply never said out loud that it must not be cached. Being dynamic was a property it was given by the framework rather than one it asked for, and properties you were given quietly stop being true.
The detection has to see through auth0.getSession() into a cookies() call inside a library. When it does, everything works and there is nothing to notice. When it does not, the page is cached, and a cache has no idea whose view it is holding.
What catches the class rather than the instance is a test that reads every page which touches the session and fails unless it declares force-dynamic. Writing it immediately found a second page relying on the same detection: /to-do, the admin list. The build marked it dynamic, so nothing was broken — but it was one refactor away from the same bug on the page whose whole point is that other people do not see it.
Update — August 20, 2026
The force-dynamic on the root page held for as long as it was there, but it worked by never caching: every visit to the landing paid a serverless render, and the field vitals showed it as the TTFB floor under the page's LCP and FCP. The interesting discovery, going back in to fix that, was how little of the page ever depended on the session — in v5 the "hub" and the landing are the same component, and the only thing me controlled was the header's log in/log out button and a Settings row in the menu.
So the root page renders no session-derived markup at all now. It is static (regenerated hourly), and the one auth-dependent control resolves its own state client-side from /api/me — the same query the header menu already ran on every other page. A signed-in visitor sees the guest button for one round trip, which shows less than the truth rather than more. This bug cannot recur on that page because the thing that leaked no longer exists in the HTML; a test now pins the page to no session read at all, and the force-dynamic guard keeps protecting every page that still does read one.