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:
- Route + middleware behavior: 7/7 PASS
- CE tokens (dark mode): 5/5 MATCH
- Inter font via
<link>: PASS (edge-runtime-compatible pattern) - Edge runtime confirmed CF: PASS (server: cloudflare, cf-ray present, no Express header)
- Mobile layout 390×844: 4/4 PASS
- WebAuthn JS API available: PASS
- 404 uses Next.js global not-found (not CRA LandingRoot fallback): PASS
Two follow-up findings are filed; neither blocks Phase 3:
- React hydration errors (CF edge script injection conflict) — tracked, fix before production cutover
/dashboard404 (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.