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 /vitals. 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, loading vitals — 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.