Files
route-commerce/docs/superpowers/plans/2026-05-23-login-production-failure.md
T

10 KiB

Login Page Production Failure — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Diagnose why the login page works in development but fails in production, then implement a fix.

Architecture: The login flow involves three layers: (1) the login page form posting to /api/login, (2) the API route validating credentials and setting auth cookies, (3) middleware reading cookies to restore sessions. Production failures typically stem from cookie security settings, environment variable mismatches, or proxy/hosting platform differences.

Tech Stack: Next.js 16 App Router · Supabase Auth · Vercel (production) · Node.js


File Structure

  • src/app/login/page.tsx — Login form UI, posts to /api/login
  • src/app/api/login/route.ts — Validates credentials, creates admin_users, sets rc_access_token and rc_uid cookies
  • src/middleware.ts — Reads auth cookies (rc_auth_uid, rc_auth_token, dev_session) to restore session
  • src/lib/admin-permissions.tsgetAdminUser() resolves auth from cookies, dev bypass, proxy UID, Supabase token
  • src/lib/admin-permissions-service.ts — Creates admin_users via Supabase REST API (apikey-only, no Authorization header)

Diagnostic Tasks

Task 1: Reproduce and Instrument the Production Failure

Files:

  • Modify: src/app/api/login/route.ts:1-120

  • Modify: src/middleware.ts:1-100

  • Step 1: Add diagnostic logging to /api/login route

Open src/app/api/login/route.ts and add console logging at each failure point. The route currently handles three failure modes: (1) credentials rejected by Supabase, (2) admin_users lookup fails, (3) cookie setting fails.

// Add at the top of the POST handler, after extracting formData
console.log("[/api/login] isProd:", isProd, "NODE_ENV:", process.env.NODE_ENV);
console.log("[/api/login] Email:", email, "Password length:", password?.length);
console.log("[/api/login] Supabase URL:", supabaseUrl ? "set" : "MISSING");
console.log("[/api/login] Anon key:", supabaseAnonKey ? "set" : "MISSING");
console.log("[/api/login] Service role:", serviceRoleKey ? "set" : "MISSING");
  • Step 2: Add diagnostic logging to middleware auth resolution

Open src/middleware.ts and log each auth resolution path at the start of the middleware function:

console.log("[middleware] pathname:", request.nextUrl.pathname);
console.log("[middleware] dev_session:", devSession);
console.log("[middleware] rc_auth_uid:", rcAuthUid);
console.log("[middleware] rc_auth_token:", rcAuthToken ? "present" : "MISSING");
console.log("[middleware] rc_access_token:", rcAccessToken ? "present" : "MISSING");
console.log("[middleware] isProd:", process.env.NODE_ENV === "production");
  • Step 3: Deploy and reproduce

Run npm run build && npm run start locally in production mode (or deploy to Vercel) and attempt login. Capture all console output from both the API route and middleware.

Expected: You will see which path fails — either the Supabase signInWithPassword call, the admin_users creation, or the cookie setting.


Files:

  • Review: src/app/api/login/route.ts:90-110 (cookie options)

  • Review: src/middleware.ts:23-54 (cookie reading)

  • Review: src/app/api/water-admin-auth/route.ts (another cookie example)

  • Step 1: Document cookie options in each file

The /api/login route sets cookies with these options (lines 99-105):

const cookieOpts = {
  path: "/",
  maxAge: 60 * 60 * 24 * 30,
  httpOnly: true,
  sameSite: "lax",
  ...(isProd ? { secure: true } : {}),  // secure:true only in production
};

In production (HTTPS), secure: true is required for cookies to work. In development (HTTP), secure: false is needed. The conditional ...(isProd ? { secure: true } : {}) handles this.

  • Step 2: Verify the isProd variable is correctly computed

Find where isProd is defined in src/app/api/login/route.ts. It should be:

const isProd = process.env.NODE_ENV === "production";

If NODE_ENV is set differently in production (e.g., not explicitly set, or set to "production" but the Next.js server runs differently on Vercel), this could cause the cookie to be set without secure: true in production, making the cookie get rejected by the browser.

  • Step 3: Check if SameSite=lax causes issues on Vercel

On some Vercel configurations, SameSite=lax combined with secure: true can cause issues if the domain has redirects or if there's a proxy in front. Try changing to sameSite: "none" with secure: true in production (requires HTTPS), or investigate if the cookie domain needs to be explicitly set.


Task 3: Verify Environment Variables in Production

Files:

  • Review: src/app/api/login/route.ts:20-35

  • Review: .env.example

  • Review: .env.local (development values)

  • Step 1: List all env vars used in the login flow

The login route uses:

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
  • Step 2: Check Vercel environment variables

