# SPA Auth Redirect Loop — Diagnostic Workflow ## Symptoms User visits `https://domain/` and the page flickers/refreshes repeatedly, alternating between two URLs (e.g., `index.html` and `login.html`). The browser URL bar visibly changes back and forth. ## Root Causes (check in order) 1. **Host header mismatch** — VPS `proxy_set_header` sends a Host the home nginx doesn't recognize. Request hits the default server block instead of the correct one. PocketBase/Firebase API calls fail or hit wrong context → auth state flips → redirect loop. 2. **Stale auth token in localStorage** — PocketBase JS SDK reads an expired token from `localStorage`, initially considers it valid (JWT not yet expired), but the first API call returns 401 → authStore clears → redirect fires. On the next page load, the token is read from localStorage again → cycle repeats. 3. **Bounce-back doesn't clear the stale token** — Even after the architecture is fixed (one-way redirects), the protected pages' `onAuthStateChanged(null)` handlers redirect to the login page WITHOUT clearing the stale `pocketbase_auth` from localStorage. The login page's inline check (`if(localStorage.getItem('pocketbase_auth'))`) fires again → redirects back → infinite loop. **Fix**: add `localStorage.removeItem('pocketbase_auth')` immediately before `window.location.href = 'index.html'` in every protected page's auth-failure path. See `references/auth-redirect-loop.md` in the `shop-pro-quote` skill for the full file list. 4. **Browser-cached 302 redirect** — after fixing the server, the user still sees the old loop because their browser cached the 302 redirect from before the fix. `curl -skI` returns 200, but the browser shows a redirect. ## Diagnostic Steps ### 0. FULL-TEXT GREP FIRST (do not skip this) **Before looking at individual files or nav links, grep EVERY JS file for all redirect sources at once.** The redirect can be buried anywhere — an auth listener, a guard function, a keyboard shortcut handler, an inline script. Nav links are the most visible but often NOT the root cause. ```bash # Find ALL redirect sources in one sweep grep -rn "window\.location\|location\.replace\|location\.href\|meta.*refresh" --include="*.js" --include="*.html" . ``` Missing a hidden `onAuthStateChanged` redirect is the #1 diagnostic failure in SPA redirect loops. The fix may look deceptively simple (rename files, update nav links) but the real loop lives in auth listeners that fire on state changes. ### 1. Check what the server actually returns ```bash curl -skI https://domain/ # HTTP/1.1 200 OK → server is fine, problem is client-side # HTTP/1.1 302 + Location: login.html → server still has old redirect ``` ### 2. Check if the target file exists ```bash curl -skI https://domain/login.html # 404 → file was deleted, redirect is cached # 200 → file still exists, redirect is real ``` ### 3. Check VPS ↔ Home Host header alignment ```bash # On VPS: what Host header goes to home? grep "proxy_set_header Host" /etc/nginx/sites-enabled/ # On home: what server_name does nginx expect? grep "server_name" /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* ``` ### 4. Check for stale PocketBase auth token Open browser DevTools → Application → Local Storage → look for `pocketbase_auth` key. If present, the SDK will try to use it on page load. If the token is expired server-side but not client-side, this creates the loop. ## Fix Patterns ### Pattern A: Server alias (preferred) Add the VPS domain as a server_name alias on the home nginx config: ```bash sudo sed -i 's/server_name grajmedia.duckdns.org;/server_name grajmedia.duckdns.org ;/' /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` Allows `Host: ` to reach the correct server block without changing the VPS config. ### Pattern B: Fix Host header on VPS ```nginx proxy_set_header Host grajmedia.duckdns.org; ``` Matches what the home nginx expects. Only affects one service. ### Pattern C: File rename — swap index and login When the root `index.html` is the dashboard (auth-guarded) and `login.html` is separate, visiting `/` triggers: load dashboard → no auth → redirect to login.html → has auth → redirect to index.html → loop. **Fix**: Make `index.html` the login page, rename dashboard to `dashboard.html`, update ALL references: ```bash # 1. Backup cp login.html _login.html.bak && cp index.html _index.html.bak # 2. Swap cp login.html index.html && cp _index.html.bak dashboard.html && rm login.html # 3. Update JS references (grep first to find them all) grep -rn "login\.html\|index\.html" --include="*.js" --include="*.html" . # 4. Bulk-fix auth redirects: login.html → index.html grep -rl "'login\.html'" --include="*.js" . | xargs sed -i "s/'login\.html'/'index.html'/g" # 5. Bulk-fix nav links: index.html → dashboard.html grep -rl 'href="index\.html"' --include="*.html" . | xargs sed -i 's|href="index\.html"|href="dashboard.html"|g' # 6. Fix login-redirect targets: index.html? → dashboard.html? sed -i "s/'index\.html?/'dashboard.html?/g" login.js # after-login redirect ``` Full file list from a real fix (13 files, 28 references): - `dashboard.js`, `login.js`, `main.js`, `appointments.js`, `repair-orders.js`, `customers.js` - `shared/header-functionality.js` - `dashboard.html`, `appointments.html`, `repair-orders.html`, `customers.html` - `index.html` (the new login page — inline redirect scripts) ### Pattern D: Stub page cache-breaker When the browser has a cached 302 redirect to a now-deleted file, put a minimal stub back that redirects to the correct page with no-cache headers: ```html Redirecting…

