Home/Blog/Build and Deploy a Full-Stack Project This Weekend (Step-by-Step, Free Tools Only)
CareerPlacementsInterview

Build and Deploy a Full-Stack Project This Weekend (Step-by-Step, Free Tools Only)

Every placement guide says 'have projects.' This guide actually shows you how β€” a complete full-stack task manager built with Next.js, Supabase, and deployed on Vercel in 48 hours, at zero cost.

S
SCS TeamΒ·15 February 2026Β·14 min read

"Build projects" is the most common career advice for CS students. It's also the vaguest. What kind of project? How complex? What stack? How do you deploy it?

This guide removes all ambiguity. By the end of this weekend, you'll have a deployed, working full-stack application with authentication, a database, and a real URL you can put on your resume.

We're building a Task Manager β€” simple enough to finish in a weekend, complex enough to demonstrate real full-stack skills.


What You'll Build

A Task Manager with:

  • Sign up / Sign in
  • Create, read, update, delete tasks
  • Mark tasks complete
  • Tasks stored per user in a real database
  • Live at your-name-tasks.vercel.app (or a custom domain)

Tech stack (all free):

  • Next.js 14 β€” React framework with App Router
  • Supabase β€” PostgreSQL database + authentication (free tier: 2 projects, 500MB)
  • Vercel β€” Hosting (free tier: unlimited personal projects)
  • Tailwind CSS β€” styling

This stack is what real companies use. It's not a toy.


Saturday: Setup and Authentication (4-5 hours)

Hour 1: Project Setup

npx create-next-app@latest task-manager \
  --typescript \
  --tailwind \
  --app \
  --no-src-dir

cd task-manager
npm install @supabase/ssr @supabase/supabase-js

Hour 2: Supabase Setup

  1. Go to supabase.com β†’ New Project
  2. Choose a name and password (save the password)
  3. Wait for project to spin up (~2 minutes)
  4. Go to Settings β†’ API β†’ copy Project URL and anon public key

Create .env.local:

NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key

Create the Supabase client at lib/supabase/client.ts:

import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Hour 3: Database Schema

In Supabase Dashboard β†’ SQL Editor, run:

-- Tasks table
CREATE TABLE tasks (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  title       TEXT NOT NULL CHECK (char_length(title) BETWEEN 1 AND 200),
  description TEXT,
  is_done     BOOLEAN NOT NULL DEFAULT false,
  priority    TEXT CHECK (priority IN ('low', 'medium', 'high')) DEFAULT 'medium',
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Row Level Security: users can only see their own tasks
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can CRUD own tasks"
  ON tasks FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

Hour 4: Authentication Pages

In Supabase Dashboard β†’ Authentication β†’ Providers β†’ enable Email.

Create app/signup/page.tsx:

'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'

export default function SignUpPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  const handleSignUp = async () => {
    setError('')
    setLoading(true)
    const { error } = await supabase.auth.signUp({ email, password })
    if (error) {
      setError(error.message)
    } else {
      router.push('/tasks')
    }
    setLoading(false)
  }

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-4 p-8 bg-white rounded-xl shadow">
        <h1 className="text-2xl font-bold">Create account</h1>
        
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          className="w-full border rounded px-3 py-2"
        />
        <input
          type="password"
          placeholder="Password (min 6 chars)"
          value={password}
          onChange={e => setPassword(e.target.value)}
          className="w-full border rounded px-3 py-2"
        />
        
        {error && <p className="text-red-500 text-sm">{error}</p>}
        
        <button
          onClick={handleSignUp}
          disabled={loading}
          className="w-full bg-blue-600 text-white py-2 rounded font-semibold hover:bg-blue-700 disabled:opacity-50"
        >
          {loading ? 'Creating account...' : 'Sign up'}
        </button>
        
        <p className="text-center text-sm">
          Already have an account?{' '}
          <a href="/signin" className="text-blue-600 hover:underline">Sign in</a>
        </p>
      </div>
    </div>
  )
}

Create a similar app/signin/page.tsx using supabase.auth.signInWithPassword({ email, password }).


Sunday: Core Features and Deployment (5-6 hours)

Hour 1: Tasks API Routes

Create app/api/tasks/route.ts:

import { NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

function createClient() {
  const cookieStore = cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (toSet) => {
          try { toSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) }
          catch {}
        }
      }
    }
  )
}

// GET: fetch all tasks for the current user
export async function GET() {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { data, error } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false })

  if (error) return NextResponse.json({ error: error.message }, { status: 500 })
  return NextResponse.json({ tasks: data })
}

// POST: create a new task
export async function POST(req: NextRequest) {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const body = await req.json()
  const { data, error } = await supabase
    .from('tasks')
    .insert({ ...body, user_id: user.id })
    .select()
    .single()

  if (error) return NextResponse.json({ error: error.message }, { status: 400 })
  return NextResponse.json({ task: data }, { status: 201 })
}

Hour 2-3: Tasks Dashboard UI

Create app/tasks/page.tsx:

