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.
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 )
Node.js 16+ installed
Basic knowledge of React and JavaScript
Create a Supabase Project
Go to supabase.com/dashboard
Click New Project
Fill in your project details:
Name : Todo App
Database Password : Choose a strong password
Region : Select the closest region to your users
Click Create new project
Wait for your database to finish setting up (this takes about 2 minutes).
Set Up the Database
In the SQL Editor, run the “Todo List” quickstart:
Navigate to SQL Editor in the sidebar
Scroll down and select TODO LIST: Build a basic todo list with Row Level Security
Click Run to execute the SQL
This creates a todos table with the following schema: 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);
Get Your API Keys
Go to Project Settings (the cog icon)
Click on API
Find your URL and anon public key
Save these for the next step
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.
Create a Next.js App
Create a new Next.js application: npx create-next-app@latest my-todo-app
cd my-todo-app
Install Supabase dependencies: npm install @supabase/supabase-js @supabase/auth-helpers-nextjs @supabase/auth-helpers-react @supabase/auth-ui-react @supabase/auth-ui-shared
Configure Environment Variables
Create a .env.local file in your project root: 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.
Initialize Supabase Client
Create lib/initSupabase.ts: import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { Database } from './schema'
export const createClient = () => createPagesBrowserClient < Database >()
Create the Todo Component
Create components/TodoList.tsx: 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 >
)
}
Create the Main Page
Update pages/index.tsx: 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 >
)
}
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
Add Real-time Updates Subscribe to database changes for instant updates
User Management Build user profiles and avatar uploads
Deploy to Vercel Deploy your app to production
Example Code Explore more database examples
Troubleshooting
Check that:
You’re logged in
RLS policies are enabled
The user_id matches auth.uid()
Verify:
Environment variables are set correctly
Email confirmation is disabled in Auth settings (for testing)
Check the browser console for detailed errors
Make sure your Supabase URL is correct and matches the project URL exactly.