/cafe/login placeholder LOCAL · GATE 1
Diagnosed live from a user screen-recording (2026-05-22): clicking NIX Cafe on
the Commerce launchpad after an idle period — or after our regression sweep —
bounced through /cafe/login and ended back on the launchpad with
the original destination lost. Verified the root cause against deployed code:
the (authed)/layout.tsx redirect to /login didn't
pass a ?redirect= param, so the Cafe placeholder forwarded to
Commerce with ?redirect=/ — which the H5.8 guard correctly
(per its rules) treats as not-a-product-path and sends to launchpad.
x-nix-pathname on every passed-through
request; new loginRedirectUrl() helper in lib/auth.ts
reads it; 5 server-side redirect("/login") callsites updated to
redirect(await loginRedirectUrl()). The Cafe placeholder
app/login/login-form.tsx already honors the ?redirect=
query — once the path threads through to it, Commerce's H5.8 guard kicks in
and the login form renders for a sibling-product redirect.
| nix-cafe/middleware.ts | Inject x-nix-pathname on every NextResponse.next(...) via new pass() helper |
| nix-cafe/lib/auth.ts | New loginRedirectUrl() export; signIn stub uses it |
| nix-cafe/lib/authz.ts | requirePermission — 2 callsites (no-session + no-accessible-page fallback) |
| nix-cafe/lib/permissions.server.ts | requirePermission — no-session callsite |
| nix-cafe/app/(authed)/layout.tsx | Defensive !session guard — the primary bug site from the video |
| nix-cafe/app/(pos-fullscreen)/pos/register/[configId]/page.tsx | !session?.user guard at top of register page |
middleware.ts injects x-nix-pathname header on every passed-through requestlib/auth.ts exports loginRedirectUrl() with correct shape (Promise<string>, reads header, encoded path)redirect(await loginRedirectUrl()) — not bare redirect("/login")redirect("/login") calls in auth/authz paths (the deliberate signOut path keeps its existing semantics)GET /cafe/dashboard still 307 → /auth/login?redirect=%2Fcafe%2Fdashboard (Commerce)/cafe/orders?status=paid → /auth/login?redirect=%2Fcafe%2Forders%3Fstatus%3DpaidWhat's NOT exercised in Gate 1. The deepest failure path
— valid JWT signature + middleware passes + auth() rejects
on sid mismatch → (authed)/layout.tsx redirects via the new
helper — needs a forged JWT cookie, which requires JWT_SECRET
that isn't in nix-cafe/.env.local (cookies are minted by the
backend at :3001; local cafe dev just verifies them). Plus
the local backend dev server (start-dev.sh) isn't running
this session, so a full backend-mint → cafe-verify round-trip isn't
available either. Gate 2 prod is the right place — we rotate
tenant_users.active_session_token via SQL on a logged-in
lumiere-coffee owner to force the deep-auth-fail path, then assert the
redirect chain end-to-end.
Also: an earlier check probed GET /cafe/login directly via
page.request and hit a 500 — the
AGENTS.md-documented
stale-dev-server gotcha for middleware-bypass routes on a long-running
Next dev server. The placeholder route's CODE is unchanged by this slice;
only its callers (middleware injection + server-side
?redirect=-building callsites) changed. Both are
covered above. Dropping that probe rather than restarting the dev server
since it's not load-bearing.

Push nix-cafe commit to karouna-dev → CF Pages auto-deploys ~90s. Then run a Playwright prod test against lumiere-coffee that:
owner@lumiere-coffee.com — gets a real nix_session cookie with valid sidUPDATE tenant_users SET active_session_token = gen_random_uuid() WHERE id = <owner-id> — invalidates the user's current sid (simulating the regression-sweep rotation)GET /cafe/dashboard with the now-stale cookie → middleware passes, auth() rejects → server-side redirectLocation: /cafe/login?redirect=%2Fcafe%2Fdashboard (the new behavior)/cafe/login?redirect=... → asserts useEffect forwards to /auth/login?redirect=%2Fcafe%2Fdashboard/cafe/dashboard (full happy path)active_session_token if needed (or leave — user already invalidated by submit)Plus 51/51 regression sweep. Risk: low — the change is server-side redirect URL plumbing, no DB, no API contract, no UI rendering. Most likely failure mode is a dev-only TypeScript regression we already cleared with tsc --noEmit.