Back to Blog
TutorialJan 10, 2025

Full-Stack App with Supabase & Next.js

Build authentication, database, and real-time features from scratch. Complete walkthrough with code.

16 min read
Published Jan 10, 2025

Why Supabase + Next.js is the Best Stack Right Now

I used to spend days setting up authentication, databases, and real-time features. Now? Supabase + Next.js handles all of it.

In this post, I'll build a complete real-time collaborative note-taking app from scratch. By the end, you'll have:

  • User authentication (Google + email/password)
  • PostgreSQL database
  • Real-time updates
  • Row-level security
  • Deployed on Vercel

Step 1: Setup

Create a Supabase Project

Go to supabase.com, sign up, and create a new project. This takes 2 minutes. You'll get a PostgreSQL database instantly.

Create a Next.js App

npx create-next-app@latest my-app
cd my-app
npm install @supabase/supabase-js

Add Env Variables

Copy your Supabase URL and API key to .env.local:

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-api-key

Step 2: Database Schema

Create Tables

Open the Supabase dashboard and run this SQL:

-- Notes table
CREATE TABLE notes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users(id) NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Share table
CREATE TABLE note_shares (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  note_id UUID REFERENCES notes(id) ON DELETE CASCADE,
  shared_with_email TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Enable Row Level Security

-- Enable RLS on notes
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;

-- Users can only see their own notes
CREATE POLICY "Users can see own notes" ON notes
  FOR SELECT USING (auth.uid() = user_id);

-- Users can only insert their own notes
CREATE POLICY "Users can insert own notes" ON notes
  FOR INSERT WITH CHECK (auth.uid() = user_id);

Step 3: Authentication

Create Auth Helper

// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export const signUp = (email: string, password: string) =>
  supabase.auth.signUp({ email, password })

export const signIn = (email: string, password: string) =>
  supabase.auth.signInWithPassword({ email, password })

export const signOut = () => supabase.auth.signOut()

Create Login Component

'use client'

import { useState } from 'react'
import { signIn } from '@/lib/supabase'

export function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    const { error } = await signIn(email, password)
    if (error) alert(error.message)
    setLoading(false)
  }

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button disabled={loading}>
        {loading ? 'Loading...' : 'Sign In'}
      </button>
    </form>
  )
}

Step 4: CRUD Operations

Fetch Notes (Read)

const getNotes = async () => {
  const { data, error } = await supabase
    .from('notes')
    .select('*')
    .order('created_at', { ascending: false })
  
  if (error) throw error
  return data
}

Create Note

const createNote = async (title: string, content: string) => {
  const { data: { user } } = await supabase.auth.getUser()
  
  const { data, error } = await supabase
    .from('notes')
    .insert([
      {
        user_id: user?.id,
        title,
        content
      }
    ])
    .select()
  
  if (error) throw error
  return data
}

Update Note

const updateNote = async (id: string, title: string, content: string) => {
  const { data, error } = await supabase
    .from('notes')
    .update({ title, content, updated_at: new Date() })
    .eq('id', id)
    .select()
  
  if (error) throw error
  return data
}

Delete Note

const deleteNote = async (id: string) => {
  const { error } = await supabase
    .from('notes')
    .delete()
    .eq('id', id)
  
  if (error) throw error
}

Step 5: Real-Time Updates

Subscribe to Changes

'use client'

import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'

export function NotesList() {
  const [notes, setNotes] = useState([])

  useEffect(() => {
    // Get initial data
    getNotes()

    // Subscribe to changes
    const subscription = supabase
      .from('notes')
      .on('*', (payload) => {
        if (payload.eventType === 'INSERT') {
          setNotes(prev => [payload.new, ...prev])
        } else if (payload.eventType === 'UPDATE') {
          setNotes(prev =>
            prev.map(n => n.id === payload.new.id ? payload.new : n)
          )
        } else if (payload.eventType === 'DELETE') {
          setNotes(prev => prev.filter(n => n.id !== payload.old.id))
        }
      })
      .subscribe()

    return () => subscription.unsubscribe()
  }, [])

  return (
    <div>
      {notes.map(note => (
        <div key={note.id}>{note.title}</div>
      ))}
    </div>
  )
}

Step 6: Deploy on Vercel

Connect Repository

Push your code to GitHub, then connect to Vercel. Takes 2 minutes.

Add Environment Variables

In Vercel dashboard → Settings → Environment Variables, add:

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY

Deploy

Done. Your app is live.

Key Takeaways

  • Supabase = Firebase but with PostgreSQL - Better for complex queries
  • Row-level security is built-in - Your users can't access others' data
  • Real-time is easy - Just subscribe to changes
  • Cost is minimal - Free tier covers most side projects
  • Deploy in minutes - Supabase + Next.js + Vercel is the fastest stack

Next Steps

From here, you can add:

  • File uploads to Supabase Storage
  • Webhooks for notifications
  • Edge Functions for complex logic
  • Full-text search

The fundamentals you've learned cover 80% of full-stack needs.

Key Takeaways

Related Guides

Want more articles like this?

Subscribe to get practical guides and case studies delivered to your inbox. No spam, just real systems that work.