DEV Community

Cover image for Building a Simple Blog with Supabase (Posts & Comments)
Fongoh Martin T.
Fongoh Martin T.

Posted on

Building a Simple Blog with Supabase (Posts & Comments)

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

Sign up to supabase

  • Click Sign Up

  • Choose your preferred method (Email or GitHub)

Signup with 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

Setup supabase account

Setup project on supabase

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

Run sql commands on supabase

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);
Enter fullscreen mode Exit fullscreen mode

Click Run.

Execute sql command on supabase

Now go to Database → Tables in the sidebar.

Database tables on supabase

You should see:

  • profiles
  • posts
  • comments

Click into each table to view structure.

Go to table details in supabase

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)

Getting the api anon key on supabase

SUPABSE URL

API Docs > Introduction

Getting the supabase url

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
Enter fullscreen mode Exit fullscreen mode

Add the following inside it:

VITE_SUPABASE_URL=your_project_url_here
VITE_SUPABASE_ANON_KEY=your_anon_public_key_here
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)