- Learn
- Guided Projects
- Build a Todo App
Beginner45 min2 prerequisites
Create a full-stack todo application with AI tools, covering CRUD operations, authentication, and deployment.
Build a Todo App
Build a complete todo application from scratch using AI tools. This project covers database design, CRUD operations, user authentication, and deployment.
Project Overview
What We're Building
Terminal
Todo App Features:
├── User authentication (signup/login)
├── Create, read, update, delete todos
├── Mark todos as complete
├── Filter by status
└── User-specific data (RLS)
Tech Stack
- Frontend: Next.js + Tailwind + shadcn/ui
- Backend: Supabase (database + auth)
- Deployment: Vercel
Tools You Can Use
| Task | Recommended Tool |
|---|---|
| Generate full app | Lovable, v0, Bolt |
| Multi-file changes | Claude Code, Cursor |
| Quick edits | GitHub Copilot, Windsurf |
| Database setup | Supabase Dashboard + AI SQL |
| Background tasks | GitHub Copilot Coding Agent |
| Deploy | Vercel |
Phase 1: Project Setup
Option A: AI Builder (Fastest)
Use Lovable or Bolt:
Terminal
Prompt:
"Create a todo app with Next.js, Tailwind, and shadcn/ui.
Include:
- Login/signup pages
- Todo list with add, edit, delete
- Mark complete functionality
- Filter by all/active/completed
Use a clean, minimal design."
Option B: Manual + AI Editor
Terminal
# Create Next.js project
npx create-next-app@latest todo-app --typescript --tailwind --eslint
cd todo-app
# Add shadcn/ui
npx shadcn@latest init
# Add components
npx shadcn@latest add button input checkbox card
Phase 2: Database Setup
Create Supabase Project
- Go to supabase.com
- Create new project
- Note URL and anon key
Create Todos Table
Run in SQL Editor:
Terminal
-- Create todos table
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Users can only see their own todos
CREATE POLICY "Users can view own todos"
ON todos FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- Users can create own todos
CREATE POLICY "Users can create own todos"
ON todos FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Users can update own todos
CREATE POLICY "Users can update own todos"
ON todos FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Users can delete own todos
CREATE POLICY "Users can delete own todos"
ON todos FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
Connect to App
Create environment variables:
Terminal
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
Phase 3: Authentication
AI Prompt for Auth
Terminal
"Add Supabase authentication to this Next.js app.
Create:
- Login page at /login
- Signup page at /signup
- Auth callback handler
- Protected dashboard route
- Sign out functionality
Use the Supabase client pattern for Next.js App Router."
Expected Files
Terminal
app/
├── login/
│ └── page.tsx # Login form
├── signup/
│ └── page.tsx # Signup form
├── auth/
│ └── callback/
│ └── route.ts # OAuth callback
├── dashboard/
│ └── page.tsx # Protected page
lib/
└── supabase/
├── client.ts # Browser client
└── server.ts # Server client
Verify Auth Works
- Create account at
/signup - Login at
/login - Verify redirect to dashboard
- Check Supabase Auth dashboard for user
Phase 4: CRUD Operations
AI Prompt for Todo Features
Terminal
"Add todo CRUD operations using Supabase.
Create:
- TodoList component showing all todos
- AddTodo form to create new todo
- TodoItem with edit and delete buttons
- Toggle complete checkbox
- Filter tabs for All/Active/Completed
Fetch todos on dashboard page load.
Use Server Components where possible."
Todo List Component
Expected structure:
Terminal
// components/TodoList.tsx
interface Todo {
id: string
title: string
description: string | null
completed: boolean
created_at: string
}
export function TodoList({ todos }: { todos: Todo[] }) {
return (
<div className="space-y-2">
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
)
}
Add Todo Form
Terminal
// components/AddTodo.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
export function AddTodo() {
const [title, setTitle] = useState('')
const router = useRouter()
const supabase = createClient()
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!title.trim()) return
const { data: { user } } = await supabase.auth.getUser()
await supabase.from('todos').insert({
title,
user_id: user?.id
})
setTitle('')
router.refresh()
}
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
className="flex-1 px-4 py-2 border rounded"
/>
<button type="submit" className="px-4 py-2 bg-primary text-white rounded">
Add
</button>
</form>
)
}
Dashboard Page
Terminal
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { TodoList } from '@/components/TodoList'
import { AddTodo } from '@/components/AddTodo'
export default async function Dashboard() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')
const { data: todos } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false })
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">My Todos</h1>
<AddTodo />
<div className="mt-6">
<TodoList todos={todos || []} />
</div>
</div>
)
}
Phase 5: Refinement
AI Prompt for Polish
Terminal
"Improve the todo app:
1. Add loading states during operations
2. Add error handling with toast notifications
3. Add empty state when no todos
4. Add keyboard shortcuts (Enter to add)
5. Add optimistic updates for better UX"
Add Toast Notifications
Terminal
npx shadcn@latest add toast
Add Empty State
Terminal
{todos.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<p>No todos yet.</p>
<p>Add one above to get started!</p>
</div>
) : (
<TodoList todos={todos} />
)}
Phase 6: Deployment
Push to GitHub
Terminal
git init
git add .
git commit -m "Initial todo app"
git remote add origin https://github.com/you/todo-app.git
git push -u origin main
Deploy to Vercel
- Go to vercel.com/new
- Import GitHub repository
- Add environment variables:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
- Deploy
Update Supabase Auth
Add your Vercel URL to Supabase:
Terminal
Supabase Dashboard → Authentication → URL Configuration
Site URL: https://your-app.vercel.app
Redirect URLs: https://your-app.vercel.app/auth/callback
Verification Checklist
- Can create account
- Can login/logout
- Can add todos
- Can mark todos complete
- Can edit todos
- Can delete todos
- Can filter todos
- Data persists across sessions
- Each user sees only their todos
- Deployed and accessible
Common Issues
"No todos showing"
Check RLS policies are correct and user_id matches.
"Auth not working after deploy"
Update Supabase redirect URLs with Vercel domain.
"Type errors"
Generate types from Supabase:
Terminal
npx supabase gen types typescript --project-id xxx > types/supabase.ts
Extensions
Once working, try adding:
- Due dates
- Priority levels
- Categories/tags
- Search
- Dark mode
- Mobile responsiveness
Summary
You built a full-stack todo app with:
- User authentication
- Database with RLS
- CRUD operations
- Production deployment
Next Steps
Build a more complex project: a blog with content management.
Mark this lesson as complete to track your progress