Modern times have seen an explosion in services providing a multitude of serverless possibilities, but what is serverless? Does this mean there are no servers? You’d think, but no. 

“Serverless” traditionally describes cloud providers that abstract the overhead involved in provisioning, maintaining, patching, and scaling a server infrastructure away from application development. As a result, application development teams can focus on delivering functionality to their customers while simultaneously benefiting from a pay-as-you-use model.

In recent years, service providers have taken the idea of serverless even further by providing service offerings that fully manage both server infrastructure and standard application development features such as Authentication, Authorization, Relational Databases, Edge Functions, and Realtime services. These services abstract the infrastructure and implementation responsibilities that application developers would otherwise need to design and implement themselves.

Introducing Supabase

Supabase tags itself as the open-source alternative to Google’s Firebase. Both manage the most common services needed to build applications which include managed Databases, Authentication, Edge Functions, Realtime, and Storage. The Supabase Realtime broadcast and presence services use a pub-sub pattern to enable client communication. The presence capability maintains the state of all clients currently “connected” to a realtime channel, handling technically detailed requirements such as online statuses.

There are several alternatives to Supabase, such as Ably, Pusher, and PubNub, but we found Supabase’s open-source principles and local development support to be best in class. Supabase enables developers to run most of their services locally during development, including a local version of its dashboard.

Building our Serverless Chat Application

Using TypeScript, React, Vite, and Mui, we built a shell for a chat application where users can input a message and the application renders messages in the chat window.

We will update our project for this demo to use Supabase Realtime to turn our shell into a working, multi-user chat application. We have some simple goals for our chat application:

  1. Send messages to all the users connected to the chat room
  2. Show online users in a chat room
  3. Indicate when users join and leave the chat room

To follow the building of our chat application, you can use the Github repository or use our StackBlitz project.

Note: StackBlitz requires you to register and create a Supabase project as the Supabase local development is not supported.

Development Prerequisites

To build the chat application, you will need to have the following installed.

  • Node version 16+
  • Docker (tested with version 20.10.17)

Starting the chat application

To use the application run, npm run dev in a terminal and navigate to http://localhost:9999?room=test.

Installing & Initializing Supabase

Install the Supabase client and SDK using NPM:

npm install supabase --save-dev
npm install @supabase/supabase-js

Once the client is installed, configure the project to use Supabase locally:

./node_modules/.bin/supabase init

NOTE: If following along in the Stackblitz project the above steps are not required

In src/App.tsx, import Supabase client factory:

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

And then create the client using the project’s anonymous key and URL. These are configured to the default values in the .env.local for local development. When using StackBlitz, you must update the environment variables to reflect the Supabase project values. Read more about managing environment variables with Vite.

/** create the supabase client using the env variables */
const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, 
 import.meta.env.VITE_SUPABASE_ANON_KEY);

Setting Up the Supabase Channel for our Chat Room

The broadcast channel must be created and configured for the chat room using the Supabase client. 

  1. Create the Supabase channel for the roomCode configured to receive messages it sends
  2. Listen to the channel for a broadcast with the message event type.
  3. Subscribe to the realtime channel.
  4. Set the channel in the application state so the message from the MessageInput can be sent over the channel in the onMessage callback.
  5. Return a function that unsubscribes from the channel and clears the state.
/** state to store a reference of the realtime channel */
const [channel, setChannel] = useState<RealtimeChannel>();

/** Setup supabase realtime chat channel and subscription */
useEffect(() => {
    /** only create the channel if we have a roomCode and username */
    if (roomCode && username) {
        /**
         * Step 1:
         *
         * Create the supabase channel for the roomCode, configured
         * so the channel receives its own messages
         */
        const channel = supabase.channel(`room:${roomCode}`, {
            config: {
                broadcast: {
                    self: true
                }
            }
        });

        /**
         * Step 2:
         *
         * Listen to broadcast messages with a `message` event
         */
        channel.on('broadcast', { event: 'message' }, ({ payload }) => {
            setMessages((messages) => [...messages, payload]);
        });

        /**
         * Step 3:
         *
         * Subscribe to the channel
         */
        channel.subscribe();

        /**
         * Step 4:
         *
         * Set the channel in the state
         */
        setChannel(channel);

        /**
         * * Step 5:
         *
         * Return a clean-up function that unsubscribes from the channel
         * and clears the channel state
         */
        return () => {
            channel.unsubscribe();
            setChannel(undefined);
        };
    }
}, [roomCode, username]);

