Raxx · internal docs

internal · gated

Antlers Next.js Staging Tour — Phase 2 QA Report

Date: 2026-05-27
Tour run time: 00:59:06–00:59:16 UTC
Site: https://staging-nextjs.raxx.app
Issue: #2880 — qa(antlers-next): full Playwright tour against staging deploy
Triggered by: Phase 2 deploy — nodejs_compat patch landed (#2945), deploy confirmed HTTP 200
QA agent: raxx-ops-bot — Playwright 1.58.0, Chromium headless


Summary

Phase 2 sign-off: GO for Phase 3 dispatch, with two tracked follow-up findings.

All 7 route/middleware contract tests pass. All 4 mobile layout checks pass. CE tokens resolve correctly in dark mode. Edge runtime is confirmed CF. Two non-blocking findings are filed as follow-up cards and do not gate Phase 3: - React hydration errors present on every page (CF edge script injection conflict) - /dashboard is a planned Phase 0 gap (no page file yet), correctly redirected to but resulting in a 404 landing


1. Route + Middleware Behavior (Phase 0-C contract)

URL Expected HTTP Actual HTTP Expected behavior Observed behavior Result
GET / 200 200 Public landing placeholder, no redirect Status 200, stayed at /, title "Raxx", body contains "Raxx" + "placeholder" PASS
GET /login 200 200 Login placeholder, stays on /login Status 200, final URL /login, h1 "Sign in" PASS
GET /signup 200 200 Signup placeholder, stays on /signup Status 200, final URL /signup, h1 "Create your account" PASS
GET /setup (no session) 307 → /login?next=%2Fsetup 307 redirect Redirect to login with next param Final URL https://staging-nextjs.raxx.app/login?next=%2Fsetup PASS
GET /setup (session=test) 200 200 Setup wizard placeholder renders Status 200, final URL /setup, h1 "Setup wizard (Phase 0-C scaffold)", body contains "placeholder" PASS
GET /login (session=test) 307 → /dashboard 307 redirect Redirect away from login to /dashboard Final URL https://staging-nextjs.raxx.app/dashboard PASS
GET /404-doesnotexist 404 404 Next.js global not-found Status 404, body is Next.js RSC payload (not CRA LandingRoot fallback) PASS

Route pass rate: 7/7 PASS

Redirect chain verification

GET /setup (no session) redirect chain:

307  https://staging-nextjs.raxx.app/setup
     → location: https://staging-nextjs.raxx.app/login?next=%2Fsetup

next param is correctly URL-encoded (%2F), matching the Phase 0-C contract.

GET /login (with session cookie) redirect chain:

307  https://staging-nextjs.raxx.app/login
     → location: https://staging-nextjs.raxx.app/dashboard

Middleware correctly detects cookie presence and routes away from the auth surface.


2. CE Token + Inter Font Rendering (Phase 0-B contract)

Token verification — dark mode (prefers-color-scheme: dark)

Tokens were evaluated via getComputedStyle(document.documentElement) in a Playwright context forced to color_scheme="dark".

Token Expected (brand.css) Actual (computed) Result
--raxx-bg #0b0f14 #0b0f14 MATCH
--raxx-fg #f7f8fa #f7f8fa MATCH
--raxx-ink #0b0f14 #0b0f14 MATCH
--raxx-moss-bright #7fb77e #7fb77e MATCH
--raxx-moss #5b8c5a #5b8c5a MATCH
html background (computed) rgb(11, 15, 20) rgb(11, 15, 20) MATCH

Dark mode: all 5 CE tokens correct.

Light mode behavior (informational)

Headless Chromium defaults to prefers-color-scheme: light. In light mode, brand.css correctly applies its @media (prefers-color-scheme: light) override: - --raxx-bg resolves to #f7f8fa (snow surface — correct per light-mode spec) - --raxx-fg resolves to #0b0f14 (ink text on snow — correct) - --raxx-ink stays #0b0f14 (named alias, not overridden — correct) - --raxx-moss-bright stays #7fb77e (correct — accent unchanged in light mode)

Light mode behavior is correct. The token structure works in both modes.

Inter font loading

Inter font is loaded via <link> tag (not next/font/google), per the Phase 1 Wave A change for CF Pages edge-runtime compatibility.

Check Expected Observed Result
<link rel="stylesheet" href="fonts.googleapis.com/...Inter..."> present Yes https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap PASS
<link rel="preconnect" href="fonts.googleapis.com"> present Yes Yes PASS
next/font (data-nscript style tag) absent Yes (edge-incompatible) Absent PASS

3. Edge Runtime Sanity

Headers captured from GET / response.

Check Expected Observed Result
x-powered-by is NOT Express Not "Express" x-powered-by: Next.js (not Express) PASS
server: cloudflare present Yes server: cloudflare PASS
cf-ray present Yes cf-ray: a02950be49adf3ec-LAX PASS
cf-cache-status present Yes cf-cache-status: DYNAMIC PASS
x-edge-runtime: 1 (bonus) Present INFORMATIONAL

Edge runtime: confirmed CF. All checks pass.

Note on cf-cache-status: DYNAMIC: expected for RSC pages with export const runtime = 'edge'. These are dynamically rendered server responses; static caching would require ISR configuration which is not part of Phase 0.

Full raw header set:

alt-svc: h3=":443"; ma=86400
cf-cache-status: DYNAMIC
cf-ray: a02950be49adf3ec-LAX
content-encoding: br
content-type: text/html; charset=utf-8
server: cloudflare
server-timing: cfCacheStatus;desc="DYNAMIC", cfEdge;dur=7, cfOrigin;dur=22, cfExtPri
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, accept-encoding
x-edge-runtime: 1
x-matched-path: /
x-powered-by: Next.js

4. Mobile (390×844)

All routes tested in a Playwright context with viewport: {width: 390, height: 844}, isMobile: true. Layout OK = document.body.scrollWidth <= window.innerWidth (no horizontal overflow).

Route Status Final URL Layout OK Result
GET / 200 / Yes PASS
GET /login 200 /login Yes PASS
GET /signup 200 /signup Yes PASS
GET /setup (session=test) 200 /setup Yes PASS

Mobile pass rate: 4/4 PASS. No horizontal overflow on any route.


5. WebAuthn Prep (navigator.credentials)

Checked on /login page (desktop context).

Check Expected Observed Result
typeof navigator.credentials "object" "object" PASS
typeof navigator.credentials.get "function" "function" PASS
typeof navigator.credentials.create "function" "function" PASS

WebAuthn JS API is available. Phase 3 passkey ceremony can be wired up client-side. No attempt was made to create or assert a credential — staging-nextjs.raxx.app is a different RP ID from raxx.app and any ceremony would fail by design per the RP ID lock.


6. Console Errors

React hydration errors (errors #418, #423, #425) appear on every page. This is a real finding.

Errors observed (per route)

Route Error count Distinct errors
/ 4 #425 (×2), #418, #423
/login 4 #425 (×2), #418, #423
/signup 4 #425 (×2), #418, #423
/setup (no session, lands at /login) 4 #425 (×2), #418, #423
/setup (with session) 5 #425 (×2), #418, #423, Cannot read properties of null (reading 'document')
/login (with session, lands at /dashboard 404) 4 + 1 #425 (×2), #418, #423, Failed to load resource: 404
/404-doesnotexist 4 + 1 #425 (×2), #418, #423, Failed to load resource: 404

React hydration errors — assessment

React errors #418/#423/#425 are classic hydration mismatch errors. The server-rendered HTML does not match what React expects to hydrate.

Root cause (most likely): Cloudflare Insights analytics (static.cloudflareinsights.com/beacon.min.js) is injected by CF at the edge, modifying the DOM after the server-side HTML is emitted but before React hydrates. The injected <script> node is not present in the RSC render, so React's virtual DOM and the actual DOM diverge.

Secondary candidate: the inline <style> block in app/layout.tsx (the --raxx-font-inter declaration) may conflict with RSC streaming expectations on the edge runtime. This pattern is unusual and merits investigation.

Functional impact: Pages render correctly. Content is present and accessible. React falls back to full client-side rendering after hydration failure. SSR correctness and SEO benefits are degraded. This is not a show-stopper for Phase 3 routing work, but must be resolved before production cutover.

The /setup Cannot read properties of null (reading 'document') error is an additional issue — likely the CF script trying to access document in a context where it is not available (possible timing issue with the RSC edge stream).

Filed as: follow-up finding #1 below

/dashboard 404 — assessment

When /login is visited with a valid session cookie, middleware correctly redirects to /dashboard. However, /dashboard has no page file in the Phase 0 scaffold. The browser lands on a 404.

This is a known Phase 0 gap — not a middleware regression. The middleware contract is honored (it redirects to /dashboard). The gap is that /dashboard does not yet exist as a ported page.

Filed as: follow-up finding #2 below


Screenshots

Located at docs/qa/screenshots/antlers-next-staging-2026-05-27/.

File Description
01-root-desktop.png GET / desktop 1440×900
02-login-desktop.png GET /login desktop 1440×900
03-login-with-session-desktop.png GET /login with session — landed at /dashboard 404
04-signup-desktop.png GET /signup desktop 1440×900
05-setup-no-session-desktop.png GET /setup no session — landed at /login?next=%2Fsetup
06-setup-with-session-desktop.png GET /setup with session — setup placeholder
07-404-desktop.png GET /404-doesnotexist — Next.js 404
mobile-root-390x844.png GET / mobile 390×844
mobile-login-390x844.png GET /login mobile 390×844
mobile-signup-390x844.png GET /signup mobile 390×844
mobile-setup-with-session-390x844.png GET /setup with session mobile 390×844

Findings

Finding 1: React hydration errors on all pages {#finding-1-react-hydration-errors-on-all-pages}

Severity: MEDIUM
Observed: React errors #418, #423, #425 appear in browser console on every route, every page load. Plus Cannot read properties of null (reading 'document') specifically on /setup with session.
Root cause (primary): CF Insights beacon script injected at edge modifies the DOM between RSC HTML emission and React hydration, causing a server/client DOM mismatch.
Root cause (secondary): Inline <style> block in app/layout.tsx may conflict with RSC streaming edge behavior.
Functional impact: Pages render and function. React falls back to full CSR on hydration failure. SSR stream correctness and SEO are degraded.
Phase 3 gate: Not a blocker for Phase 3 routing cutover. Must be resolved before public traffic or production SEO matters.
Recommended fix: Disable CF Insights on staging-nextjs.raxx.app (or defer loading via afterInteractive); investigate whether the inline style can move to a proper CSS file to avoid streaming conflicts.
Filed as: #2947 Linked to epic: #2872

Finding 2: /dashboard route missing from Phase 0 scaffold {#finding-2-dashboard-route-missing-from-phase-0-scaffold}

Severity: LOW (known gap)
Observed: Middleware correctly redirects an authenticated user from /login to /dashboard, but /dashboard has no page file in the Phase 0 scaffold. The browser lands on a 404.
Context: The middleware contract is working correctly per its design. The absence of /dashboard is an expected Phase 0 gap — the dashboard page is not yet ported.
Phase 3 gate: Not a blocker, but /dashboard must exist before Phase 3 cutover would be functional for any authenticated user.
Recommended fix: Add a /dashboard placeholder stub (Phase 0-C pattern) or include /dashboard in the Phase 3 porting scope.
Filed as: #2948 Linked to epic: #2872


Phase 2 Sign-off

GO / NO-GO verdict: GO for Phase 3 dispatch

All Phase 0-C contracts honored:

Two follow-up findings are filed; neither blocks Phase 3:

  1. React hydration errors (CF edge script injection conflict) — tracked, fix before production cutover
  2. /dashboard 404 (Phase 0 known gap) — track in Phase 3 porting scope

Phase 3 may dispatch. The two findings should be linked in the Phase 3 PR as outstanding items.