Building Real-Time Applications with Supabase Realtime : A Complete Guide

Elias SoykatElias Soykat
9 min read

Learn how to create interactive, collaborative web applications that update in real-time across all connected clients using Supabase's powerful realtime features.

Introduction

Remember the days when users had to refresh their browser to see new content? Those days are long gone. Today's users expect apps that update instantly - when someone posts a comment, sends a message, or makes a change, everyone should see it right away without refreshing.

This is where Supabase Realtime comes in. It gives your application superpowers by enabling instant updates across all connected clients. In this guide, I'll walk you through everything you need to know to implement real-time features in your applications using Supabase.

What is Supabase Realtime?

Supabase Realtime is a feature that allows you to listen to changes in your Supabase database and receive those changes instantly over WebSockets. This means when data changes on the server, all connected clients get updated immediately - without polling or manual refreshes.

Supabase Realtime is built on:

  • PostgreSQL's built-in replication capabilities

  • WebSockets for efficient client-server communication

  • Channel-based architecture for organizing different realtime subscriptions

Use Cases for Realtime Applications

Before diving into the code, let's explore what you can build with realtime features:

  • Collaborative tools: Like Google Docs where multiple users can edit simultaneously

  • Chat applications: Messages appear instantly for all participants

  • Live dashboards: Analytics that update in real-time

  • Multiplayer games: Game states synchronized across players

  • Notifications: Alert users when something relevant happens

  • IoT monitoring: Track device status and data in real-time

Getting Started with Supabase Realtime

Let's walk through implementing a realtime feature step by step.

Step 1: Setting Up Your Supabase Project

First, you need a Supabase project:

  1. Create an account on supabase.com

  2. Create a new project

  3. Note your project URL and anon key from the API settings

Create a .env.local file in your project:

NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Step 2: Installing Supabase Client

Install the Supabase JavaScript client:

npm install @supabase/supabase-js

Create a client instance in src/lib/supabase.ts:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  realtime: {
    params: {
      eventsPerSecond: 10, // Adjust based on your needs
    },
  },
});

Step 3: Setting Up Your Database for Realtime

For this example, let's imagine we're building a blog with realtime comments. Create your tables in the Supabase SQL Editor:

-- Create posts table
CREATE TABLE public.posts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    user_id UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Create comments table with realtime enabled
CREATE TABLE public.comments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    post_id UUID NOT NULL REFERENCES public.posts(id),
    content TEXT NOT NULL,
    user_id UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Enable Row Level Security
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY;

-- Set up RLS policies
CREATE POLICY "Anyone can read posts" ON public.posts FOR SELECT USING (true);
CREATE POLICY "Users can create posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Anyone can read comments" ON public.comments FOR SELECT USING (true);
CREATE POLICY "Users can create comments" ON public.comments FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Enable realtime for comments
ALTER PUBLICATION supabase_realtime ADD TABLE public.comments;

The key line here is ALTER PUBLICATION supabase_realtime ADD TABLE public.comments, which enables realtime functionality for the comments table.

Step 4: Creating TypeScript Interfaces

Define types for your data in src/types/supabase.ts:

export interface Post {
  id: string;
  title: string;
  content: string;
  user_id: string;
  created_at: string;
}

export interface Comment {
  id: string;
  post_id: string;
  content: string;
  user_id: string;
  created_at: string;
  // Include any joins you might need, like user information
  user_name?: string;
}

Step 5: Creating a Realtime Hook for Comments

Now, let's create a custom hook to handle realtime comment updates:

// src/hooks/useRealtimeComments.ts
import { supabase } from "@/lib/supabase";
import { Comment } from "@/types/supabase";
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";

