> ## 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.

# User Management Tutorial

> Build a complete user management system with profiles and avatar uploads

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

<Steps>
  <Step title="Create a New Project">
    1. Go to [supabase.com/dashboard](https://supabase.com/dashboard)
    2. Click **New Project**
    3. Configure your project:
       * **Name**: User Management App
       * **Database Password**: Use a strong password
       * **Region**: Choose your preferred region
    4. Click **Create new project**
  </Step>

  <Step title="Run the User Management Quickstart">
    In the SQL Editor:

    1. Navigate to **SQL Editor**
    2. Find **User Management Starter: Sets up a public Profiles table**
    3. Click **Run**

    This creates the necessary database schema:

    ```sql theme={null}
    -- 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]);
    ```
  </Step>

  <Step title="Set Up Your Next.js App">
    Create a new Next.js application:

    ```bash theme={null}
    npx create-next-app@latest user-management
    cd user-management
    ```

    Install Supabase packages:

    ```bash theme={null}
    npm install @supabase/supabase-js @supabase/ssr
    ```

    Create `.env.local`:

    ```bash theme={null}
    NEXT_PUBLIC_SUPABASE_URL=your-project-url
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
    ```
  </Step>

  <Step title="Create Supabase Client Utilities">
    Create `lib/supabase/client.ts` for client-side operations:

    ```typescript theme={null}
    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:

    ```typescript theme={null}
    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
              }
            },
          },
        }
      )
    }
    ```
  </Step>

  <Step title="Build the Avatar Component">
    Create `app/account/avatar.tsx`:

    ```typescript theme={null}
    '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>
      )
    }
    ```
  </Step>

  <Step title="Build the Account Form">
    Create `app/account/account-form.tsx`:

    ```typescript theme={null}
    '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>
      )
    }
    ```
  </Step>

  <Step title="Create the Account Page">
    Create `app/account/page.tsx`:

    ```typescript theme={null}
    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} />
    }
    ```
  </Step>

  <Step title="Create the Login Page">
    Create `app/login/page.tsx`:

    ```typescript theme={null}
    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`:

    ```typescript theme={null}
    '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')
    }
    ```
  </Step>

  <Step title="Test Your Application">
    Run the development server:

    ```bash theme={null}
    npm run dev
    ```

    Visit [http://localhost:3000/login](http://localhost:3000/login):

    1. Create a new account
    2. Fill in your profile information
    3. Upload an avatar image
    4. Update your profile
  </Step>
</Steps>

## 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

<CardGroup cols={2}>
  <Card title="Add Social Auth" icon="share-nodes" href="/auth/oauth">
    Enable Google, GitHub, and other OAuth providers
  </Card>

  <Card title="File Upload Tutorial" icon="upload" href="/guides/file-uploads">
    Learn advanced storage techniques
  </Card>

  <Card title="Realtime Presence" icon="users" href="/realtime/presence">
    Show online users in real-time
  </Card>

  <Card title="Example Code" icon="code" href="/examples/auth">
    Explore more authentication examples
  </Card>
</CardGroup>
