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

TaskRecommended Tool
Generate full appLovable, v0, Bolt
Multi-file changesClaude Code, Cursor
Quick editsGitHub Copilot, Windsurf
Database setupSupabase Dashboard + AI SQL
Background tasksGitHub Copilot Coding Agent
DeployVercel

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

  1. Go to supabase.com
  2. Create new project
  3. 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

  1. Create account at /signup
  2. Login at /login
  3. Verify redirect to dashboard
  4. 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

  1. Go to vercel.com/new
  2. Import GitHub repository
  3. Add environment variables:
    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
  4. 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