Tools
Tools: Building a Simple Blog with Supabase (Posts & Comments)
2026-02-12
0 views
admin
SECTION 1 – Setting Up Supabase ## Step 1 – Create a Supabase Project ## Step 2 – Create a New Project ## Step 3 – Create Tables Using SQL Editor ## Step 4 – Get API Keys and URL ## API ANON KEY: ## SUPABSE URL ## SECTION 2 – React + TypeScript Code ## Step 0 – Create Environment Variables ## 1. Supabase Client ## Assumptions for Section 2 ## File: src/pages/Auth.tsx ## File: src/pages/ListPosts.tsx ## File: src/pages/CreatePost.tsx ## File: src/pages/PostDetails.tsx ## Final Result In this tutorial, we will build a simple blog system using Supabase (PostgreSQL + Auth) and React with TypeScript. The goal is not to build something fancy, but to understand how authentication, database tables, and frontend logic connect together in a real application. This is a practical example that shows how modern backend services and frontend applications work together. We will use a simple Posts and Comments example. Choose your preferred method (Email or GitHub) After authentication, you’ll land on your backend. From your account dashboard: After the project is created, you’ll see your project dashboard. From the left sidebar: Paste the following SQL: Now go to Database → Tables in the sidebar. Click into each table to view structure. You can optionally insert dummy data for testing. Settings > API Keys > Legacy anon, service_role API Keys (tab) API Docs > Introduction These will be used in your frontend. We assume a Vite + React + TypeScript setup. Before writing any code, create a .env file at the root of your project. If you are using Vite, name it: Add the following inside it: You can get these values from:
Settings → API → Project URL and anon public key. Restart your development server after creating or editing the .env file. This component handles both Register and Login. This component fetches and lists all posts. This component allows authenticated users to create a post. This is a complete base-level blog system using Supabase and React. You can now extend it with: Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
-- Enable extension (usually already enabled in Supabase)
create extension if not exists "pgcrypto"; -- profiles (linked to auth.users)
create table profiles ( id uuid primary key references auth.users(id) on delete cascade, full_name text, bio text, image_url text, created_at timestamp default now()
); -- posts
create table posts ( id uuid primary key default gen_random_uuid(), user_id uuid references profiles(id) on delete cascade, title text not null, content text not null, image_url text, created_at timestamp default now()
); -- comments
create table comments ( id uuid primary key default gen_random_uuid(), post_id uuid references posts(id) on delete cascade, user_id uuid references profiles(id) on delete cascade, content text not null, image_url text, created_at timestamp default now()
); -- ==========================
-- ENABLE ROW LEVEL SECURITY
-- ========================== alter table profiles enable row level security;
alter table posts enable row level security;
alter table comments enable row level security; -- PROFILES POLICIES
create policy "Users can view all profiles" on profiles for select using (true); create policy "Users can insert own profile" on profiles for insert with check (auth.uid() = id); create policy "Users can update own profile" on profiles for update using (auth.uid() = id); -- POSTS POLICIES
create policy "Anyone can view posts" on posts for select using (true); create policy "Authenticated users can create posts" on posts for insert with check (auth.uid() = user_id); create policy "Users can update their own posts" on posts for update using (auth.uid() = user_id); create policy "Users can delete their own posts" on posts for delete using (auth.uid() = user_id); -- COMMENTS POLICIES
create policy "Anyone can view comments" on comments for select using (true); create policy "Authenticated users can create comments" on comments for insert with check (auth.uid() = user_id); create policy "Users can delete their own comments" on comments for delete using (auth.uid() = user_id); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
-- Enable extension (usually already enabled in Supabase)
create extension if not exists "pgcrypto"; -- profiles (linked to auth.users)
create table profiles ( id uuid primary key references auth.users(id) on delete cascade, full_name text, bio text, image_url text, created_at timestamp default now()
); -- posts
create table posts ( id uuid primary key default gen_random_uuid(), user_id uuid references profiles(id) on delete cascade, title text not null, content text not null, image_url text, created_at timestamp default now()
); -- comments
create table comments ( id uuid primary key default gen_random_uuid(), post_id uuid references posts(id) on delete cascade, user_id uuid references profiles(id) on delete cascade, content text not null, image_url text, created_at timestamp default now()
); -- ==========================
-- ENABLE ROW LEVEL SECURITY
-- ========================== alter table profiles enable row level security;
alter table posts enable row level security;
alter table comments enable row level security; -- PROFILES POLICIES
create policy "Users can view all profiles" on profiles for select using (true); create policy "Users can insert own profile" on profiles for insert with check (auth.uid() = id); create policy "Users can update own profile" on profiles for update using (auth.uid() = id); -- POSTS POLICIES
create policy "Anyone can view posts" on posts for select using (true); create policy "Authenticated users can create posts" on posts for insert with check (auth.uid() = user_id); create policy "Users can update their own posts" on posts for update using (auth.uid() = user_id); create policy "Users can delete their own posts" on posts for delete using (auth.uid() = user_id); -- COMMENTS POLICIES
create policy "Anyone can view comments" on comments for select using (true); create policy "Authenticated users can create comments" on comments for insert with check (auth.uid() = user_id); create policy "Users can delete their own comments" on comments for delete using (auth.uid() = user_id); CODE_BLOCK:
-- Enable extension (usually already enabled in Supabase)
create extension if not exists "pgcrypto"; -- profiles (linked to auth.users)
create table profiles ( id uuid primary key references auth.users(id) on delete cascade, full_name text, bio text, image_url text, created_at timestamp default now()
); -- posts
create table posts ( id uuid primary key default gen_random_uuid(), user_id uuid references profiles(id) on delete cascade, title text not null, content text not null, image_url text, created_at timestamp default now()
); -- comments
create table comments ( id uuid primary key default gen_random_uuid(), post_id uuid references posts(id) on delete cascade, user_id uuid references profiles(id) on delete cascade, content text not null, image_url text, created_at timestamp default now()
); -- ==========================
-- ENABLE ROW LEVEL SECURITY
-- ========================== alter table profiles enable row level security;
alter table posts enable row level security;
alter table comments enable row level security; -- PROFILES POLICIES
create policy "Users can view all profiles" on profiles for select using (true); create policy "Users can insert own profile" on profiles for insert with check (auth.uid() = id); create policy "Users can update own profile" on profiles for update using (auth.uid() = id); -- POSTS POLICIES
create policy "Anyone can view posts" on posts for select using (true); create policy "Authenticated users can create posts" on posts for insert with check (auth.uid() = user_id); create policy "Users can update their own posts" on posts for update using (auth.uid() = user_id); create policy "Users can delete their own posts" on posts for delete using (auth.uid() = user_id); -- COMMENTS POLICIES
create policy "Anyone can view comments" on comments for select using (true); create policy "Authenticated users can create comments" on comments for insert with check (auth.uid() = user_id); create policy "Users can delete their own comments" on comments for delete using (auth.uid() = user_id); CODE_BLOCK:
.env Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here CODE_BLOCK:
VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here CODE_BLOCK:
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey) CODE_BLOCK:
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey) COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function Auth() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [fullName, setFullName] = useState('') const handleRegister = async () => { const { data, error } = await supabase.auth.signUp({ email, password }) if (error) return alert(error.message) if (data.user) { await supabase.from('profiles').insert({ id: data.user.id, full_name: fullName }) } alert('Registered') } const handleLogin = async () => { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) return alert(error.message) alert('Logged in') } return ( <div> <h2>Auth</h2> <input placeholder="Full Name" onChange={e => setFullName(e.target.value)} /> <input placeholder="Email" onChange={e => setEmail(e.target.value)} /> <input placeholder="Password" type="password" onChange={e => setPassword(e.target.value)} /> <button onClick={handleRegister}>Register</button> <button onClick={handleLogin}>Login</button> </div> )
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function Auth() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [fullName, setFullName] = useState('') const handleRegister = async () => { const { data, error } = await supabase.auth.signUp({ email, password }) if (error) return alert(error.message) if (data.user) { await supabase.from('profiles').insert({ id: data.user.id, full_name: fullName }) } alert('Registered') } const handleLogin = async () => { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) return alert(error.message) alert('Logged in') } return ( <div> <h2>Auth</h2> <input placeholder="Full Name" onChange={e => setFullName(e.target.value)} /> <input placeholder="Email" onChange={e => setEmail(e.target.value)} /> <input placeholder="Password" type="password" onChange={e => setPassword(e.target.value)} /> <button onClick={handleRegister}>Register</button> <button onClick={handleLogin}>Login</button> </div> )
} COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function Auth() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [fullName, setFullName] = useState('') const handleRegister = async () => { const { data, error } = await supabase.auth.signUp({ email, password }) if (error) return alert(error.message) if (data.user) { await supabase.from('profiles').insert({ id: data.user.id, full_name: fullName }) } alert('Registered') } const handleLogin = async () => { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) return alert(error.message) alert('Logged in') } return ( <div> <h2>Auth</h2> <input placeholder="Full Name" onChange={e => setFullName(e.target.value)} /> <input placeholder="Email" onChange={e => setEmail(e.target.value)} /> <input placeholder="Password" type="password" onChange={e => setPassword(e.target.value)} /> <button onClick={handleRegister}>Register</button> <button onClick={handleLogin}>Login</button> </div> )
} COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} export default function ListPosts({ onSelect }: { onSelect: (id: string) => void }) { const [posts, setPosts] = useState<Post[]>([]) useEffect(() => { const fetchPosts = async () => { const { data } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }) if (data) setPosts(data) } fetchPosts() }, []) return ( <div> <h2>Posts</h2> {posts.map(post => ( <div key={post.id}> <h3 onClick={() => onSelect(post.id)}>{post.title}</h3> <p>{post.content}</p> </div> ))} </div> )
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} export default function ListPosts({ onSelect }: { onSelect: (id: string) => void }) { const [posts, setPosts] = useState<Post[]>([]) useEffect(() => { const fetchPosts = async () => { const { data } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }) if (data) setPosts(data) } fetchPosts() }, []) return ( <div> <h2>Posts</h2> {posts.map(post => ( <div key={post.id}> <h3 onClick={() => onSelect(post.id)}>{post.title}</h3> <p>{post.content}</p> </div> ))} </div> )
} COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} export default function ListPosts({ onSelect }: { onSelect: (id: string) => void }) { const [posts, setPosts] = useState<Post[]>([]) useEffect(() => { const fetchPosts = async () => { const { data } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }) if (data) setPosts(data) } fetchPosts() }, []) return ( <div> <h2>Posts</h2> {posts.map(post => ( <div key={post.id}> <h3 onClick={() => onSelect(post.id)}>{post.title}</h3> <p>{post.content}</p> </div> ))} </div> )
} COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function CreatePost() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const handleCreate = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('posts').insert({ user_id: user.id, title, content }) setTitle('') setContent('') alert('Post created') } return ( <div> <h2>Create Post</h2> <input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <textarea placeholder="Content" value={content} onChange={e => setContent(e.target.value)} /> <button onClick={handleCreate}>Create</button> </div> )
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function CreatePost() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const handleCreate = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('posts').insert({ user_id: user.id, title, content }) setTitle('') setContent('') alert('Post created') } return ( <div> <h2>Create Post</h2> <input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <textarea placeholder="Content" value={content} onChange={e => setContent(e.target.value)} /> <button onClick={handleCreate}>Create</button> </div> )
} COMMAND_BLOCK:
import { useState } from 'react'
import { supabase } from '../lib/supabase' export default function CreatePost() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const handleCreate = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('posts').insert({ user_id: user.id, title, content }) setTitle('') setContent('') alert('Post created') } return ( <div> <h2>Create Post</h2> <input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <textarea placeholder="Content" value={content} onChange={e => setContent(e.target.value)} /> <button onClick={handleCreate}>Create</button> </div> )
} COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} interface Comment { id: string content: string
} export default function PostDetails({ postId }: { postId: string }) { const [post, setPost] = useState<Post | null>(null) const [comments, setComments] = useState<Comment[]>([]) const [commentText, setCommentText] = useState('') useEffect(() => { const fetchPost = async () => { const { data } = await supabase .from('posts') .select('*') .eq('id', postId) .single() if (data) setPost(data) } const fetchComments = async () => { const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } fetchPost() fetchComments() }, [postId]) const handleAddComment = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('comments').insert({ post_id: postId, user_id: user.id, content: commentText }) setCommentText('') const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } if (!post) return null return ( <div> <h2>{post.title}</h2> <p>{post.content}</p> <h3>Comments</h3> {comments.map(c => ( <p key={c.id}>{c.content}</p> ))} <input placeholder="Write comment" value={commentText} onChange={e => setCommentText(e.target.value)} /> <button onClick={handleAddComment}>Add Comment</button> </div> )
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} interface Comment { id: string content: string
} export default function PostDetails({ postId }: { postId: string }) { const [post, setPost] = useState<Post | null>(null) const [comments, setComments] = useState<Comment[]>([]) const [commentText, setCommentText] = useState('') useEffect(() => { const fetchPost = async () => { const { data } = await supabase .from('posts') .select('*') .eq('id', postId) .single() if (data) setPost(data) } const fetchComments = async () => { const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } fetchPost() fetchComments() }, [postId]) const handleAddComment = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('comments').insert({ post_id: postId, user_id: user.id, content: commentText }) setCommentText('') const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } if (!post) return null return ( <div> <h2>{post.title}</h2> <p>{post.content}</p> <h3>Comments</h3> {comments.map(c => ( <p key={c.id}>{c.content}</p> ))} <input placeholder="Write comment" value={commentText} onChange={e => setCommentText(e.target.value)} /> <button onClick={handleAddComment}>Add Comment</button> </div> )
} COMMAND_BLOCK:
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase' interface Post { id: string title: string content: string
} interface Comment { id: string content: string
} export default function PostDetails({ postId }: { postId: string }) { const [post, setPost] = useState<Post | null>(null) const [comments, setComments] = useState<Comment[]>([]) const [commentText, setCommentText] = useState('') useEffect(() => { const fetchPost = async () => { const { data } = await supabase .from('posts') .select('*') .eq('id', postId) .single() if (data) setPost(data) } const fetchComments = async () => { const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } fetchPost() fetchComments() }, [postId]) const handleAddComment = async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) return alert('Login first') await supabase.from('comments').insert({ post_id: postId, user_id: user.id, content: commentText }) setCommentText('') const { data } = await supabase .from('comments') .select('*') .eq('post_id', postId) if (data) setComments(data) } if (!post) return null return ( <div> <h2>{post.title}</h2> <p>{post.content}</p> <h3>Comments</h3> {comments.map(c => ( <p key={c.id}>{c.content}</p> ))} <input placeholder="Write comment" value={commentText} onChange={e => setCommentText(e.target.value)} /> <button onClick={handleAddComment}>Add Comment</button> </div> )
} - Set up a Supabase project
- Create database tables for profiles, posts, and comments
- Enable Row Level Security (RLS)
- Write frontend logic for registration, login, creating posts, and commenting - Go to Supabase
- Click Start your project - Click Sign Up
- Choose your preferred method (Email or GitHub) - Click New Project
- Provide project name
- Set database password - Click SQL Editor - You are using Vite + React + TypeScript
- Supabase client is configured in src/lib/supabase.ts
- No form validation is implemented
- No styling is applied
- This is purely for learning the logic - Shows a single post
- Fetches and displays comments for that post
- Allows adding a new comment - Supabase project
- Database tables
- Authentication
- Post creation
- Comment system - image uploads
- protected routes
how-totutorialguidedev.toaiserverpostgresqldatabasegitgithub