Parent Epic
Part of #124 — Landing Page Polish, CTA/Form/Event Tracked Flows & Backend Integration Gaps
Blocks: #127 (CTA wiring is meaningless if OAuth is broken), #119 (Auth journey QA)
🐛 Bug Reference
This is the implementation fix for the issue reported in Bug #86.
🎯 Objective
The GitHub OAuth callback at app/auth/callback/route.ts is producing a redirect loop or 500 error after the @supabase/ssr migration in PR #106. This is the single most critical blocking bug in the entire project — the primary landing page CTA (/auth/login) leads directly into this broken flow.
🔍 Root Cause Diagnosis
The @supabase/ssr package handles cookies differently from @supabase/auth-helpers-nextjs. Common failure modes after migration:
| Symptom |
Likely Root Cause |
500 on /auth/callback |
createServerClient not receiving correct cookies() adapter from next/headers |
Redirect loop (/auth/login → /auth/callback → /auth/login) |
Session not persisted — set-cookie header not written because cookies().set() is being called outside a Server Action or Route Handler boundary |
AuthSessionMissingError on dashboard after login |
getUser() called before the session cookie is set in the response |
| Callback succeeds but user not onboarded correctly |
is_onboarded check in middleware running before Supabase session is fully established |
🛠️ Tasks
1. Audit app/auth/callback/route.ts
// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const cookieStore = cookies()
const response = NextResponse.redirect(new URL('/dashboard', request.url))
if (code) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get: (name) => cookieStore.get(name)?.value,
set: (name, value, options) => response.cookies.set({ name, value, ...options }),
remove: (name, options) => response.cookies.set({ name, value: '', ...options }),
},
}
)
await supabase.auth.exchangeCodeForSession(code)
}
return response
}
2. Audit middleware.ts
3. Audit app/dashboard/layout.tsx
4. Local reproduction
✅ Acceptance Criteria
Parent Epic
Part of #124 — Landing Page Polish, CTA/Form/Event Tracked Flows & Backend Integration Gaps
Blocks: #127 (CTA wiring is meaningless if OAuth is broken), #119 (Auth journey QA)
🐛 Bug Reference
This is the implementation fix for the issue reported in Bug #86.
🎯 Objective
The GitHub OAuth callback at
app/auth/callback/route.tsis producing a redirect loop or 500 error after the@supabase/ssrmigration in PR #106. This is the single most critical blocking bug in the entire project — the primary landing page CTA (/auth/login) leads directly into this broken flow.🔍 Root Cause Diagnosis
The
@supabase/ssrpackage handles cookies differently from@supabase/auth-helpers-nextjs. Common failure modes after migration:/auth/callbackcreateServerClientnot receiving correctcookies()adapter fromnext/headers/auth/login→/auth/callback→/auth/login)set-cookieheader not written becausecookies().set()is being called outside a Server Action or Route Handler boundaryAuthSessionMissingErroron dashboard after logingetUser()called before the session cookie is set in the responseis_onboardedcheck in middleware running before Supabase session is fully established🛠️ Tasks
1. Audit
app/auth/callback/route.tscreateServerClientis usingcookies()fromnext/headerswith bothgetandsetmethodsawait supabase.auth.exchangeCodeForSession(code)is called before any redirectNextResponsebeing returned carries the updatedSet-Cookieheader from the Supabase client2. Audit
middleware.tscreateServerClientin middleware also uses the cookie adapter pattern (not the oldcreateMiddlewareClient)supabase.auth.getUser()(notgetSession()) —getSession()does not validate the JWT server-sideis_onboardedredirect logic: verify it reads from the Supabaseuserstable, not from session metadata that may not be set during the callback3. Audit
app/dashboard/layout.tsxgetUser()notgetSession()nulluser goes to/auth/login?redirect=/dashboardso the post-login redirect works4. Local reproduction
/onboardor/dashboardwith no errors✅ Acceptance Criteria
/onboardafter first loginis_onboarded = true): redirected to/dashboardAuthSessionMissingErroror unhandled exceptions/auth/callbackreturns a 302, not a 500