WhatsApp Web Clone Part 1: User Authentication & Chat

10 min read

Let’s build a WhatsApp web clone with NextJS, using Supabase for authentication.

Jeroen L.
Jeroen L.
Published September 27, 2023

Welcome to part one of our WhatsApp web clone project. In this section, we’ll set up our project and required tooling, and add a way to authenticate users with Supabase. Then, we’ll add messaging functionality with Stream. In part two of this series, we’ll add video calling, and in part three, we’ll deploy our new app on Vercel.

Supabase has a convenient user authentication feature allowing many different means of authentication. To keep things simple and move quickly, we will use a username/password combination. After authentication, we need a way for the authenticated user to be carried into Stream’s backend to interact with the chat functionality. We will handle that with some server-side logic in our NextJS application. We’ll stick to one of the cleanest and simplest approaches available, accelerating our implementation as much as possible – but you can always customize things along the way.

Once we are authenticated, we need infrastructure to handle our chat messages. And that’s where Stream Chat comes in. Stream Chat provides a very capable backend we can rely on for transmitting our chat messages. Not only does Stream provide a convenient frontend SDK for our chat integration, it also provides a worldwide edge network allowing fast delivery and capacity to support billions of users.

Let us know what you think of what we built! We always enjoy chatting with fellow developers on X or LinkedIn.Let us know what you think of what we built! We always enjoy chatting with fellow developers on X or LinkedIn.

The finished version of this project is available on Github.

Now that we have a sense of what we’re building and the tools and approaches we’ll use, let’s get started!

Install Required Tooling

If you are familiar with NodeJS and have a runtime installed, you can probably skip this step.

There are many ways you can get a running Node environment (or any language runtimes like Go, Ruby, or Python). Something to be aware of is to try and keep your system-installed language runtime untouched. The runtimes supplied by your operating system are important. In fact, often they are reset and cleaned with a system update. If you do not isolate your system-installed language runtime from installing and updating random packages, there will come a day that parts of your system or your whole system will come down with random issues. Also, if you install packages used for development into your system-installed language runtimes, there’s a good chance a system update will reset parts of your development environment and thus break your workflow.

In general, our advice is to keep it simple by separating these kinds of things. Leave your host OS untouched and try to run as much of your development work in an isolated environment. This also makes it easier to transfer your local work to a server later.

For Node, within Stream we often use NVM. It can be installed on a Mac with:

shell
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

Make sure to check the current version of that NVM installation command at the GitHub page of NVM.

We recommend against using HomeBrew in this instance. It is the NVM recommended approach to use their script. I personally had a few issues using NVM installed through HomeBrew — issues I did not have when using their installer script.

The README of NVM specifically states the following:

Homebrew installation is not supported. If you have issues with homebrew-installed nvm, please brew uninstall it, and install it using the instructions below, before filing an issue.

Once you have installed NVM, you also need to install a Node version.

shell
nvm install node

And that’s it. You are now ready to start using Node on your machine in isolation.

Set Up Project (NextJS / tailwind)

Since we want to get started with a NextJS project using Supabase, I opted to go with a pre-built template created by Supabase. This template contains all of the authentication logic we need. It will save us a lot of time. And, since it is provided by Supabase it should be well supported if any issues arise.

To create a project with Supabase integrated I ran this command.

shell
npx create-next-app@latest -e with-supabase

This command will download all required packages and set up a project with all the Supabase and NextJS packages we need to get started.

If you want to learn more about the pre-built Supabase template, please have a look at this page.

Set Up Authentication Using Supabase Auth

To get everything in working order, we need to create a project with Supabase. For that, make sure you have an account with them and create a new project.

In your fresh project, create a file named .env.local and add the following content:

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

You will need to copy the Supabase URL and anon key from your project in Supabase.

We will only use Supabase as an authentication layer with basic authentication, just an email address and a password.. Still, you can imagine it would be possible to add any of the supported authentication integrations provided by Supabase. Such as SAML, Facebook, X, Apple, GitHub, or any other of the over twenty available means of authentication available on Supebase Auth.

Create and Connect a Stream Chat Project

Now, let’s connect Stream to our project so we can add messaging functionality. First, sign up for an account with Stream. Stream has a risk-free/no credit card free trial and a free tier for hobby projects and small startups. You will get access to all of Stream’s services with a single account, including our Chat API and Video & Audio API.

Once you have registered for an account with Stream:

Navigate to the Stream Dashboard
Copy the key and secret

The secret and the key need to be added to the .env.local file we created earlier.

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

NEXT_PUBLIC_REACT_APP_STREAM_KEY=your-stream-key
REACT_APP_STREAM_SECRET=your-stream-secret