export const useRealtimeComments = (postId: string, userId?: string) => {
  const [comments, setComments] = useState<Comment[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // Load initial comments
  useEffect(() => {
    const fetchComments = async () => {
      try {
        setLoading(true);

        const { data, error } = await supabase
          .from("comments")
          .select(`
            *,
            profiles:user_id(name, avatar_url)
          `)
          .eq("post_id", postId)
          .order("created_at", { ascending: true });

        if (error) throw error;

        setComments(data as Comment[]);
      } catch (error: unknown) {
        console.error("Error fetching comments:", error);
        setError(error instanceof Error ? error.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };

    fetchComments();
  }, [postId]);

  // Set up realtime subscription
  useEffect(() => {
    // Create a channel for this specific post's comments
    const channel = supabase
      .channel(`comments-for-post-${postId}`)
      .on("postgres_changes", {
        event: "*",
        schema: "public",
        table: "comments",
        filter: `post_id=eq.${postId}` // Only listen to comments for this post
      }, async (payload) => {
        const { eventType, new: newRecord, old: oldRecord } = payload;

        // Fetch user details for the new comment if needed
        let commentWithUser = newRecord as Comment;

        if (eventType === "INSERT") {
          // Try to fetch the user's info if not already included
          if (!commentWithUser.user_name) {
            const { data } = await supabase
              .from("profiles")
              .select("name")
              .eq("id", commentWithUser.user_id)
              .single();

            if (data) {
              commentWithUser = {
                ...commentWithUser,
                user_name: data.name
              };
            }
          }

          // Add the new comment to state
          setComments(prev => [...prev, commentWithUser]);
        } 
        else if (eventType === "UPDATE") {
          // Update the changed comment
          setComments(prev => 
            prev.map(comment => 
              comment.id === commentWithUser.id ? commentWithUser : comment
            )
          );
        } 
        else if (eventType === "DELETE") {
          // Remove the deleted comment
          setComments(prev => 
            prev.filter(comment => comment.id !== (oldRecord as Comment).id)
          );
        }
      })
      .subscribe();

    // Cleanup on unmount
    return () => {
      supabase.removeChannel(channel);
    };
  }, [postId]);

  // Add a new comment
  const addComment = async (content: string) => {
    if (!userId || !content.trim()) return;

    const newComment = {
      id: uuidv4(),
      post_id: postId,
      content,
      user_id: userId,
      created_at: new Date().toISOString()
    };

    try {
      const { error } = await supabase
        .from("comments")
        .insert([newComment]);

      if (error) throw error;
      // We don't need to update the local state here
      // The realtime subscription will handle that automatically
    } catch (error: unknown) {
      console.error("Error adding comment:", error);
      setError(error instanceof Error ? error.message : 'Unknown error');
      return false;
    }

    return true;
  };

  return { 
    comments, 
    loading, 
    error, 
    addComment 
  };
};

Step 6: Creating the Comments Component

Now, let's create a component to display and add comments:

// src/components/Comments.tsx
import { useAuth } from "@/hooks/useAuth";
import { useRealtimeComments } from "@/hooks/useRealtimeComments";
import { Comment as CommentType } from "@/types/supabase";
import { useState } from "react";

interface CommentsProps {
  postId: string;
}

// Single comment component
const Comment = ({ comment }: { comment: CommentType }) => {
  const date = new Date(comment.created_at).toLocaleString();

  return (
    <div className="bg-white p-4 rounded-lg shadow-sm mb-3">
      <div className="flex justify-between items-start">
        <div className="font-semibold">{comment.user_name || "Anonymous"}</div>
        <div className="text-xs text-gray-500">{date}</div>
      </div>
      <div className="mt-2 text-gray-700">{comment.content}</div>
    </div>
  );
};

export default function Comments({ postId }: CommentsProps) {
  const { user } = useAuth();
  const { comments, loading, error, addComment } = useRealtimeComments(
    postId,
    user?.id
  );
  const [newComment, setNewComment] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (newComment.trim()) {
      const success = await addComment(newComment);
      if (success) {
        setNewComment("");
      }
    }
  };

  if (loading) {
    return <div className="animate-pulse p-4">Loading comments...</div>;
  }

  if (error) {
    return <div className="text-red-500 p-4">Error loading comments: {error}</div>;
  }

  return (
    <div className="mt-8">
      <h3 className="text-xl font-bold mb-4">Comments ({comments.length})</h3>

      {user ? (
        <form onSubmit={handleSubmit} className="mb-6">
          <textarea
            className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            placeholder="Add a comment..."
            value={newComment}
            onChange={(e) => setNewComment(e.target.value)}
            rows={3}
            required
          />
          <button
            type="submit"
            className="mt-2 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors"
          >
            Post Comment
          </button>
        </form>
      ) : (
        <div className="bg-gray-100 p-4 rounded-lg mb-6">
          Please sign in to leave a comment.
        </div>
      )}

      <div className="space-y-4">
        {comments.length > 0 ? (
          comments.map((comment) => (
            <Comment key={comment.id} comment={comment} />
          ))
        ) : (
          <div className="text-gray-500 text-center py-8">
            No comments yet. Be the first to comment!
          </div>
        )}
      </div>
    </div>
  );
}

Step 7: Using the Comments Component in a Post Page

// src/app/posts/[id]/page.tsx
"use client";

import Comments from "@/components/Comments";
import { supabase } from "@/lib/supabase";
import { Post } from "@/types/supabase";
import { useEffect, useState } from "react";

export default function PostPage({ params }: { params: { id: string } }) {
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchPost = async () => {
      try {
        setLoading(true);

        const { data, error } = await supabase
          .from("posts")
          .select("*")
          .eq("id", params.id)
          .single();

        if (error) throw error;
        setPost(data);
      } catch (error) {
        console.error("Error fetching post:", error);
      } finally {
        setLoading(false);
      }
    };

    fetchPost();
  }, [params.id]);

  if (loading) {
    return <div className="animate-pulse p-8">Loading post...</div>;
  }

  if (!post) {
    return <div className="p-8">Post not found</div>;
  }

  return (
    <div className="max-w-3xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
      <div className="prose max-w-none mb-8">
        {post.content}
      </div>

      {/* This is our realtime comments component */}
      <Comments postId={post.id} />
    </div>
  );
}

