In this in-depth tutorial you'll build a production-ready FullStack Dating Website using Next.js, Supabase (Postgres + Realtime), and Stream for chat & video. We cover everything from authentication and DB schema to matching logic, realtime chat, and one-to-one video calls β a complete, intermediate β advanced project.
- Next.js (App Router) β Server & client components for fast SSR/SSG
- Supabase β Postgres database, auth, RLS, storage & realtime
- Stream β Real-time chat & video/call SDK
- TailwindCSS β Utility-first styling & responsive layouts
- TypeScript β Type-safe codebase
- Vercel β Recommended hosting & serverless deployment
- π Auth β Secure sign-up, sign-in, and session handling
- π§Ύ Postgres Schema β Profiles, matches, messages, calls (RLS-ready)
- π€ Profile Page β View & edit user profile with photos & bio
- π§ͺ Fake Profiles Seeder β Seed the database for local testing
- β€οΈ Matching System β Discover, like & match users
- π¬ Realtime Chat β One-to-one messaging via Stream
- π₯ Live Video Calls β WebRTC-backed calls using Stream SDK
- π± Responsive UI β Mobile-first design with Tailwind
- β Production-ready β Env config, deployment guide, and seeding scripts
- Node.js (v18+)
- Supabase account
- Stream account
- Vercel account for deployment
-- =====================================================
-- MatchParaSaTarong Dating App - Complete Database Schema
-- =====================================================
-- Enable necessary extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- =====================================================
-- TABLES
-- =====================================================
-- Users table (extends Supabase auth.users)
CREATE TABLE public.users (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
full_name TEXT NOT NULL,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
gender TEXT CHECK (gender IN ('male', 'female', 'other')) NOT NULL,
birthdate DATE NOT NULL,
bio TEXT,
avatar_url TEXT,
preferences JSONB DEFAULT '{"age_range": {"min": 18, "max": 50}, "distance": 25, "gender_preference": []}'::jsonb,
location_lat DECIMAL(10, 8),
location_lng DECIMAL(11, 8),
last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_verified BOOLEAN DEFAULT FALSE,
is_online BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Likes table
CREATE TABLE public.likes (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
from_user_id UUID REFERENCES public.users(id) ON DELETE CASCADE NOT NULL,
to_user_id UUID REFERENCES public.users(id) ON DELETE CASCADE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(from_user_id, to_user_id)
);
-- Matches table
CREATE TABLE public.matches (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user1_id UUID REFERENCES public.users(id) ON DELETE CASCADE NOT NULL,
user2_id UUID REFERENCES public.users(id) ON DELETE CASCADE NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user1_id, user2_id)
);
-- =====================================================
-- INDEXES
-- =====================================================
-- Users table indexes
CREATE INDEX idx_users_username ON public.users(username);
CREATE INDEX idx_users_email ON public.users(email);
CREATE INDEX idx_users_gender ON public.users(gender);
CREATE INDEX idx_users_birthdate ON public.users(birthdate);
CREATE INDEX idx_users_location ON public.users(location_lat, location_lng);
CREATE INDEX idx_users_last_active ON public.users(last_active);
CREATE INDEX idx_users_created_at ON public.users(created_at);
-- Likes table indexes
CREATE INDEX idx_likes_from_user ON public.likes(from_user_id);
CREATE INDEX idx_likes_to_user ON public.likes(to_user_id);
CREATE INDEX idx_likes_created_at ON public.likes(created_at);
-- Matches table indexes
CREATE INDEX idx_matches_user1 ON public.matches(user1_id);
CREATE INDEX idx_matches_user2 ON public.matches(user2_id);
CREATE INDEX idx_matches_created_at ON public.matches(created_at);
-- =====================================================
-- TRIGGERS & FUNCTIONS
-- =====================================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger for users table
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to create match when both users like each other
CREATE OR REPLACE FUNCTION create_match_on_mutual_like()
RETURNS TRIGGER AS $$
BEGIN
-- Check if the other user has also liked this user
IF EXISTS (
SELECT 1 FROM public.likes
WHERE from_user_id = NEW.to_user_id
AND to_user_id = NEW.from_user_id
) THEN
-- Create a match (avoid duplicates)
INSERT INTO public.matches (user1_id, user2_id)
VALUES (
LEAST(NEW.from_user_id, NEW.to_user_id),
GREATEST(NEW.from_user_id, NEW.to_user_id)
)
ON CONFLICT (user1_id, user2_id) DO NOTHING;
END IF;
RETURN NEW;
END;
$$ language 'plpgsql' SECURITY DEFINER;
-- Trigger for creating matches
CREATE TRIGGER create_match_trigger AFTER INSERT ON public.likes
FOR EACH ROW EXECUTE FUNCTION create_match_on_mutual_like();
-- Function to update user's last_active timestamp
CREATE OR REPLACE FUNCTION update_last_active()
RETURNS TRIGGER AS $$
BEGIN
UPDATE public.users SET last_active = NOW() WHERE id = NEW.from_user_id;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger for updating last_active when user interacts
CREATE TRIGGER update_last_active_trigger AFTER INSERT ON public.likes
FOR EACH ROW EXECUTE FUNCTION update_last_active();
-- Function to create user profile when user signs up
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (
id,
full_name,
username,
email,
gender,
birthdate,
bio,
avatar_url,
preferences
) VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'full_name', split_part(NEW.email, '@', 1), 'User'),
COALESCE(NEW.raw_user_meta_data->>'username', split_part(NEW.email, '@', 1), 'user'),
NEW.email,
'other',
CURRENT_DATE,
'',
NULL,
'{"age_range": {"min": 18, "max": 50}, "distance": 25, "gender_preference": []}'::jsonb
);
RETURN NEW;
END;
$$ language 'plpgsql' SECURITY DEFINER;
-- Trigger to create user profile on signup
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
-- =====================================================
-- ROW LEVEL SECURITY (RLS) POLICIES
-- =====================================================
-- Enable RLS on all tables
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.likes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.matches ENABLE ROW LEVEL SECURITY;
-- Users table policies
CREATE POLICY "Users can view their own profile" ON public.users
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update their own profile" ON public.users
FOR UPDATE USING (auth.uid() = id);
-- Likes table policies
CREATE POLICY "Users can view their own likes" ON public.likes
FOR SELECT USING (auth.uid() = from_user_id OR auth.uid() = to_user_id);
CREATE POLICY "Users can create their own likes" ON public.likes
FOR INSERT WITH CHECK (auth.uid() = from_user_id);
CREATE POLICY "Users can delete their own likes" ON public.likes
FOR DELETE USING (auth.uid() = from_user_id);
-- Matches table policies
CREATE POLICY "Users can view their own matches" ON public.matches
FOR SELECT USING (auth.uid() = user1_id OR auth.uid() = user2_id);
# replace with your repo when created
git clone https://github.com/yourusername/nextjs-dating-app.git
cd nextjs-dating-app
npm installCopy the example env and fill your keys:
cp .env.example .env.local
# then edit .env.local with:
# NEXT_PUBLIC_SUPABASE_URL=
# NEXT_PUBLIC_SUPABASE_ANON_KEY=
# SUPABASE_SERVICE_ROLE_KEY=
# STREAM_API_KEY=
# STREAM_API_SECRET=
# NEXT_PUBLIC_CLERK_FRONTEND_API= (if using Clerk)- Create a new Supabase project and Postgres database.
- Run the provided SQL (schema) file or use the SQL editor to create tables (users, profiles, matches, messages, calls).
- Enable Realtime or replication features if you plan to use Supabase realtime.
- Optionally run the seeder script to create fake profiles for testing:
npm run seed:profilesnpm run dev
# opens at http://localhost:3000- Push your completed code to GitHub.
- Go to Vercel.
- Import your repository.
- Add Environment Variables in Vercel (same as
.env.local). - Set up any server-side secrets (Stream secret, Supabase service key).
- Click Deploy.
Your live app will be hosted on a Vercel subdomain (e.g. https://your-dating-app.vercel.app).
- Next.js Documentation
- Supabase Docs (Auth, Database, Realtime)
- Stream Chat & Video SDK
- Tailwind CSS Docs
- Vercel
- Thank you so much for the tutorial: https://github.com/machadop1407
