Documentation Index Fetch the complete documentation index at: https://mintlify.com/supabase/supabase/llms.txt
Use this file to discover all available pages before exploring further.
Learn how to build a user management system that allows users to sign up, sign in, and manage their profile information including avatar images.
What You’ll Build
A user management system with:
Email/password authentication
User profile management
Avatar image uploads
Public profile pages
Row Level Security for data protection
Architecture Overview
This application uses:
Supabase Auth for user authentication
Supabase Database for storing profile data
Supabase Storage for avatar images
Next.js App Router for the frontend
Create a New Project
Go to supabase.com/dashboard
Click New Project
Configure your project:
Name : User Management App
Database Password : Use a strong password
Region : Choose your preferred region
Click Create new project
Run the User Management Quickstart
In the SQL Editor:
Navigate to SQL Editor
Find User Management Starter: Sets up a public Profiles table
Click Run
This creates the necessary database schema: -- Create a table for public profiles
create table profiles (
id uuid references auth . users on delete cascade not null primary key ,
updated_at timestamp with time zone ,
username text unique ,
full_name text ,
avatar_url text ,
website text ,
constraint username_length check (char_length(username) >= 3 )
);
-- Set up Row Level Security (RLS)
alter table profiles enable row level security ;
create policy "Public profiles are viewable by everyone."
on profiles for select
using (true);
create policy "Users can insert their own profile."
on profiles for insert
with check (( select auth . uid ()) = id);
create policy "Users can update own profile."
on profiles for update
using (( select auth . uid ()) = id);
-- Auto-create profile on user signup
create function public .handle_new_user()
returns trigger as $$
begin
insert into public . profiles (id, full_name, avatar_url)
values ( new . id , new . raw_user_meta_data ->> 'full_name' , new . raw_user_meta_data ->> 'avatar_url' );
return new;
end ;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth . users
for each row execute procedure public . handle_new_user ();
-- Set up Storage
insert into storage . buckets (id, name )
values ( 'avatars' , 'avatars' );
-- Storage policies
create policy "Avatar images are publicly accessible."
on storage . objects for select
using (bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage . objects for insert
with check (bucket_id = 'avatars' );
create policy "Anyone can update their own avatar."
on storage . objects for update
using ( auth . uid ():: text = ( storage . foldername ( name ))[1]);
Set Up Your Next.js App
Create a new Next.js application: npx create-next-app@latest user-management
cd user-management
Install Supabase packages: npm install @supabase/supabase-js @supabase/ssr
Create .env.local: NEXT_PUBLIC_SUPABASE_URL = your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY = your-anon-key
Create Supabase Client Utilities
Create lib/supabase/client.ts for client-side operations: 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 for server-side operations: 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 . set ( name , value , options )
)
} catch {
// Server Component - ignore
}
},
},
}
)
}
Build the Avatar Component
Create app/account/avatar.tsx: 'use client'
import React , { useEffect , useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import Image from 'next/image'
export default function Avatar ({
uid ,
url ,
size ,
onUpload ,
} : {
uid : string | null
url : string | null
size : number
onUpload : ( url : string ) => void
}) {
const supabase = createClient ()
const [ avatarUrl , setAvatarUrl ] = useState < string | null >( url )
const [ uploading , setUploading ] = useState ( false )
useEffect (() => {
async function downloadImage ( path : string ) {
try {
const { data , error } = await supabase . storage
. from ( 'avatars' )
. download ( path )
if ( error ) throw error
const url = URL . createObjectURL ( data )
setAvatarUrl ( url )
} catch ( error ) {
console . log ( 'Error downloading image: ' , error )
}
}
if ( url ) downloadImage ( url )
}, [ url , supabase ])
const uploadAvatar : React . ChangeEventHandler < HTMLInputElement > = async ( event ) => {
try {
setUploading ( true )
if ( ! event . target . files || event . target . files . length === 0 ) {
throw new Error ( 'You must select an image to upload.' )
}
const file = event . target . files [ 0 ]
const fileExt = file . name . split ( '.' ). pop ()
const filePath = ` ${ uid } - ${ Math . random () } . ${ fileExt } `
const { error : uploadError } = await supabase . storage
. from ( 'avatars' )
. upload ( filePath , file )
if ( uploadError ) throw uploadError
onUpload ( filePath )
} catch ( error ) {
alert ( 'Error uploading avatar!' )
} finally {
setUploading ( false )
}
}
return (
< div className = "flex flex-col items-center gap-4" >
{ avatarUrl ? (
< Image
width = { size }
height = { size }
src = { avatarUrl }
alt = "Avatar"
className = "rounded-full"
/>
) : (
< div
className = "rounded-full bg-gray-200"
style = {{ height : size , width : size }}
/>
)}
< div >
< label
className = "px-4 py-2 bg-blue-500 text-white rounded cursor-pointer"
htmlFor = "single"
>
{ uploading ? 'Uploading ...' : 'Upload Avatar' }
</ label >
< input
className = "hidden"
type = "file"
id = "single"
accept = "image/*"
onChange = { uploadAvatar }
disabled = { uploading }
/>
</ div >
</ div >
)
}
Build the Account Form
Create app/account/account-form.tsx: 'use client'
import { useCallback , useEffect , useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { type User } from '@supabase/supabase-js'
import Avatar from './avatar'
export default function AccountForm ({ user } : { user : User | null }) {
const supabase = createClient ()
const [ loading , setLoading ] = useState ( true )
const [ fullname , setFullname ] = useState < string | null >( null )
const [ username , setUsername ] = useState < string | null >( null )
const [ website , setWebsite ] = useState < string | null >( null )
const [ avatar_url , setAvatarUrl ] = useState < string | null >( null )
const getProfile = useCallback ( async () => {
try {
setLoading ( true )
const { data , error , status } = await supabase
. from ( 'profiles' )
. select ( `full_name, username, website, avatar_url` )
. eq ( 'id' , user ?. id )
. single ()
if ( error && status !== 406 ) throw error
if ( data ) {
setFullname ( data . full_name )
setUsername ( data . username )
setWebsite ( data . website )
setAvatarUrl ( data . avatar_url )
}
} catch ( error ) {
alert ( 'Error loading user data!' )
} finally {
setLoading ( false )
}
}, [ user , supabase ])
useEffect (() => {
getProfile ()
}, [ user , getProfile ])
async function updateProfile ({
username ,
website ,
avatar_url ,
} : {
username : string | null
fullname : string | null
website : string | null
avatar_url : string | null
}) {
try {
setLoading ( true )
const { error } = await supabase . from ( 'profiles' ). upsert ({
id: user ?. id as string ,
full_name: fullname ,
username ,
website ,
avatar_url ,
updated_at: new Date (). toISOString (),
})
if ( error ) throw error
alert ( 'Profile updated!' )
} catch ( error ) {
alert ( 'Error updating the data!' )
} finally {
setLoading ( false )
}
}
return (
< div className = "max-w-md mx-auto p-8 space-y-6" >
< Avatar
uid = {user?.id ?? null }
url = { avatar_url }
size = { 150 }
onUpload = {(url) => {
setAvatarUrl ( url )
updateProfile ({ fullname , username , website , avatar_url: url })
}}
/>
< div >
< label htmlFor = "email" className = "block text-sm font-medium" >
Email
</ label >
< input
id = "email"
type = "text"
value = {user?. email }
disabled
className = "mt-1 block w-full rounded border p-2 bg-gray-100"
/>
</ div >
< div >
< label htmlFor = "fullName" className = "block text-sm font-medium" >
Full Name
</ label >
< input
id = "fullName"
type = "text"
value = {fullname || '' }
onChange = {(e) => setFullname (e.target.value)}
className = "mt-1 block w-full rounded border p-2"
/>
</ div >
< div >
< label htmlFor = "username" className = "block text-sm font-medium" >
Username
</ label >
< input
id = "username"
type = "text"
value = {username || '' }
onChange = {(e) => setUsername (e.target.value)}
className = "mt-1 block w-full rounded border p-2"
/>
</ div >
< div >
< label htmlFor = "website" className = "block text-sm font-medium" >
Website
</ label >
< input
id = "website"
type = "url"
value = {website || '' }
onChange = {(e) => setWebsite (e.target.value)}
className = "mt-1 block w-full rounded border p-2"
/>
</ div >
< div className = "space-y-2" >
< button
className = "w-full px-4 py-2 bg-blue-500 text-white rounded"
onClick = {() => updateProfile ({ fullname , username , website , avatar_url })}
disabled = { loading }
>
{ loading ? 'Loading ...' : 'Update Profile' }
</ button >
< form action = "/auth/signout" method = "post" >
< button className = "w-full px-4 py-2 bg-gray-200 rounded" type = "submit" >
Sign Out
</ button >
</ form >
</ div >
</ div >
)
}
Create the Account Page
Create app/account/page.tsx: import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import AccountForm from './account-form'
export default async function Account () {
const supabase = await createClient ()
const {
data : { user },
} = await supabase . auth . getUser ()
if ( ! user ) {
return redirect ( '/login' )
}
return < AccountForm user = { user } />
}
Create the Login Page
Create app/login/page.tsx: import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { login , signup } from './actions'
export default async function LoginPage () {
const supabase = await createClient ()
const { data } = await supabase . auth . getUser ()
if ( data ?. user ) {
redirect ( '/account' )
}
return (
< div className = "min-h-screen flex items-center justify-center" >
< div className = "max-w-md w-full p-8 bg-white rounded-lg shadow" >
< h1 className = "text-2xl font-bold mb-6" > Sign In </ h1 >
< form className = "space-y-4" >
< div >
< label htmlFor = "email" className = "block text-sm font-medium" >
Email
</ label >
< input
id = "email"
name = "email"
type = "email"
required
className = "mt-1 block w-full rounded border p-2"
/>
</ div >
< div >
< label htmlFor = "password" className = "block text-sm font-medium" >
Password
</ label >
< input
id = "password"
name = "password"
type = "password"
required
className = "mt-1 block w-full rounded border p-2"
/>
</ div >
< div className = "flex gap-2" >
< button
formAction = { login }
className = "flex-1 px-4 py-2 bg-blue-500 text-white rounded"
>
Sign In
</ button >
< button
formAction = { signup }
className = "flex-1 px-4 py-2 bg-gray-200 rounded"
>
Sign Up
</ button >
</ div >
</ form >
</ div >
</ div >
)
}
Create app/login/actions.ts: 'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function login ( formData : FormData ) {
const supabase = await createClient ()
const data = {
email: formData . get ( 'email' ) as string ,
password: formData . get ( 'password' ) as string ,
}
const { error } = await supabase . auth . signInWithPassword ( data )
if ( error ) {
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/account' )
}
export async function signup ( formData : FormData ) {
const supabase = await createClient ()
const data = {
email: formData . get ( 'email' ) as string ,
password: formData . get ( 'password' ) as string ,
}
const { error } = await supabase . auth . signUp ( data )
if ( error ) {
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/account' )
}
Test Your Application
Run the development server: Visit http://localhost:3000/login :
Create a new account
Fill in your profile information
Upload an avatar image
Update your profile
Key Concepts
Row Level Security
RLS policies ensure:
Anyone can view profiles (select using (true))
Users can only update their own profile
Profiles are automatically created on signup via trigger
Storage Policies
Storage policies control:
Public read access to avatars
Authenticated users can upload
Users can only update their own avatars
Server vs Client Components
Next.js App Router pattern:
Server Components : Fetch user data securely
Client Components : Handle interactivity (forms, uploads)
Next Steps
Add Social Auth Enable Google, GitHub, and other OAuth providers
File Upload Tutorial Learn advanced storage techniques
Realtime Presence Show online users in real-time
Example Code Explore more authentication examples