In Vercel dashboard → Settings → Environment Variables, verify:

  1. NEXT_PUBLIC_SUPABASE_URL is set (not NEXT_PUBLIC_ prefix in some cases)
  2. NEXT_PUBLIC_SUPABASE_ANON_KEY is set
  3. SUPABASE_SERVICE_ROLE_KEY is set (this MUST be a server-side only variable, never exposed to client)

Common issue: SUPABASE_SERVICE_ROLE_KEY is set but the Next.js server component can't read it because Vercel's edge runtime doesn't have access to server-side env vars the same way.

  • Step 3: Verify Supabase URL format

The Supabase URL must be the project URL (e.g., https://xxxxx.supabase.co), not the internal postgres URL. Check that the production env var matches the development value.


Task 4: Check Middleware Auth Resolution

Files:

  • Modify: src/middleware.ts:23-54

  • Step 1: Instrument the middleware auth resolution path

Add logging to each branch of the auth resolution:

if (isDevMode) {
  console.log("[middleware] Using dev mode, DEV_UID:", DEV_UID);
  authUid = DEV_UID;
} else if (rcAuthUid === DEV_FORCE_UID) {
  console.log("[middleware] Using dev force UID");
  authUid = DEV_FORCE_UID;
} else if (rcAuthUid) {
  console.log("[middleware] Using rc_auth_uid:", rcAuthUid);
  authUid = rcAuthUid;
} else if (rcAccessToken) {
  console.log("[middleware] Attempting rc_access_token validation");
  // JWT decode and validate...
}
  • Step 2: Check the trust proxy setting

If the application runs behind a Vercel proxy or CDN, the x-forwarded-proto header may need to be trusted. In Next.js, add to next.config.ts:

async headers() {
  return [{
    source: '/(.*)',
    headers: [{ key: 'X-Forwarded-Proto', value: 'https' }],
  }];
},

Or in middleware.ts, check request.headers.get('x-forwarded-proto').


Task 5: Check Supabase Auth Configuration

Files:

  • Review: Supabase Dashboard → Authentication → Settings

  • Review: src/lib/supabase.ts

  • Step 1: Verify Supabase site URL in dashboard

In Supabase Dashboard → Authentication → Settings → Site URL, verify the production URL matches:

  • Dev: http://localhost:3000
  • Prod: https://your-production-domain.com

If the Site URL doesn't match, Supabase may reject the auth redirect.

  • Step 2: Check redirect URLs in Supabase

Supabase → Authentication → URL Configuration → Redirect URLs should include:

  • https://your-production-domain.com/login
  • https://your-production-domain.com/admin/*

Any redirect URL mismatch causes silent failures.

  • Step 3: Check Supabase anon key permissions

The anon key is used client-side but also in server components. Verify the anon key hasn't been rotated or the project hasn't been migrated to a new Supabase instance.


Task 6: Implement Fixes Based on Diagnosis

Files:

  • Modify: src/app/api/login/route.ts

  • Modify: src/middleware.ts

  • Step 1: If cookie secure flag is the issue

Update cookie options to explicitly handle all cases:

const isProd = process.env.NODE_ENV === "production";
const cookieOpts = {
  path: "/",
  maxAge: 60 * 60 * 24 * 30,
  httpOnly: true,
  sameSite: isProd ? "none" : "lax",  // none requires secure
  secure: isProd,  // true in production (HTTPS), false in dev
};
  • Step 2: If env vars are missing in Vercel edge runtime

Move critical env var reading to a function that handles missing vars gracefully:

function getRequiredEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    console.error(`[login] Missing required env var: ${key}`);
    throw new Error(`Missing required environment variable: ${key}`);
  }
  return value;
}
  • Step 3: If middleware cookie reading is failing

The middleware runs on the Vercel Edge Runtime which has limited cookie access. Ensure rc_auth_uid and rc_auth_token cookies are set with appropriate domain and path:

cookieStore.set("rc_auth_uid", uid, {
  httpOnly: true,
  secure: isProd,
  sameSite: "lax",
  path: "/",
  // No domain specified — uses current hostname
});

Verification Plan

After each fix:

  1. Clear all cookies in browser dev tools (Application tab → Cookies → Delete all)
  2. Restart the production server or redeploy to Vercel
  3. Attempt login with a known-valid account
  4. Check browser dev tools → Network tab → /api/login request
    • Response status should be 200 or 3xx (redirect), not 400/401/500
  5. Check Application tab → Cookies for rc_access_token and rc_uid
  6. Check console logs in both the browser and server for errors

Execution OPTIONS

Option A (Recommended): Use superpowers:subagent-driven-development — dispatch a subagent to work through each diagnostic task with two-stage review.

Option B: Use superpowers:systematic-debugging first to pinpoint the exact failure point, then implement targeted fixes.

Which approach?