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.
We will:
- 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
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.
SECTION 1 – Setting Up Supabase
Step 1 – Create a Supabase Project
- Go to Supabase
- Click Start your project
Click Sign Up
Choose your preferred method (Email or GitHub)
After authentication, you’ll land on your backend.
Step 2 – Create a New Project
From your account dashboard:
- Click New Project
- Provide project name
- Set database password
After the project is created, you’ll see your project dashboard.
Step 3 – Create Tables Using SQL Editor
From the left sidebar:
- Click SQL Editor
Paste the following SQL:
-- 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);
Click Run.
Now go to Database → Tables in the sidebar.
You should see:
- profiles
- posts
- comments
Click into each table to view structure.
You can optionally insert dummy data for testing.
Step 4 – Get API Keys and URL
API ANON KEY:
Settings > API Keys > Legacy anon, service_role API Keys (tab)
SUPABSE URL
API Docs > Introduction
These will be used in your frontend.
SECTION 2 – React + TypeScript Code
We assume a Vite + React + TypeScript setup.
Step 0 – Create Environment Variables
Before writing any code, create a .env file at the root of your project.
If you are using Vite, name it:
.env
Add the following inside it:
VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here
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.
1. Supabase Client
// 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)
Assumptions for Section 2
- 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
File: src/pages/Auth.tsx
This component handles both Register and Login.
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>
)
}
File: src/pages/ListPosts.tsx
This component fetches and lists all posts.
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>
)
}
File: src/pages/CreatePost.tsx
This component allows authenticated users to create a post.
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>
)
}
File: src/pages/PostDetails.tsx
This component:
- Shows a single post
- Fetches and displays comments for that post
- Allows adding a new comment
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>
)
}
Final Result
You now have:
- Supabase project
- Database tables
- Authentication
- Post creation
- Comment system
This is a complete base-level blog system using Supabase and React.
You can now extend it with:
- image uploads
- protected routes










Top comments (0)