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:
- Send messages to all the users connected to the chat room
- Show online users in a chat room
- 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.
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.
- Create the Supabase channel for the
roomCode
configured to receive messages it sends - Listen to the channel for a
broadcast
with themessage
event type. - Subscribe to the realtime channel.
- Set the channel in the application state so the message from the
MessageInput
can be sent over the channel in theonMessage
callback. - 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:
- Add a listener for the
join
presence event, map to the presence message format, and add to the message state. - 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
andPresenceMessage
interfaces are in thesrc/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.