The chat channel is ready for use! Our last step is to send the chat messages to the channel instead of storing them directly in the “messages” state. We do this by calling the channel’s .send in the MessageInput component’s onMessage callback:

onMessage={(message) => {
    setMessages((messages) => {
        return [
            ...messages,
            {
                id: createIdentifier(),
                message,
                username,
                type: 'chat'
            }
        ];
    });
}}

To test our changes locally, start the Supabase services using the following command:

npm run start:supabase

NOTE: The first time Supabase starts locally, it can take a few minutes to download the required docker images.

Open http://localhost:9999 in two browsers and test that messages are being sent and received between the two chat windows.

Display Online Chat Users

Leveraging Supabase’s presence functionality, we can subscribe to all users in the rooms channel who are “connected.” Listening to the sync event for presence type messages, we can update the list of connected users in the application’s state. 

The presenceState is an object containing multiple items for each key, so we need to massage the data to return the list of users.

Adding the following to our existing useEffect:

channel.on('presence', { event: 'sync' }, () => {
    /** Get the presence state from the channel, keyed by realtime identifier */
    const presenceState = channel.presenceState();

    /** transform the presence */
    const users = Object.keys(presenceState)
        .map((presenceId) => {
            const presences = presenceState[presenceId] as unknown as { username: string }[];
            return presences.map((presence) => presence.username);
        })
        .flat();
    /** sort and set the users */
    setUsers(users.sort());
});

For users to discover the username of the connected clients, each client needs to pass their details channel’s track function within the subscription callback. We do this by updating the existing channel subscribe call:

channel.subscribe((status) => {
    if (status === 'SUBSCRIBED') {
        channel.track({ username });
    }
});

Indicate when Users Join and Leave the Chat Room

The last goal of the chat room is to indicate when users join and leave the room, using the join and leave presence message event types. We need to add a listen for these events in our existing useEffect, and we will send a distinct kind of message to indicate it is related to presence:

  1. Add a listener for the join presence event, map to the presence message format, and add to the message state.
  2. Add a listener for the leave presence event, map to the presence message format, and add to the message state.
/**
 * Step 1:
 *
 * Listen to presence event for users joining the chat room
 */
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
	const presenceMsg = newPresences.map(({ username }) => {
		return {
			id: createIdentifier(),
			type: 'presence' as const,
			username,
			message: 'joined' as const
		};
	});
	setMessages((messages) => [...messages, ...presenceMsg]);
});

/**
 * Step 2:
 *
 * Listen to presence event for users leaving the chat room
 */
channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
	const presenceMsg = leftPresences.map(({ username }) => {
		return {
			id: createIdentifier(),
			type: 'presence' as const,
			username,
			message: 'left' as const
		};
	});
	setMessages((messages) => [...messages, ...presenceMsg]);
});

NOTE: The ChatMessage and PresenceMessage interfaces are in the src/components/ChatWindow.tsx component.

Creating a Supabase Project

It’s time to try out our chat application using the live Supabase service! 

Sign up for an account at https://app.supabase.com and create your “chatter” project. 

Update Chatter’s VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables in the .env.production file with the values from your newly created Supabase project.

Run npm run build && npm run preview in the terminal.

NOTE: The Supabase anonymous key and URL are available in the API project settings

To be sure you are no longer using the Supabase local development services, run: ./node_modules/.bin/supabase stop to terminate all services.

We now have our completed chat application, and even though there are plenty of additional changes we could implement to improve the usability of our fun little app, that’s all we have time for today, kids. Here are some ideas for further exploration:

  • Save the username for the chat room in the browser’s local storage
  • Enable editing messages using the message identifier
  • Show chat messages in pending and failed states with a retry option
  • Persist chat room messages using Supabase’s serverless database services
  • User avatars using Supabase’s serverless storage services
  • Abstract the chatroom functionality to a custom React hook

Final Thoughts

Supabase is a fantastic set of tools that enables developers to prototype ideas and deliver end-to-end products without needing to set up and maintain any infrastructure or, in some cases, backend code entirely. SitePen used Supabase to create a digital version of Milestone Mayhem, a game that deals with the trials and tribulations of software development.