Building a Simple Blog with Supabase (Posts & Comments)
Source: Dev.to
Tutorial: Simple Blog with Supabase, React & TypeScript
In this tutorial we will build a simple blog system using Supabase (PostgreSQL + Auth) and React with TypeScript.
The goal isn’t to create something fancy, but to understand how authentication, database tables, and frontend logic connect together in a real application.
What We’ll Do
- 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 & Comments scenario.
Supabase Setup
1. Create a Supabase Project
- Go to Supabase → Start your project
- Click Sign Up and choose your preferred method (Email or GitHub)
- After authentication you’ll land on your backend dashboard
From the dashboard
- New Project
- Provide a project name
- Set a database password
After the project is created you’ll see the project dashboard.
2. Add the Database Schema
- From the left sidebar select SQL Editor
- Paste the following SQL and click Run
-- 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);
3. Verify Tables
- Navigate to Database → Tables in the sidebar.
- You should see three tables: profiles, posts, comments.
- Click each table to view its structure.
Optional: Insert dummy data for testing.
4. Grab API Keys
- Settings → API Keys → note the Legacy anon and service_role keys.
- API Docs → Introduction – you’ll need these values in the frontend.
Frontend Setup (Vite + React + TypeScript)
1. Environment Variables
Create a .env file at the project root (Vite expects exactly this name):
VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here
Tip: Restart the development server after creating or editing the
.envfile.
2. 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)
The client is now ready for use throughout the app.
3. Auth Component (Register & Login)
src/components/Auth.tsx
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 (
## Auth
setFullName(e.target.value)}
/>
setEmail(e.target.value)} />
setPassword(e.target.value)}
/>
Register
Login
)
}
Note: No form validation or styling is applied – this is purely for learning the logic.
4. ListPosts Component (Fetch & Display Posts)
src/components/ListPosts.tsx
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([])
useEffect(() => {
const fetchPosts = async () => {
const { data, error } = await supabase
.from('posts')
.select('id, title, content')
.order('created_at', { ascending: false })
if (error) {
console.error('Error fetching posts:', error)
return
}
setPosts(data as Post[])
}
fetchPosts()
}, [])
return (
## Posts
{posts.map(post => (
onSelect(post.id)}>
### {post.title}
{post.content}
))}
)
}
The component receives an
onSelectcallback to handle navigation to a single‑post view (implementation left as an exercise).
Final Remarks
- No styling or form validation is included – focus is on the data flow.
- You now have a working backend (Supabase) and a minimal frontend (React + TS) that can register, login, list posts, and (with further code) create and comment on posts.
Feel free to expand this foundation with UI libraries, better error handling, and additional features like editing posts or real‑time subscriptions! 🚀
Posts List Component
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
interface Post {
id: string;
title: string;
content: string;
}
export default function Posts({ onSelect }: { onSelect: (id: string) => void }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false });
if (data) setPosts(data);
};
fetchPosts();
}, []);
return (
## Posts
{posts.map(post => (
### onSelect(post.id)}>{post.title}
{post.content}
))}
);
}
This component allows authenticated users to create a post.
Create Post Component
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 (
## Create Post
setTitle(e.target.value)}
/>
setContent(e.target.value)}
/>
Create
);
}
Post Details Component
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(null);
const [comments, setComments] = useState([]);
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 (
## {post.title}
{post.content}
### Comments
{comments.map(c => (
{c.content}
))}
setCommentText(e.target.value)}
/>
Add Comment
);
}
Summary
You now have:
- A Supabase project with the necessary tables.
- Authentication set up.
- Functionality for post creation.
- A comment system for each post.
This constitutes a complete, base‑level blog system using Supabase and React.
Next Steps (Ideas for Extension)
- Image uploads (e.g., using Supabase Storage).
- Protected routes (e.g., with Next.js middleware or React Router guards).
Feel free to build on this foundation!