Next, we need to add the Stream Chat React SDK to the project. The SDK provides straightforward frontend access to the chat API and global edge infrastructure, with ready-made UI components you can drop into your project. After the initial integration, our UI components are flexible enough to allow you to tailor the resulting user interface to match the style and tone of the rest of your app. To add the Stream Chat React SDK, run the following command.

npm install stream-chat-react

Testing Our Progress

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

To run our code as-is, run the following command:

shell
npm run dev

This should start a local development server at http:localhost:3000/

Navigate to that page in a browser, and you should see a page with a Supabase logo. Notice the login option at the top. Try it!

Supabase NextJS template

When you go through the signup flow, be aware that you will receive an email with a link to complete your signup. The registration does require you to click a link in your email. The information will end up in the Supabase project you created earlier under the authentication section in the Supabase dashboard.

At this point, we have basic account sign-up and login working, and we have not written any code yet.

Let’s set up an initial chat window using Stream chat.

Building Our Chat Interface

To get a chat going we will need a couple things. We’ll need to add a second user and we need to implement our chat user interface.

To get that second user, we need to sign up for our app again through our newly created authentication pages. Start a private browser session to get a window without a logged-in user. You can re-use the email address you used before by appending a term starting with a plus sign to the local part of your email address.

If you signed up with me@example.com, you can sign up again using the email address me+too@example.com. This is called sub addressing, and it saves the work of creating a second email address manually.

You now have two users signed up to your chat backend.

Next we will implement our chat user interface. Once done, you will be able to start a chat in one window and send messages to your second user in another window.

Let’s begin creating that user interface by replacing the entire contents of the app/page.tsx file with the following:

jsx
import WhatsAppChat from "@/components/WhatsAppChat";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import Link from "next/link";

export const dynamic = "force-dynamic";

export default async function Index() {
  const supabase = createServerComponentClient({ cookies });

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (

Notice the <WhatsAppChat … /> component on this page. It is the central component we will create that contains everything related to the WhatsApp-like user interface. It does not exist yet, so we have to create it now.

Create a file named WhatsAppChat.tsx in the folder components. Make sure the contents of this file are as follows:

"use client"

import { useEffect, useState } from 'react';

import { StreamChat } from 'stream-chat';
import {
  Channel,
  ChannelHeader,
  ChannelList,
  Chat,
  MessageInput,
  MessageList,
  Thread,
  Window,
} from 'stream-chat-react';

import '@stream-io/stream-chat-css/dist/css/index.css';

export default function WhatsAppChat() {  
    const apiKey = process.env.NEXT_PUBLIC_REACT_APP_STREAM_KEY || 'Set API Key';
    const userId = process.env.NEXT_PUBLIC_REACT_APP_CHAT_USER_ID_DUMMY || 'Set USER ID';
    const userToken = process.env.NEXT_PUBLIC_REACT_APP_CHAT_USER_TOKEN_DUMMY;

    const [chatClient, setChatClient] = useState<StreamChat>(StreamChat.getInstance(apiKey));

    useEffect(() => {
    //   const chatClient = StreamChat.getInstance(apiKey);

        chatClient.connectUser({ id: userId }, userToken);

    const channel = chatClient.channel('messaging', {
        name: 'Create a Messaging Channel',
        members: ['dummy', 'jeroenleenartsgetstreamio'],
        // option to add custom fields
        });

        channel.create().then(response => {
            console.log(response);
        }).catch(err => {
            console.log(err);
        });

      // const {
      //   data: { user },
      // } = await supabase.auth.getUser()
    }, [])

    return (
      <Chat client={chatClient}>
       <ChannelList />
       <Channel>
         <Window>
           <ChannelHeader />
           <MessageList />
           <MessageInput />
         </Window>
         <Thread />
       </Channel>
    </Chat>
    )
  }

Looking at the code for the WhatsAppChat component, we do several things:
Import all the things we need.
Get a hold of the credentials we need to be able to use the Stream Chat SDK.
Define the UI by using the SDK components.

Now, let’s return to our browser and see if our application is running!

As you may have noticed, we had to write some code, but not all that much if you look at all the functionality we already have. That’s the power of the Stream Chat SDK UI components working for us. Much of the heavy lifting is done by the components provided by the Stream Chat SDK.

Our app runs (🤞), but it just looks nothing like WhatsApp yet. Let’s change that in the next section by overriding the default styling and colors using Stream’s styling system and Tailwind CSS.

Let’s make it look more like WhatsApp!

To make our current version of the code look more like WhatsApp, we need to apply some style to our implementation.

So, let’s create a file named layout.css in the components directory.

html,
body,
#root {
  height: 100%;
  width: 100%;
}
body {
  margin: 0;
}
#root {
  display: grid;
  grid-template-columns: 40% 1fr;
  gap: 0;
}

.str-chat__channel-list {
  width: 100%;
}
.str-chat__channel {
  width: 100%;
}
.str-chat__thread {
  width: 45%;
}

