Session dies mid-session on read-only navigation ~1h after login
SEPARATE from TERRA-19 (the swallow). Mechanism read from installed code + measured GoTrue config by coder-terra-cc 2026-08-16: jwt_exp=3600, refresh_token_rotation_enabled=true, reuse_interval=10s. @supabase/ssr 0.12.3 sets autoRefreshToken:false, which disables only the background timer; auth-js 2.110.8 __loadSession refreshes past EXPIRY_MARGIN regardless and then persists. On the Server Component path (auth.ts:154 getSessionUser) cookies are NOT writable, so ~1h in the persist throws, GoTrue has already rotated and revoked the old refresh token after 10s, and the cookie keeps a dead token. Next navigation: refresh fails, getUser null, silent logout mid-session. Route Handler (callback/route.ts:196) and Server Action (session-actions.ts:10) paths are unaffected - cookies writable. Unplanned partial mitigation: any Server Action rewrites the cookie, so read-only navigation is what dies. LATENT: ~3 active users, none reach 1h; arrives with the cohort (166 memberships, ~163 never logged in). Three candidate shapes, undecided: (a) middleware refresh - canonical Supabase pattern but a new always-on request-path component on a project that has never had one; (c) reduce dependence on the GoTrue cookie post-login since terra already resolves identity by authId through pg. Decide AFTER TERRA-19 makes the failure observable. Symptom presents as 'it logs me out for no reason' and is unattributable with current telemetry.
Questions
Activity
-
OBSERVED IN PRODUCTION 2026-08-14 07:12Z, one user - no longer latent. coder-terra-cc correlated Vercel runtime logs to appEvents to the second: 07:12:14 GET / 307 with AuthApiError refresh_token_not_found (cookie ALREADY held a dead refresh token on arrival), same error across four renders through 07:12:19; then three sign-in attempts - 07:12:20 signInInitiated / 07:12:25 signInCompleted exchangeFailed, 07:12:30 / 07:12:44 noProfile, 07:12:53 / 07:12:57 authorized actorUserId 6ebaffae. User arrived logged out without being told and needed THREE attempts to return. ATTRIBUTION HELD, do not upgrade this: refresh_token_not_found proves only that the cookie held a token GoTrue rejects. An aged cookie or a sign-out elsewhere produce the IDENTICAL error, and TERRA-20's second front door was still open on 08-14 (closed 08-16), which is a live confound. This is the observation TERRA-19's instrument exists to attribute, not a confirmation of the rotate-then-failed-persist mechanism.
-
status=backlog
-
1
-
Instrument plane corrected be8e838 v0.16.8 - see TERRA-24. Attribution for the 2026-08-14 07:12Z occurrence remains UNRESOLVED and is what TERRA-19/24's instrument exists to supply.
-
SEQUENCING RULED 2026-08-16 by pm-terra-cc, on coder-terra-cc raising it as a project call (tag coder-terra-cc-msvhw519sxn0). SHIP THIS NOW. Reasons in order, and the second is the load-bearing one. (1) A P1 that logs real users out outranks an observability exercise. You do not hold a live defect open to keep an instrument able to prove itself. (2) THE CHOICE WAS FALSE. The stated fork was "fix TERRA-22 and TERRA-24 loses the natural occurrence it is waiting for". TERRA-24 does not need a natural occurrence - it needs the cookie write to FAIL. That is injectable: an unwritable cookie store in a test exercises the swallow on demand. An injected-effect fixture does not decay and does not depend on production reproducing a condition we are about to remove, so it is the better instrument even setting TERRA-22 aside. TERRA-24 is reframed accordingly. If it turns out not to be constructible, that is a real fork and comes back. FIX SHAPE APPROVED: (a) middleware, running where cookies are writable so the rotation persists. Shape (c) - dropping dependence on the GoTrue cookie - was WITHDRAWN BY THE CODER and the withdrawal is the record: terra resolves roles and identity from pg by authId and never trusts JWT claims, but getUser() is a live GoTrue call that VALIDATES the token, and that validation is the authentication. Remove the refresh and the token simply expires at 1h. CONSTRAINTS, both from the coder and both accepted: matcher excludes _next/static and assets so a GoTrue round-trip is not added to every file; and it fails OPEN on its own errors, because terra has never carried an always-on request-path component and one that throws is an every-route 500. FAIL-OPEN NEEDS ITS REASON IN THE DIFF so review does not read it as a relaxed auth gate. Middleware REFRESHES, it does not authorize. resolveAccess runs downstream and still denies. Middleware that throws breaks every route; middleware that declines yields a request that meets the real gate unchanged. That is why fail-open is correct HERE and is not a precedent for failing open in a component that decides access. MECHANISM, re-read from installed auth-js 2.110.8 rather than memory: getUser to _getUser to _useSession to __loadSession, which refreshes past EXPIRY_MARGIN and persists - so every Server-Component render past ~1h triggers rotate-then-fail-to-persist. GoTrueClient.js:2622-2666. New detail: on AuthSessionMissingError, _getUser calls _removeSession - another cookie WRITE on the same unwritable path - and returns {user:null} rather than throwing, which is why the symptom is a silent logged-out render and not an error. CLASS-A: pre-implementation audit ping, diff review before push, full PTD. NO BEFORE/AFTER READING EXISTS. TERRA-23 closed with the verdict that it cannot detect this defect at this population - the interval it measures is time-to-logout plus time-until-next-visit, and nobody in this cohort browses across the boundary. Verify by the mechanism, never by that screen.
-
Class-A pre-push diff review dispatched to audit-terra-ca (pm-terra-cc-msvlfuzyt430). Built by coder-terra-cc, uncommitted, 4 new files, nothing modified: src/proxy.ts, src/lib/proxy-session.ts, src/lib/proxy-matcher.ts, src/lib/proxy-session.test.ts. 8 tests green, tsc --noEmit clean. DELIBERATE FORK from the pm plan that changed the file set: next/server is not loadable under bare node --test (measured ERR_MODULE_NOT_FOUND), so the untestable boundary was drawn AT the Next import - proxy-session.ts takes structural RequestLike/ResponseLike plus injected passThrough, proxy.ts supplies NextResponse.next, and the matcher moved to its own module so the test asserts the SHIPPED string rather than a retyped copy. Coder's stated coverage bound, carried forward rather than dropped: client and response factory are both injected, so the tests cover terra's half of the contract with @supabase/ssr and NOT @supabase/ssr, auth-js, GoTrue, or Next's matcher compilation - and there is NO evidence the ~1h logout is gone. Test (e) anchors the raw matcher string with ^...$, testing the pattern's intent, not routing. Audit asked to judge whether that boundary leaves anything load-bearing untested and whether the coverage bound is drawn wider than the tests support. Push waits on the verdict.
-
BLOCK from audit-terra-ca (msvlgn5o6q52). The uncommitted diff is NOT BUILDABLE: pnpm build fails in Next 16.2.10/Turbopack at src/proxy.ts:65:14 - 'Next.js can't recognize the exported config field... matcher[0] need to be static strings or static objects'. config.matcher cannot consume the imported PROXY_MATCHER, so the matcher-module split moved a load-bearing Next static-analysis requirement onto the untested side. THE REUSABLE FINDING, worth more than the fix: 97/97 tests passed and pnpm typecheck passed, both before and after. Neither checker can see this class of defect. Turbopack's static analysis of an exported config field is a THIRD checker, and it is the one that reads the shape a module split changes. A green tsc --noEmit plus green tests is not a green build. This is the second time in one day on terra that a stack of independent green checks missed the actual defect (TERRA-35: four green checks over source and units while the OnFailure= trigger path was broken) - and both times the finding came from exercising the real mechanism rather than from adding another static check. The Class-A pre-push gate is what caught it. Had TERRA-22 been tiered Class-S on 'coder self-verifies', an unbuildable diff reaches the push lane and the failure surfaces as a Vercel build failure on production's deploy queue. Audit accepted the rest: the injected-seam design is justified, and the coder's stated coverage bound is correct as written - the tests prove request-first write-through, options preservation, the rebuilt response, getUser invocation, and structural matcher intent, and they prove NOTHING about @supabase/ssr, auth-js, GoTrue, Next routing, or whether the ~1h logout is gone. FIX DISPATCHED (pm-terra-cc-msvlh8yk6e1e): matcher returns to a literal in src/proxy.ts; anti-drift preserved by extracting the source literal in the test or asserting source equality, NOT by retyping the string into the test - that reintroduces the drift the split existed to prevent. Re-run test, typecheck AND build before re-submitting.
-
BLOCK on build; rework dispatched to coder-terra-cc
-
IMPLEMENTATION SHIPPED, BUG NOT VERIFIED FIXED. c33f37a..9e9911b, v0.18.0. Committed files read from push output, not predicted: src/proxy.ts, src/lib/proxy-session.ts, src/lib/proxy-session.test.ts (new) + package.json. 468 insertions. TERRA-24's two files listed as stray-outside---only and did not go. CLASS-A PTD, four planes: - /api/health cache-busted: 0.18.0, dpl_B4VPd2fTbTkupdx4iurE4HpZPTJq, dbOk true, 09:29:34Z, matching package.json at origin/main. - Deployment: READY, target production, aliased to terra-enamel.pensanta.com, gru1, turbopack. - SHA read from the deployment record regardless of the version match: githubCommitSha 9e9911b9790cac443f4a11a273e9d7a8d9b32053. - Build log: 'f Proxy (Middleware)' in the route table at 09:28:20, Next.js 16.2.10 Turbopack, compiled 7.0s, TypeScript clean. The proxy is in the DEPLOYED OUTPUT, not merely in the repo. RUNTIME PLANE IS UNREAD, NOT CLEAN - coder stated the limit rather than the result, correctly. / /login /mi-cursada exercised (307/200/307, the expected unauthenticated shape); the runtime log for that window shows only serverless entries and NO edge-middleware entries at all. That is not evidence the proxy did or did not execute. Vercel's runtime plane is not surfacing proxy invocations for this project. No errors in the window. THE BOUND, CARRIED UNSOFTENED AND WRITTEN INTO THE COMMIT MESSAGE IN THESE WORDS: this closes the implementation, not the bug. audit's PASS is unit and build proof. Nothing exercises an aged live session, a GoTrue rotation, or browser cookie acceptance, and NOTHING SHOWS THE ~1h LOGOUT HAS STOPPED. The WI stays open for that reason - closing it here would close it on work it never named. A FAILED PUSH BURNS A VERSION, found by reading back: the first attempt printed 0.17.0, the pre-push hook rejected it, but sitebump runs BEFORE the hook so 0.17.0 stayed on disk and the successful re-run bumped to 0.18.0. The number printed by a failed run is not the number that ships. Coder would have reported 0.17.0 from memory. UNCOMMITTED WORK IN A SHARED TREE IS NOT ISOLATED FROM ANOTHER PUSH'S GATE: typecheck is tree-wide, so TERRA-24's unsubmitted file BLOCKED this push on noUncheckedIndexedAccess until fixed. It cannot dirty the commit; it can stop it.
-
Implementation shipped at 9e9911b v0.18.0 and verified present in the deployed build output. The BUG is unverified: no aged live session has been exercised. Blocked on eq 32 (live session-expiry test) with Elazar - a real browser session must age past an hour. Until then the fix is shipped and unvalidated.