Overview
Accept FormData, upload a new file to Supabase Storage when provided, keep or clear the existing avatar accordingly, and update profiles.
import { createAdminClient } from '@/config/supabase/adminClient'
import { DatabaseTable } from '@/constants/enums'
import { getCurrentUser } from '@/lib/auth'
import { uploadAvatar } from '@/lib/storage'
import type { ActionResponse } from '@/types'
export async function updateUserProfileAction(formData: FormData): Promise<ActionResponse> {
const supabase = createAdminClient()
const user = await getCurrentUser()
if (!user) return { success: false, error: 'Not authenticated' }
const name = String(formData.get('name') ?? '')
const file = formData.get('avatarFile') as File | null
const existing = String(formData.get('existingAvatar') ?? '')
const removed = existing === ''
let avatarUrl: string | null = null
if (file) avatarUrl = await uploadAvatar(file, user.id)
else if (!removed) avatarUrl = existing || null
const update: Record<string, string | null> = { full_name: name }
if (file || removed) update.avatar_url = avatarUrl
const { error } = await supabase
.from(DatabaseTable.Profiles)
.update(update)
.eq('id', user.id)
if (error) return { success: false, error: 'Failed to update profile' }
return { success: true }
}Key conventions
- Three states for the avatar field: new file uploaded, existing URL kept, or explicitly removed (empty string sent from the form).
- Upload happens in
uploadAvatar(file, userId)which returns the public URL from theavatarsSupabase Storage bucket. - Only update
avatar_urlin the database when the avatar actually changed — avoids unnecessary writes when onlyfull_namechanged. - Use the admin client for the
profilesupdate so RLS doesn't block server-side writes.