Rather have it done for you?
Vetted AI builders who specialize in auth rescue work. Escrow-protected, done in days — not weeks.
Your Bolt.new app has a login page that looks great. But one or more of these is broken:
useUser() or supabase.auth.getUser() returns null even after loginJWT expired or Invalid JWT errors in the consoleThese are the four most common Supabase auth failures in Bolt-generated code, and they all have the same root cause: Bolt generates client-side auth patterns that don't work when combined with Next.js server components and middleware.
Bolt generates sensible auth code for a pure client-side React app. The problem is that most Bolt projects use Next.js, and Next.js App Router requires a different approach.
Specifically, Bolt typically generates:
// ❌ What Bolt generates — works in client components, breaks everywhere else
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)This single client is used everywhere — in server components, in middleware, in API routes. It breaks because:
The fix is replacing this with @supabase/ssr — Supabase's package built specifically for server-side rendering.
@supabase/ssrnpm install @supabase/ssrRemove the old client import from wherever Bolt put it — usually a lib/supabase.ts or utils/supabase.ts file.
Create lib/supabase/client.ts:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}Create lib/supabase/server.ts:
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.
This is the step Bolt almost never generates, and it's why sessions disappear on refresh. Without middleware, Supabase's session token isn't refreshed between requests.
Create or replace middleware.ts at the root of your project:
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({
Critical: The supabaseResponse object must be returned from middleware — not a plain NextResponse.next(). The session cookie updates are written into this response object. If you create a new response object and return it instead, the session is silently lost.
Bolt often generates an incomplete callback handler. Replace or create app/auth/callback/route.ts:
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse, type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const cookieStore = await cookies()
const supabase = createServerClient(
In the Supabase dashboard → Authentication → URL Configuration:
https://yourdomain.com (your production URL)https://yourdomain.com/auth/callbackhttp://localhost:3000/auth/callback (for local dev)If you forget this step, OAuth will work locally but silently fail in production.
In client components (files with 'use client' at the top):
import { createClient } from '@/lib/supabase/client'
export function LoginButton() {
const supabase = createClient()
const handleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
}
return <button onClick={handleLogin}>Sign in with Google</button>
}In server components (no 'use client'):
import { createClient } from '@/lib/supabase/server'
export default async function Dashboard() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null // middleware should handle this, but belt-and-suspenders
return <div>Welcome, {user.email}</div>
}Never use getSession() in server components — use getUser(). getSession() returns cached data that isn't re-validated against the server, which means you can get stale session data after a user logs out.
After making these changes:
/loginsb-* cookies setMistake 1: Mixing the old createClient with the new SSR clients
If you have any file still importing from the old @supabase/supabase-js single client, sessions will break inconsistently. Search your whole project:
grep -r "from '@supabase/supabase-js'" --include="*.ts" --include="*.tsx" .Every result should be replaced with the appropriate lib/supabase/client or lib/supabase/server import.
Mistake 2: Returning a new response from middleware instead of supabaseResponse
// ❌ This loses the session cookie updates
if (!user) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next() // ← loses cookies
// ✅ Return supabaseResponse (which has the cookies) for the redirect
if (!user) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url) // redirect is fine
}
return supabaseResponse // ← returns updated cookiesMistake 3: Using getSession() instead of getUser() in server code
getSession() reads from the cookie without verifying with Supabase's servers. A manipulated cookie would pass. getUser() makes a network call to verify — it's the right one for anything security-sensitive.
Mistake 4: Forgetting to add localhost to Supabase redirect URLs
Supabase blocks redirects to any URL not in your allow-list. Add both your production URL and http://localhost:3000 during development. Without this, OAuth works nowhere or only in one environment.
Once auth is working:
public.profiles and check them in middlewareThis rescue typically takes a Smith 3–5 hours including testing across OAuth providers. If you'd rather have it done right the first time, post an Auth Rescue job on Smith.