I’m running into an issue where posting to my Supabase projects table works fine locally but fails in production on Vercel with an “Authentication required” error. From what I can tell, the API route isn’t reading the Supabase session cookies after deploy, even though they’re set correctly when I’m logged in. How do I properly configure my Next.js API routes on Vercel so Supabase Auth cookies are passed through and the user session is available? I have litterally worked on fixing this by troubleshooting through vercel, chatgpt, claude and absolutely nothing works. Basically I am able to post project on my platform which is a project posting platform before i deploy but the second i deploy it, it say Authentication error. Is anyone else running into this issue if so I would love some help thanks!
Below is my supabase and middlewear code if you could help that would be great:
supabase.ts
import { createClient } from “@supabase/supabase-js”
// Check if environment variables are availableconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URLconst supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {throw new Error(“Missing Supabase environment variables”)}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {auth: {autoRefreshToken: true,persistSession: true,detectSessionInUrl: true,},})
// Database typesexport interface User {id: stringemail: stringrole: “flipper” | “contractor”created_at: stringupdated_at: string}
export interface Profile {id: stringuser_id: stringfirst_name: stringlast_name: stringphone?: stringcompany_name?: stringbusiness_type?: stringyears_experience?: numberlicense_number?: stringinsurance_provider?: stringservice_radius?: numberteam_size?: stringhourly_rate_min?: numberhourly_rate_max?: numberaverage_budget_min?: numberaverage_budget_max?: numberprojects_per_year?: stringworking_hours?: stringcommunication_preference?: stringproject_notifications: booleanmarketing_emails: booleanprofile_photo_url?: stringbusiness_license_url?: stringinsurance_url?: stringbio?: stringverified: booleanrating: numberreviews_count: numbercompleted_jobs: numberresponse_time_hours: numberavailability: stringcreated_at: stringupdated_at: string}
export interface Project {id: stringuser_id: stringtitle: stringdescription: stringdetailed_description?: stringlocation: stringbudget_min?: numberbudget_max?: numbertimeline_weeks?: numberurgency: “low” | “medium” | “high”status: “open” | “in_progress” | “completed” | “cancelled”featured: booleanbids_count: numbercreated_at: stringupdated_at: string}
export interface Trade {id: stringname: stringcreated_at: string}
export interface Language {id: stringname: stringcreated_at: string}
export interface Bid {id: stringproject_id: stringcontractor_id: stringamount: numbermessage?: stringstatus: “pending” | “accepted” | “rejected”created_at: stringupdated_at: string}
// Client-side Supabase instance getter (singleton pattern)export const getClientSupabase = () => {return supabase}
// Re-export createClient for convenienceexport { createClient } from “@supabase/supabase-js”supabase server.ts:
import { createClient } from “@supabase/supabase-js”import { cookies } from “next/headers”
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
if (!supabaseUrl || !supabaseServiceKey) {throw new Error(“Missing Supabase server environment variables”)}
// Server-side Supabase client with service role keyexport async function createServerSupabaseClient() {const cookieStore = await cookies()
return createClient(supabaseUrl, supabaseServiceKey, {auth: {autoRefreshToken: false,persistSession: false,},global: {headers: {Authorization: `Bearer ${supabaseServiceKey}`,},},})}
// Alternative client for user authenticationexport async function createServerSupabaseClientForAuth() {const cookieStore = await cookies()const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
return createClient(supabaseUrl, supabaseAnonKey, {auth: {autoRefreshToken: false,persistSession: false,},global: {headers: {Cookie: cookieStore.toString(),},},})}supabase auth.ts:
import { supabase } from “./supabase”
export interface AuthUser {id: stringemail: stringrole: “flipper” | “contractor”profile: {firstName: stringlastName: stringcompanyName?: stringphone?: string}}
export async function signUpWithEmail(userData: {email: stringpassword: stringrole: “flipper” | “contractor”firstName: stringlastName: stringcompanyName?: stringphone?: string}) {try {console.log(“Creating new user account:”, userData.email, “as”, userData.role)// Validate input dataif (!userData.email || !userData.password || !userData.role || !userData.firstName) { return { success: false, error: "Please fill in all required fields." }}
if (userData.password.length < 8) { return { success: false, error: "Password must be at least 8 characters long." }}
if (!userData.email.includes("@")) { return { success: false, error: "Please enter a valid email address." }}
// First, sign up the userconst { data, error } = await supabase.auth.signUp({ email: userData.email.trim().toLowerCase(), password: userData.password, options: { data: { role: userData.role, first_name: userData.firstName.trim(), last_name: userData.lastName.trim(), company_name: userData.companyName?.trim() || "", phone: userData.phone?.trim() || "", }, },})
if (error) { console.error("Supabase signup error:", error)
// Provide user-friendly error messages if (error.message.includes("already registered") || error.message.includes("already been registered")) { return { success: false, error: "An account with this email already exists. Please sign in instead." } } if (error.message.includes("Password should be")) { return { success: false, error: "Password must be at least 8 characters long." } } if (error.message.includes("Invalid email")) { return { success: false, error: "Please enter a valid email address." } } if (error.message.includes("signup is disabled")) { return { success: false, error: "Account creation is currently disabled. Please contact support." } }
return { success: false, error: `Account creation failed: ${error.message}` }}
if (data.user) { console.log("User account created successfully:", data.user.id)
// Check if we have a session (user is automatically signed in) if (data.session) { console.log("User automatically signed in after signup") const authUser: AuthUser = { id: data.user.id, email: data.user.email!, role: userData.role, profile: { firstName: userData.firstName, lastName: userData.lastName, companyName: userData.companyName, phone: userData.phone, }, }
return { success: true, user: authUser } }
// If no session but user exists, try to sign them in immediately if (!data.session && data.user.email_confirmed_at) { console.log("User created but not signed in, attempting automatic sign in...")
const signInResult = await supabase.auth.signInWithPassword({ email: userData.email.trim().toLowerCase(), password: userData.password, })
if (signInResult.data.session && signInResult.data.user) { console.log("User successfully signed in after signup") const authUser: AuthUser = { id: signInResult.data.user.id, email: signInResult.data.user.email!, role: userData.role, profile: { firstName: userData.firstName, lastName: userData.lastName, companyName: userData.companyName, phone: userData.phone, }, }
return { success: true, user: authUser } } }
// If email confirmation is required if (!data.user.email_confirmed_at) { console.log("Email confirmation required") return { success: true, user: null, message: "Please check your email and click the confirmation link to activate your account.", requiresConfirmation: true, } }
return { success: false, error: "Account created but sign-in failed. Please try signing in manually." }}
return { success: false, error: "Account creation failed. Please try again." }
} catch (error: any) {console.error(“Signup error:”, error)return { success: false, error: `An unexpected error occurred: ${error.message || "Please try again."}` }}}
export async function signInWithEmail(email: string, password: string) {try {console.log(“Signing in user:”, email)
if (!email || !password) { return { success: false, error: "Please enter both email and password." }}
const { data, error } = await supabase.auth.signInWithPassword({ email: email.trim().toLowerCase(), password: password,})
if (error) { console.error("Supabase signin error:", error)
// Provide user-friendly error messages if (error.message.includes("Invalid login credentials")) { return { success: false, error: "Invalid email or password. Please check your credentials." } } if (error.message.includes("Email not confirmed")) { return { success: false, error: "Please check your email and confirm your account before signing in." } } if (error.message.includes("Too many requests")) { return { success: false, error: "Too many login attempts. Please wait a moment and try again." } }
return { success: false, error: `Sign in failed: ${error.message}` }}
if (data.user && data.session) { console.log("User signed in successfully:", data.user.id)
// Get user data from metadata first const role = data.user.user_metadata?.role || "flipper" const firstName = data.user.user_metadata?.first_name || "User" const lastName = data.user.user_metadata?.last_name || "" const companyName = data.user.user_metadata?.company_name const phone = data.user.user_metadata?.phone
const authUser: AuthUser = { id: data.user.id, email: data.user.email!, role: role as "flipper" | "contractor", profile: { firstName, lastName, companyName, phone, }, }
return { success: true, user: authUser }}
return { success: false, error: "Sign in failed. Please try again." }} catch (error: any) {console.error(“Signin error:”, error)return { success: false, error: `An unexpected error occurred: ${error.message || "Please try again."}` }}}
export async function signOut() {try {console.log(“Signing out user”)const { error } = await supabase.auth.signOut()if (error) {throw error}console.log(“User signed out successfully”)} catch (error: any) {console.error(“Sign out error:”, error)throw error}}
export function onAuthStateChange(callback: (user: AuthUser | null) => void) {console.log(“Setting up auth state listener”)
return supabase.auth.onAuthStateChange(async (event, session) => {console.log(“Auth state changed:”, event, session ? “with session” : “no session”)
if (session?.user) { try { const role = session.user.user_metadata?.role || "flipper" const firstName = session.user.user_metadata?.first_name || "User" const lastName = session.user.user_metadata?.last_name || "" const companyName = session.user.user_metadata?.company_name const phone = session.user.user_metadata?.phone
const authUser: AuthUser = { id: session.user.id, email: session.user.email!, role: role as "flipper" | "contractor", profile: { firstName, lastName, companyName, phone, }, }
console.log("Auth user created:", authUser) callback(authUser) } catch (error) { console.error("Error processing auth state change:", error) callback(null) }} else { console.log("No session, setting user to null") callback(null)}}).data.subscription.unsubscribe}
export async function getCurrentUser(): Promise<AuthUser | null> {try {const {data: { session },} = await supabase.auth.getSession()
if (!session?.user) { return null}
const role = session.user.user_metadata?.role || "flipper"const firstName = session.user.user_metadata?.first_name || "User"const lastName = session.user.user_metadata?.last_name || ""const companyName = session.user.user_metadata?.company_nameconst phone = session.user.user_metadata?.phone
return { id: session.user.id, email: session.user.email!, role: role as "flipper" | "contractor", profile: { firstName, lastName, companyName, phone, },}
} catch (error) {console.error(“Error getting current user:”, error)return null}}middlewear.ts:
import { NextResponse } from “next/server”import type { NextRequest } from “next/server”
const ALLOWLIST = (process.env.ALLOWED_ORIGINS || “”).split(“,”).map(s => s.trim()).filter(Boolean)
export function middleware(request: NextRequest) {const response = NextResponse.next()
// Security headers (fine to keep)response.headers.set(“X-Frame-Options”, “DENY”)response.headers.set(“X-Content-Type-Options”, “nosniff”)response.headers.set(“Referrer-Policy”, “origin-when-cross-origin”)response.headers.set(“X-XSS-Protection”, “1; mode=block”)
if (request.nextUrl.pathname.startsWith(“/api/”)) {const origin = request.headers.get(“origin”) || “”const allowOrigin =ALLOWLIST.length === 0 ? “” :ALLOWLIST.includes(origin) ? origin : “”
if (allowOrigin) { response.headers.set("Vary", "Origin") response.headers.set("Access-Control-Allow-Origin", allowOrigin) response.headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization") response.headers.set("Access-Control-Allow-Credentials", "true")}
// Handle preflightif (request.method === "OPTIONS") { const preflight = new NextResponse(null, { status: 204 }) if (allowOrigin) { preflight.headers.set("Vary", "Origin") preflight.headers.set("Access-Control-Allow-Origin", allowOrigin) preflight.headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") preflight.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization") preflight.headers.set("Access-Control-Allow-Credentials", "true") } return preflight}}
return response}
export const config = {matcher: \[“/api/:path\*”, “/dashboard/:path\*”, “/admin/:path\*”\],}