How To Create Realtime Chat Application using Supabase

No Comments

Are you looking to add realtime functionality to your chat application? Then Supabase can help! It makes it easy to build realtime applications, for example, to allow users to communicate. So, in this blog post, we’ll learn how to create a realtime chat application using Supabase and Svelte. Let’s get started!

This post is based on the login system created in the previous one. You can find it here. In it, we created the supabase project and a login screen supporting user creation and authentication. We will use it in this post to assign chats and messages to different users in Supabase. Thus, I suggest reading the previous post before continuing with this one.

Create the Supabase tables with realtime functionality

To create the chat app functionality, we need three different tables. All tables should support RLS (row-level security) and realtime. The tables are chats (id: int8), chats_users (chat_id: int8 -> fk to chats.id, user_id: uuid -> profiles.id), and messages (id: int8, chat_id: int8 -> fk to chats.id, author_id: uuid -> fk to profiles.id and content: varchar) where an fk (foreign key) connects two tables. You can create a table by going to your project in supabase > “Table Editor” > “New table”. To create a foreign table, click on the link symbol and select the table and column. For example, the final table definition for messages looks like this:

realtime chat using supabase: messages table definition

Next, we have to create RLS policies. The policies define who is allowed to do what with the different columns. This includes who is allowed to SELECT, INSERT, UPDATE or DELETE which entry. We need to create a policy for each table. For simplification, I will create an ALL rule stating that every authenticated user is allowed to do everything.

To create such a policy, go to “Authentication” > “Policies” > <table> > “New policy” > “For full customization” and configure it like this:

realtime chat using supabase: all policy definition

Do this for all tables. So after you are done, you can request data from every table inside the app if you are authenticated!

Now I suggest you create some mock data, for example, two users, one chat, and some messages inside supabase, so we can immediately work with some mock data in the UI building process.

Extend svelte application with routing

To extend the current application with routing, we will use the package svelte-spa-router. As a little disclaimer: I will not show the code for the different screens and components in this guide. I will only show the code necessary to retrieve the data and realtime data from supabase and other logic-related snippets. Check the GitHub repository here to get all the code from the application. If you have any questions, feel free to ask me via chat, ask in the comments, or email me at mail@programonaut.com.

The steps to set up routing are:

  1. npm i svelte-spa-router
  2. Add the possible routes inside App.svelte:
<script lang="ts">
  import Router, { push } from "svelte-spa-router";
  import LoginScreen from "./LoginScreen.svelte";
  import UserScreen from "./UserScreen.svelte";
  import ChatOverviewScreen from "./ChatOverviewScreen.svelte";
  import ChatScreen from "./ChatScreen.svelte";

  const routes = {
    "/": LoginScreen,
    "/user": UserScreen,
    "/chats": ChatOverviewScreen,
    "/chats/:id": ChatScreen
  };
</script>

<Router {routes} />
  1. Now add the navigation in needed places (after login, after logout, etc.). To open a different route, you can call push('#/route'). For example, you can add this inside the LoginScreen.svelte file after successfully logging in by pushing the route ‘user’.

Create chats in supabase and show them in realtime

To show the chats, we first need to create a new screen called ChatOverviewScreen.svelte. Inside the screen, we will have an array of chats, and we will use that array to show chat components on the screen. In addition to the chats, we will also create an option to create a new chat by entering the email address of the user we want to connect with.

To fill the array with chats, we will request an initial version of all chats and create a channel that subscribes to all changes inside the chats table. When it detects a change in the table, we will update the array of chats.

We will create a function to request the chats of the user. For that, we need to query the chats table and join it with chats_users: From this, we get a list of all the chats, and we can join this with the profiles table to get the users’ emails. Using the supabase library, this will look like this:

async function getAllChats() {
    // get all chats where the current user is a member
    const { data: chatIds } = await supabase
        .from('chats')
        .select('id, users:chats_users!inner(user_id)')
        .eq('users.user_id', $currentUser.id)

    // get all chats with the user profiles
    return await supabase
        .from('chats')
        .select('*, users:chats_users!inner(user:profiles(email))')
        .in('id', [chatIds.map(chat => chat.id)])
}

We will call this function after the page is mounted and inside the supabase realtime channel:

onMount(async () => {
    ({ data: allChats } = await getAllChats());
    
    chatsWatcher = supabase.channel('custom-all-channel')
        .on(
            'postgres_changes',
            { event: '*', schema: 'public', table: 'chats' },
            async () => {
                console.log('chats changed');
                ({ data: allChats } = await getAllChats());
            }
        )
        .subscribe()
})

Finally, to prevent memory leaks, we should unsubscribe from the channel when the component is destroyed:

onDestroy(() => {
    chatsWatcher?.unsubscribe();
})

The process for the realtime messages will be similar. But before we look into that, let’s add a function to create a new chat. For that, we will first check if the other user exists, then create a new chat, get the chat id, and add the entries to the chats_users table:

async function createChatWithUser() {
    const {data: otherUser} = await supabase
        .from('profiles')
        .select('id')
        .eq('email', newUserEmail)
        .single()

    if (otherUser) {
        const {data: chat} = await supabase
            .from('chats')
            .insert({})
            .select()
            .single()

        const {data, error} = await supabase
            .from('chats_users')
            .insert([
                {
                    chat_id: chat.id,
                    user_id: $currentUser.id
                },
                {
                    chat_id: chat.id,
                    user_id: otherUser.id
                }]
            )       
    }
}

Create messages in supabase and show them in realtime

As already mentioned, we will follow a similar process to the chats to create messages with realtime updates. For the whole screen, we will create two functions, the first one to check all messages in a given chat and then a function to create a new message in this chat. We will call the get all messages function to initialize the page and whenever the messages table changes.

So let’s first create the message retrieval function:

async function getAllMessages() {
    return await supabase.from("messages").select("*").eq("chat_id", chatId);
}

We then call this function and subscribe to realtime changes on the supabase table messages after the component finished mounting:

onMount(async () => {
    ({ data: allMessages } = await getAllMessages());

    messagesWatcher = supabase
        .channel("custom-all-channel")
        .on("postgres_changes", { event: "*", schema: "public", table: "messages" }, async () => {
        ({ data: allMessages } = await getAllMessages());
        })
        .subscribe();
});

And to prevent memory leaks, we will unsubscribe the changes when the component is destroyed:

onDestroy(() => {
    messagesWatcher?.unsubscribe();
});

Now the second function is used to create a new message and uses the author_id of the currently authenticated user and the chat_id of the current chat:

async function sendMessage() {
    const { data, error } = await supabase.from("messages").insert([
        {
        chat_id: parseInt(chatId),
        author_id: $currentUser.id,
        content: newMessage,
        },
    ]);
}

The messages are an array, and we display them in svelte based on that array. Check out the GitHub Repository here to see how I did that in svelte.

Conclusion

In this blog post, we learned how to create a realtime chat application using Supabase and Svelte. We started by creating the Supabase tables necessary for our chat functionality, including chats, chats_users, and messages, and setting up RLS policies to ensure that only authenticated users could access the data. We then extended the Svelte application from the previous post with routing and created screens to display the chats and the messages of a chat. Next, we implemented realtime functionality using the Supabase API, allowing us to display new chats and messages as they were added to the database.

If you enjoyed this post and want to stay up-to-date on the latest updates and new blog posts, consider subscribing to my monthly newsletter.

Discussion (0)

Add Comment

Your email address will not be published. Required fields are marked *