'use client'
import { useEffect, useState } from 'react'

interface Task {
  id: string
  title: string
  description: string | null
  is_done: boolean
  priority: 'low' | 'medium' | 'high'
  created_at: string
}

const PRIORITY_COLORS = {
  low: 'bg-green-100 text-green-800',
  medium: 'bg-yellow-100 text-yellow-800',
  high: 'bg-red-100 text-red-800'
}

export default function TasksPage() {
  const [tasks, setTasks] = useState<Task[]>([])
  const [newTitle, setNewTitle] = useState('')
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/tasks')
      .then(r => r.json())
      .then(data => { setTasks(data.tasks || []); setLoading(false) })
  }, [])

  const addTask = async () => {
    if (!newTitle.trim()) return
    const res = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: newTitle.trim(), priority: 'medium' })
    })
    const data = await res.json()
    if (data.task) {
      setTasks(prev => [data.task, ...prev])
      setNewTitle('')
    }
  }

  const toggleDone = async (task: Task) => {
    await fetch(`/api/tasks/${task.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ is_done: !task.is_done })
    })
    setTasks(prev => prev.map(t => t.id === task.id ? { ...t, is_done: !t.is_done } : t))
  }

  const deleteTask = async (id: string) => {
    await fetch(`/api/tasks/${id}`, { method: 'DELETE' })
    setTasks(prev => prev.filter(t => t.id !== id))
  }

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">My Tasks</h1>

      {/* Add task */}
      <div className="flex gap-2 mb-6">
        <input
          value={newTitle}
          onChange={e => setNewTitle(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTask()}
          placeholder="Add a new task..."
          className="flex-1 border rounded px-3 py-2"
        />
        <button onClick={addTask} className="bg-blue-600 text-white px-4 py-2 rounded font-semibold">
          Add
        </button>
      </div>

      {/* Task list */}
      {loading ? (
        <p className="text-gray-500">Loading...</p>
      ) : tasks.length === 0 ? (
        <p className="text-gray-500 text-center py-8">No tasks yet. Add one above!</p>
      ) : (
        <ul className="space-y-2">
          {tasks.map(task => (
            <li key={task.id} className="flex items-center gap-3 p-3 border rounded-lg bg-white">
              <input
                type="checkbox"
                checked={task.is_done}
                onChange={() => toggleDone(task)}
                className="w-4 h-4"
              />
              <span className={`flex-1 ${task.is_done ? 'line-through text-gray-400' : ''}`}>
                {task.title}
              </span>
              <span className={`text-xs px-2 py-1 rounded-full font-medium ${PRIORITY_COLORS[task.priority]}`}>
                {task.priority}
              </span>
              <button onClick={() => deleteTask(task.id)} className="text-red-400 hover:text-red-600 text-sm">
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

Hour 4: Deploy to Vercel

# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Follow the prompts:
# - Link to your account
# - Set project name
# - Add environment variables (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY)

Or deploy via GitHub:

  1. Push to GitHub (git push origin main)
  2. Go to vercel.com β†’ New Project β†’ Import from GitHub
  3. Add environment variables
  4. Deploy

Your app is now live at https://your-project.vercel.app.


What to Write on Your Resume

Bad:

Built a task manager web app

Good:

Task Manager β€” Full-stack web application with user authentication, PostgreSQL database, and REST API. Built with Next.js 14 App Router, Supabase (PostgreSQL + Auth), and deployed on Vercel. Implements Row Level Security to ensure users can only access their own data. github.com/yourname/task-manager Β· Live Demo

The second version shows: specific tech stack, a real architectural decision (RLS), and links the interviewer can click.


Extensions to Build Next (to stand out more)

Once the basic version works, these take 2-4 hours each:

1. Due dates and reminders β€” Add a due_date column, display overdue tasks in red, sort by urgency.

2. Tags/categories β€” Many-to-many relationship between tasks and tags. Shows you can design relational schemas.

3. Search β€” ILIKE '%query%' on the Supabase query. Shows basic database query knowledge.

4. Drag-to-reorder β€” @hello-pangea/dnd library. Shows frontend interactivity skills.

5. Email notifications for overdue tasks β€” Supabase Edge Functions + Resend. Shows backend integrations.

Each extension is a bullet point on your resume and a talking point in interviews.


What You've Actually Demonstrated

After this weekend, you've:

  • Used a modern full-stack framework (Next.js App Router)
  • Designed and queried a real database (PostgreSQL via Supabase)
  • Implemented authentication correctly (not rolling your own)
  • Used Row Level Security (a real backend engineering concept)
  • Deployed to production with CI/CD (Vercel + GitHub)
  • Written TypeScript

An interviewer who asks "walk me through your project" gets a 3-minute answer covering auth, database design, security decisions, and deployment. That's a strong technical signal.

Start now: While reading this feels productive, the actual value comes from building. Open your terminal, run npx create-next-app@latest, and get to work. The AI Tutor can answer any specific questions as you build.

Ready to practice what you just learned?

Apply these concepts with AI-powered tools built for CS students.