Redirecting… click here if not redirected.

``` This breaks the cached redirect: the browser loads the stub, which instantly takes them to the correct page. After a few visits the cache clears naturally. ### Pattern E: Remove auto-redirect from login page's onAuthStateChanged When BOTH the login page and the dashboard have `onAuthStateChanged` listeners that redirect to each other, any auth-state instability creates a loop: ``` dashboard.js: onAuthStateChanged(null) → redirect to login.html login.js: onAuthStateChanged(user) → redirect to index.html → LOOP ``` **Fix**: Patch the login page so `onAuthStateChanged` only manipulates UI (show/hide elements), never redirects. Only explicit login/signup button click handlers should redirect. Use an inline ` ``` This localStorage check fires before any modules load, has no auth-state dependency, and never causes a loop. It delegates the "already logged in" redirect to a simple token presence check, while login.js's `onAuthStateChanged` becomes UI-only. ## Worked Example: ShopProQuote **Domain**: `grajmedia.duckdns.org` (VPS proxied to home:443) **Backend**: PocketBase at `127.0.0.1:8091` **Problem**: Infinite redirect between `index.html` (dashboard) and `login.html`, later between `login.html` → `dashboard.html` → `index.html` **Root causes found (all three were active simultaneously)**: 1. `shopproquote.graj-media.com` VPS config sent `Host: graj-media.com` but home nginx only had `server_name grajmedia.duckdns.org` → Host header mismatch → request landed on wrong server block 2. `login.js`'s `onAuthStateChanged` auto-redirected to `dashboard.html` on auth state — coupled with dashboard.js's redirect back to `index.html` on null, any PocketBase auth instability created a loop. Also fired on `onAuthStateChanged` registration which could race with the initial state. 3. After all server-side fixes, browser had cached 302 redirects from the old setup **Fix applied (order matters — do all three)**: 1. File swap (Pattern C): `index.html` → login page, `dashboard.html` → dashboard, 28 references across 13 files updated 2. Patch `login.js` (Pattern E): `onAuthStateChanged` → UI-only (shows/hides form vs loading), no redirect. Only explicit login/signup + inline localStorage check redirect. 3. Server alias (Pattern A): added `graj-media.com shopproquote.graj-media.com` to home shopproquote nginx `server_name` 4. Stub page (Pattern D): created minimal `login.html` with no-cache redirect to `index.html` to break cached browser redirects, then deleted after cache cleared