Magic link works when sent from Supabase dashboard, but not when sent from Next.js. The return url includes code in the searchParams instead of the intended token.
Code
// middleware.ts
import { NextRequest } from 'next/server'
import { updateSession } from './supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
// supabase/middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: CookieOptions) {
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value: '',
...options,
})
},
},
},
)
await supabase.auth.getUser()
return response
}
// supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
)
}
// lib/supabase.ts
'use server'
import { createClient } from '@/supabase/server'
export async function signIn(data: FormData) {
const supabase = createClient()
const email = data.get('Username') as string
if (!email) throw new Error('No email provided.')
try {
await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
emailRedirectTo: `${process.env.NEXT_PUBLIC_BASE_URL}/account`,
},
})
} catch (error) {
throw new Error(`${error}`)
}
}
// app/log-in/page.tsx
import { signIn } from '@/lib/supabase'
export default function LogIn() {
return (
<main>
<section>
<form action={signIn}>
<input type='email' name='Username' />
<button type='submit'>Log In</button>
</form>
</section>
</main>
)
}
Magic Link Email Template
<a href="{{ .ConfirmationURL }}">Log In</a>
I've followed every tutorial and documentation on this, but no one ever mentions anything about {url}?code={code}. I don't even get that when I use the "Send Magic Link" button in the Supabase dashboard.