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

# Building a Todo App

> Learn how to build a full-stack todo application with Supabase

In this tutorial, you'll build a complete todo application with user authentication and real-time database updates using Supabase.

## What You'll Build

A todo app with the following features:

* User authentication (sign up/login)
* Create, read, update, and delete todos
* Mark todos as complete
* Row Level Security (RLS) to protect user data
* Real-time updates

## Prerequisites

Before you begin, make sure you have:

* A Supabase account ([sign up here](https://supabase.com/dashboard))
* Node.js 16+ installed
* Basic knowledge of React and JavaScript

<Steps>
  <Step title="Create a Supabase Project">
    1. Go to [supabase.com/dashboard](https://supabase.com/dashboard)
    2. Click **New Project**
    3. Fill in your project details:
       * **Name**: Todo App
       * **Database Password**: Choose a strong password
       * **Region**: Select the closest region to your users
    4. Click **Create new project**

    Wait for your database to finish setting up (this takes about 2 minutes).
  </Step>

  <Step title="Set Up the Database">
    In the SQL Editor, run the "Todo List" quickstart:

    1. Navigate to **SQL Editor** in the sidebar
    2. Scroll down and select **TODO LIST: Build a basic todo list with Row Level Security**
    3. Click **Run** to execute the SQL

    This creates a `todos` table with the following schema:

    ```sql theme={null}
    create table todos (
      id bigint generated by default as identity primary key,
      user_id uuid references auth.users not null,
      task text check (char_length(task) > 3),
      is_complete boolean default false,
      inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
    );

    alter table todos enable row level security;

    create policy "Individuals can create todos." on todos for
        insert with check ((select auth.uid()) = user_id);

    create policy "Individuals can view their own todos." on todos for
        select using ((select auth.uid()) = user_id);

    create policy "Individuals can update their own todos." on todos for
        update using ((select auth.uid()) = user_id);

    create policy "Individuals can delete their own todos." on todos for
        delete using ((select auth.uid()) = user_id);
    ```
  </Step>

  <Step title="Get Your API Keys">
    1. Go to **Project Settings** (the cog icon)
    2. Click on **API**
    3. Find your `URL` and `anon` public key
    4. Save these for the next step

    <Warning>
      The `anon` key is safe to use in a browser. It allows "anonymous access" until the user logs in. Never expose your `service_role` key in client-side code.
    </Warning>
  </Step>

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

    ```bash theme={null}
    npx create-next-app@latest my-todo-app
    cd my-todo-app
    ```

    Install Supabase dependencies:

    ```bash theme={null}
    npm install @supabase/supabase-js @supabase/auth-helpers-nextjs @supabase/auth-helpers-react @supabase/auth-ui-react @supabase/auth-ui-shared
    ```
  </Step>

  <Step title="Configure Environment Variables">
    Create a `.env.local` file in your project root:

    ```bash theme={null}
    NEXT_PUBLIC_SUPABASE_URL=your-project-url
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
    ```

    Replace `your-project-url` and `your-anon-key` with the values from Step 3.
  </Step>

  <Step title="Initialize Supabase Client">
    Create `lib/initSupabase.ts`:

    ```typescript theme={null}
    import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
    import { Database } from './schema'

    export const createClient = () => createPagesBrowserClient<Database>()
    ```
  </Step>

  <Step title="Create the Todo Component">
    Create `components/TodoList.tsx`:

    ```typescript theme={null}
    import { Database } from '@/lib/schema'
    import { Session, useSupabaseClient } from '@supabase/auth-helpers-react'
    import { useEffect, useState } from 'react'

    type Todos = Database['public']['Tables']['todos']['Row']

    export default function TodoList({ session }: { session: Session }) {
      const supabase = useSupabaseClient<Database>()
      const [todos, setTodos] = useState<Todos[]>([])
      const [newTaskText, setNewTaskText] = useState('')
      const [errorText, setErrorText] = useState('')

      const user = session.user

      useEffect(() => {
        const fetchTodos = async () => {
          const { data: todos, error } = await supabase
            .from('todos')
            .select('*')
            .order('id', { ascending: true })

          if (error) console.log('error', error)
          else setTodos(todos)
        }

        fetchTodos()
      }, [supabase])

      const addTodo = async (taskText: string) => {
        let task = taskText.trim()
        if (task.length) {
          const { data: todo, error } = await supabase
            .from('todos')
            .insert({ task, user_id: user.id })
            .select()
            .single()

          if (error) {
            setErrorText(error.message)
          } else {
            setTodos([...todos, todo])
            setNewTaskText('')
          }
        }
      }

      const deleteTodo = async (id: number) => {
        try {
          await supabase.from('todos').delete().eq('id', id).throwOnError()
          setTodos(todos.filter((x) => x.id != id))
        } catch (error) {
          console.log('error', error)
        }
      }

      const toggleComplete = async (id: number, isComplete: boolean) => {
        try {
          const { data } = await supabase
            .from('todos')
            .update({ is_complete: !isComplete })
            .eq('id', id)
            .throwOnError()
            .select()
            .single()

          if (data) {
            setTodos(todos.map(todo => todo.id === id ? data : todo))
          }
        } catch (error) {
          console.log('error', error)
        }
      }

      return (
        <div className="w-full">
          <h1 className="mb-12">Todo List</h1>
          <form
            onSubmit={(e) => {
              e.preventDefault()
              addTodo(newTaskText)
            }}
            className="flex gap-2 my-2"
          >
            <input
              className="rounded w-full p-2"
              type="text"
              placeholder="Add a new task"
              value={newTaskText}
              onChange={(e) => {
                setErrorText('')
                setNewTaskText(e.target.value)
              }}
            />
            <button className="btn-black" type="submit">
              Add
            </button>
          </form>
          {errorText && <div className="text-red-500">{errorText}</div>}
          <div className="bg-white shadow overflow-hidden rounded-md">
            <ul>
              {todos.map((todo) => (
                <li key={todo.id} className="flex items-center p-4 border-b">
                  <input
                    type="checkbox"
                    checked={todo.is_complete || false}
                    onChange={() => toggleComplete(todo.id, todo.is_complete || false)}
                    className="mr-3"
                  />
                  <span className={todo.is_complete ? 'line-through' : ''}>
                    {todo.task}
                  </span>
                  <button
                    onClick={() => deleteTodo(todo.id)}
                    className="ml-auto text-red-500"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          </div>
        </div>
      )
    }
    ```
  </Step>

  <Step title="Create the Main Page">
    Update `pages/index.tsx`:

    ```typescript theme={null}
    import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
    import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
    import TodoList from '@/components/TodoList'

    export default function Home() {
      const session = useSession()
      const supabase = useSupabaseClient()

      return (
        <div className="w-full h-full bg-gray-100">
          {!session ? (
            <div className="min-w-full min-h-screen flex items-center justify-center">
              <div className="w-full max-w-md p-8 bg-white rounded-lg shadow">
                <h1 className="text-2xl font-bold mb-4">Todo App</h1>
                <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />
              </div>
            </div>
          ) : (
            <div className="max-w-2xl mx-auto p-8">
              <TodoList session={session} />
              <button
                className="mt-8 px-4 py-2 bg-red-500 text-white rounded"
                onClick={() => supabase.auth.signOut()}
              >
                Sign Out
              </button>
            </div>
          )}
        </div>
      )
    }
    ```
  </Step>

  <Step title="Run Your App">
    Start the development server:

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

    Open [http://localhost:3000](http://localhost:3000) in your browser.
  </Step>
</Steps>

## Understanding Row Level Security

Row Level Security (RLS) ensures users can only access their own todos. The policies we created automatically filter queries based on the authenticated user's ID.

When a user is logged in:

* `auth.uid()` returns their user ID
* Policies check if `auth.uid() = user_id`
* Only matching rows are returned

## Next Steps

<CardGroup cols={2}>
  <Card title="Add Real-time Updates" icon="bolt" href="/realtime/overview">
    Subscribe to database changes for instant updates
  </Card>

  <Card title="User Management" icon="users" href="/guides/user-management">
    Build user profiles and avatar uploads
  </Card>

  <Card title="Deploy to Vercel" icon="rocket" href="/platform/deployment">
    Deploy your app to production
  </Card>

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

## Troubleshooting

<AccordionGroup>
  <Accordion title="Todos not showing up">
    Check that:

    * You're logged in
    * RLS policies are enabled
    * The `user_id` matches `auth.uid()`
  </Accordion>

  <Accordion title="Authentication errors">
    Verify:

    * Environment variables are set correctly
    * Email confirmation is disabled in Auth settings (for testing)
    * Check the browser console for detailed errors
  </Accordion>

  <Accordion title="CORS errors">
    Make sure your Supabase URL is correct and matches the project URL exactly.
  </Accordion>
</AccordionGroup>
