Building a Simple Blog with Supabase (Posts & Comments)

Published: (February 12, 2026 at 01:27 AM EST)
8 min read
Source: Dev.to

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

  1. Go to SupabaseStart your project
  2. Click Sign Up and choose your preferred method (Email or GitHub)
  3. After authentication you’ll land on your backend dashboard

From the dashboard

  1. New Project
  2. Provide a project name
  3. Set a database password

After the project is created you’ll see the project dashboard.

2. Add the Database Schema

  1. From the left sidebar select SQL Editor
  2. 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 .env file.

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 onSelect callback 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!

0 views
Back to Blog

Related posts

Read more »

미친 react key

map을 통한 렌더링 tsx export function Parent { const array, setArray = useState1, 2, 3, 4, 5; useEffect => { setTimeout => { setArrayprev => 6, 7, 8, 9, 10, ...prev;...