.profile-row {
  justify-content: space-between;
  background: var(--background-header);
}

.profile-row > div {
  gap: 1rem;
}

.profile-row button {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--icons-color);
}

.profile-row img {
  overflow: hidden;
  border-radius: 50%;
}

.channel-list-container {
  overflow-y: scroll;
}

This already improves things, but there’s more we need to do. The colors are still a bit off.

Modify the file globals.css, copy and paste the stylesheet listed below into the file.

@tailwind base;
@tailwind components;
@tailwind utilities;

@import '~stream-chat-react/dist/css/v2/index.css';

@layer base {
  :root {
    --background: 200 20% 98%;
    --btn-background: 200 10% 91%;
    --btn-background-hover: 200 10% 89%;
    --foreground: 200 50% 3%;

    --whatsapp-green: #128c7e;
    --whatsapp-greenDark: #075e54;
    --whatsapp-greenLight: #25d366;
    --whatsapp-blue: #34b7f1;

    --background-header: #f0f2f5;
    --background-base: #eae6df;
    --icons-color: #54656f;
  }

  @media (prefers-color-scheme: dark) {
    :root {
      --background: 200 50% 3%;
      --btn-background: 200 10% 9%;
      --btn-background-hover: 200 10% 12%;
      --foreground: 200 20% 96%;
    }
  }
}

@layer base {
  * {
    @apply border-foreground/20;
  }
}

.animate-in {
  animation: animateIn 0.3s ease 0.15s both;
}

@keyframes animateIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

body {
  background: var(--background-base);
}

main {
  position: relative;
}

.custom::before {
  content: '';
  position: absolute;
  background: var(--whatsapp-green);
  height: 11rem;
  width: 100%;
  top: 0;
  z-index: -10;
}

.str-chat {
  --str-chat__primary-color: var(--whatsapp-green);
  --str-chat__active-primary-color: #004d40;
  --str-chat__surface-color: #f5f5f5;
  --str-chat__secondary-surface-color: #fafafa;
  --str-chat__primary-surface-color: #e0f2f1;
  --str-chat__primary-surface-color-low-emphasis: #edf7f7;
  --str-chat__border-radius-circle: 6px;
  --str-chat__avatar-border-radius: 50%;
}

We’re almost there. We only need to create a custom ChannelListHeader. To do that, create a file named ChannelListHeader.tsx in the components directory. Its contents should be:

import { Avatar } from 'stream-chat-react';
import { UserResponse } from 'stream-chat';
import './layout.css';
import LogoutButton from './LogoutButton';

export default function ChannelListHeader({
  user,
}: {
  user: UserResponse | undefined;
}): JSX.Element {
  return (
    <div className="text-white flex items-center justify-between profile-row p-4">
      <Avatar
        image={
          'https://images.pexels.com/photos/5378700/pexels-photo-5378700.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'
        }
        shape="rounded"
        size={50}
      />
      <div className="flex items-center">
        <button className="w-6 h-6">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="currentColor"
            className="w-6 h-6"
          >
            <path
              fillRule="evenodd"
              d="M12 2.25c-2.429 0-4.817.178-7.152.521C2.87 3.061 1.5 4.795 1.5 6.741v6.018c0 1.946 1.37 3.68 3.348 3.97.877.129 1.761.234 2.652.316V21a.75.75 0 001.28.53l4.184-4.183a.39.39 0 01.266-.112c2.006-.05 3.982-.22 5.922-.506 1.978-.29 3.348-2.023 3.348-3.97V6.741c0-1.947-1.37-3.68-3.348-3.97A49.145 49.145 0 0012 2.25zM8.25 8.625a1.125 1.125 0 100 2.25 1.125 1.125 0 000-2.25zm2.625 1.125a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875-1.125a1.125 1.125 0 100 2.25 1.125 1.125 0 000-2.25z"
              clipRule="evenodd"
            />
          </svg>
        </button>
        <LogoutButton />
      </div>
    </div>
  );
}

We now have a chat application that has many of the features you would expect a chat implementation to have. It includes image and video attachment support, emojis, and a number of other features known and expected in every chat implementation nowadays. Consider how little code we had to write to get all these features. We get a lot of features working out of the box by relying on the UI components provided by Stream.

Wrapping Up and Next Steps

We would appreciate it if you can drop a ⭐️star⭐️ on the GitHub repository with all code related to this article.

If you enjoyed this article, it is part of a series of articles on building a WhatsApp clone.

Click here to learn more about Stream’s Chat product and the platforms supported by Stream Chat, like Android, iOS, Angular, Flutter, and React Native, to name a few.

To get started with Stream, head over to our trial account sign-up if you have not already done that while following this tutorial.

We aren’t done yet with our implementation though. In the next part we will add video calling to our chat experience. And when we are done adding video, we will also deploy everything on Vercel’s platform.

decorative lines
Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->