Advanced Realtime Features

Now that you've mastered the basics, let's explore some advanced features of Supabase Realtime.

Presence: Tracking Online Users

Presence allows you to track which users are currently viewing or interacting with your application. It's perfect for showing "who's online" indicators:

// Implementing presence tracking
const presenceChannel = supabase.channel("room-1");

presenceChannel
  .on('presence', { event: 'sync' }, () => {
    // Get the current state
    const state = presenceChannel.presenceState();
    setOnlineUsers(state);
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('User joined:', key, newPresences);
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('User left:', key, leftPresences);
  })
  .subscribe((status) => {
    if (status === 'SUBSCRIBED') {
      // Track this user in the presence system
      presenceChannel.track({
        user_id: userId,
        online_at: new Date().toISOString(),
        // Add any other info you want to track
        username: 'JohnDoe',
        typing: false
      });
    }
  });

You'd enable presence for your table with:

COMMENT ON TABLE public.comments IS E'@realtime {
  "presence": true
}';

Broadcast: Sending Custom Events

You can use Supabase Realtime to broadcast custom events not tied to database changes:

// Broadcast typing indicators
const sendTypingIndicator = (isTyping: boolean) => {
  supabase.channel('room-1')
    .send({
      type: 'broadcast',
      event: 'typing',
      payload: { user: username, isTyping }
    });
};

// Listen for typing indicators
supabase.channel('room-1')
  .on('broadcast', { event: 'typing' }, (payload) => {
    console.log(`${payload.user} is ${payload.isTyping ? 'typing' : 'not typing'}`);
  })
  .subscribe();

Filtering Events

For large applications, you may want to filter which realtime events you receive:

// Only get changes for a specific user's posts
supabase.channel('user-posts')
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'posts',
    filter: `user_id=eq.${userId}`
  }, (payload) => {
    console.log('Change received!', payload);
  })
  .subscribe();

Performance Considerations

When building realtime applications at scale, keep these tips in mind:

  1. Use specific filters: Only subscribe to the data you need

  2. Clean up subscriptions: Always remove channels when components unmount

  3. Batch updates: For high-volume changes, consider debouncing updates to the UI

  4. Monitor bandwidth: Realtime connections use WebSockets which maintain persistent connections

Here is the complete project source code: https://github.com/elias-soykat/supabase-realtime

Conclusion

Supabase Realtime transforms how you build interactive web applications. Instead of building complex backend infrastructure for realtime features, you can leverage Supabase's built-in capabilities to create rich, collaborative experiences with minimal code.

The pattern we've explored here - subscribing to database changes, updating local state based on events, and providing ways to modify data - can be applied to a wide range of applications:

  • Chat applications

  • Collaborative editors

  • Live dashboards

  • Multiplayer games

  • Notifications systems

The possibilities are endless! As you build more sophisticated applications, you'll discover even more powerful ways to use Supabase Realtime.

0
Subscribe to my newsletter

Read articles from Elias Soykat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Elias Soykat
Elias Soykat

A Passionate Software Developer