How to Build a Realtime Chat Application with Angular 20 and Supabase
Chat applications let you talk in real-time with your friends, family, or coworkers, and help you quickly, effectively, and efficiently transfer of information. When you’re building modern web applications, chat applications are now pretty much a req...

Chat applications let you talk in real-time with your friends, family, or coworkers, and help you quickly, effectively, and efficiently transfer of information. When you’re building modern web applications, chat applications are now pretty much a requirement to enable collaboration and enhanced the user experience.
In this tutorial, we will break down how to build a chat application using modern technologies like Angular and Supabase. Building this chat application will help you learn features such as Google OAuth 2.0 for authentication, Angular router for navigation, the CanActivate
route guard for route protection, and how to call Supabase functions to create, fetch and delete chats.
On the backend, you will learn how to create database tables in Supabase. You’ll also learn about Supabase functions and Supabase triggers.
Table of Contents
How to Set Up Google OAuth 2.0 for Authentication and Authorization
How to Create and Setup the Users Table in Supabase using the SQL Editor
How to Create and Setup the Chat Table in Supabase using the User Interface
How to Integrate Functionality to Create a New Chat Message in the Angular Application
How to Implement Logout Functionality in the Angular Application
Prerequisites
HTML
JavaScript
TypeScript
Installations and Account Configuration:
Before we begin, make sure you have the following installed and ready:
Node.js and npm: Angular requires Node. You can check to see if you have it (and what version you have) by running
node -v
in your terminal.Angular CLI: This is the command-line tool to scaffold and manage Angular projects. If you don’t have it, install it with
npm install -g @angular/cli
. Verify withng version
.A Supabase account: Supabase offers a free tier. Sign up on the Supabase website if you haven’t already.
You can also watch the video version of this article below, or on my YouTube channel:
How to Create the User Interface of the Angular Application
To create the user interface of the application, we’ll use Bootstrap 5. In the index.html
file of the Angular application, you are going to paste the Bootstrap 5 CDN link as seen below:
html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NgChattitle>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
head>
<body>
<app-root>app-root>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous">script>
body>
html>
Above, you have two CDN links from Bootstrap 5. The first is the tag within the head section, while the second is the
tag which is right below the
tag.
Now that you have the Bootstrap 5 CDN link setup within the project, the next step is to create two new components called chat and login, respectively, within a pages folder. You can do that using the command below:
ng g c pages/chat-component && ng g c pages/login-component
The login-component.html
is going to contain the code below:
<section class="login-block">
<div class="container">
<div class="row">
<div class="col-md-12">
<a class="btn btn-lg btn-google btn-block text-uppercase btn-outline" href="#"><img
src="https://res.cloudinary.com/dz4tt9omp/image/upload/v1712537582/google-logo.png"> Signup Using Googlea>
div>
div>
div>
section>
While the login-component.css
will contain the code below:
.login-block {
width: 300px;
margin: 0 auto;
display:flex;
justify-content:center;
align-items:center;
height:100vh;
}
.btn {
border-radius: 2px;
text-transform: capitalize;
font-size: 15px;
padding: 10px 19px;
cursor: pointer
}
.btn-google {
color: #545454;
background-color: #ffffff;
box-shadow: 0 1px 2px 1px #ddd;
}
To see how the user interface looks, you can call the
tag with the app.component.html
file, since the route navigations have not yet been configured. The user interface should look like the screenshot below:
How to Set Up a New Supabase Project
To set up Supabase, you will need to create a new account on Supabase.com by using either a GitHub account, or the traditional email and password. Once you’ve done this, you will be presented with a form to create a new organization as you can see in the image below:
The organization will be created as fast as your internet speed. Once that is done, the next form you’ll see will allow you to create a new Supabase project.
As you can see from the image above, all you need to do to create a new project is to set a database password and select a region close to where you think most of your users will be. This will help reduce latency. With that you can now click on the create button to create a new project.
Once the project creation is complete, you will be navigated to the dashboard below:
With that, you have now set up your new Supabase project.
How to Set Up Google OAuth 2.0 for Authentication and Authorization
To set up Google OAuth 2.0, you need to create an account on Google Cloud Console. Once you create an account, you will be navigated to the dashboard, where you can create a new project by clicking on the select project button on the top left-hand side of the dashboard.
Once you’ve selected the newly created project, you can now begin implementing Google OAuth 2.0 by following these steps:
Click on the hamburger menu on the left-hand side of the dashboard and hover over APIs and services.
Click on Credentials, on the Credentials page, select Create Credentials at the top menu of the dashboard. A dropdown menu will appear. Select Create OAuth client ID.
On the Client ID page, you’ll get a warning message that says “To create an OAuth client ID, you must first configure your consent screen.” Click on the Configure consent screen button.
Next, you’ll be directed to the Branding page. Click on the getting Started button, and you’ll be presented with a form on the overview page as you can see below. Then just fill out the form:
You can now create the OAuth Consent Screen by heading to the Clients tab on the left-side of the dashboard and filling out the details for your application type, the name of your OAuth 2.0 client, as well as the Authorized JavaScript origins.
For the Authorized JavaScript origins, you can enter the URL (http://localhost:4200), since that is the development URL for our Angular application. Then click on the create button. You may get a warning saying “Note: It may take five minutes to a few hours for settings to take effect.”
Once the configuration is complete, you will get a modal that contains a Client ID and a Client Secret, as you can see below:
Make sure you copy the Client ID and Client secret, as you will use this in the Supabase dashboard.
To complete the authentication and authorization setup, head to the Supabase dashboard. Then navigate to the Authentication menu, which is located in the items on the left-side of the dashboard. On this part of the dashboard, you will select Sign In / Providers.
On the Sign In / Providers page, scroll down to the Auth Providers, then select and enable Google. This is where you will paste in the credentials of the Client ID and Client Secret created on the Google Cloud Console. Then click on the save button – and make sure you copy the Callback URL (for OAuth).
The final step in this process is to head back to the GCP dashboard, and under the Clients tab, click on the edit icon of the OAuth 2.0 Client IDs you created previously.
Under the Authorized redirect URIs, click on the Add URI button. An input box will appear. Paste in the link of the Callback URL (for OAuth) you grabbed in the Supabase dashboard and click save.
How to Configure the Router of the Angular Application
Earlier in this tutorial, you created two components: Chat and Login. At this point, you need to setup the route configuration in the app.routes.ts
. In this file, add the code below:
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'chat',
loadComponent: () =>
import('./pages/chat/chat-component').then((com) => com.ChatComponent),
},
{
path: 'login',
loadComponent: () =>
import('./pages/login/login-component').then((com) => com.LoginComponent),
},
{
path: '',
loadComponent: () =>
import('./pages/login/login-component').then((com) => com.LoginComponent),
},
];
Above, you can see the two components now have their separate routes called chat and login, respectively. They can be accessed anywhere in the application.
How to Set Up the Authentication Service
To setup the authentication service in the Angular application, use the following command:
ng g s services/auth-service
Next, you’ll generate the environments folders to setup the environment variables using the below command:
ng g environments
The final configuration you need to do from the terminal before you begin creating the function for the Angular authentication service is to install Supabase with the command below:
npm i @supabase/supabase-js
And with that, you now have Supabase installed in the project and you can begin integrating the functions in the service. Start from the environment.development.ts
file. The current structure of this file should look this way by default:
export const environment = {};
To configure this file, you need to head to the Supabase dashboard. Locate and select the settings menu on the left hand panel of the dashboard. Under the Configuration tab, click on Data API.
You can now grab both the Project URL and anon public key (the arrow is pointing to it in the image above). You can now head over to the environment.development.ts
file and paste in the values of the copied link following the format below:
export const environment = {
production: false,
supabaseUrl: 'https://zktqzszvllbxvjfzkhvk.supabase.co',
supabaseKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InprdHF6c3p2bGxieHZqZnpraHZrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDcyNTg3MDgsImV4cCI6MjA2MjgzNDcwOH0.qf3MA-La6se8QijzLFALKc_XdiISmzDk7AZw4-na0uA',
};
With the environment variables all in place, you can now create the functions for the authentication service.
In the auth-service.ts
which you created previously, start by importing the Supabase package as well as the environments file as you can see below:
import { Injectable } from '@angular/core';
import { SupabaseClient, createClient } from '@supabase/supabase-js';
import { environment } from '../../environments/environment.development';
Next, complete the injection of the Supabase npm
package by injecting it into your constructor:
import { Injectable } from '@angular/core';
import { SupabaseClient, createClient } from '@supabase/supabase-js';
import { environment } from '../../environments/environment.development';
@Injectable({
providedIn: 'root',
})
export class AuthService {
supabase!: SupabaseClient;
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
}
}
How to Create the Service Functions for Login and Sign Out Functionality
The final step in setting up the Auth
service is to create the functions which will later be called in the template. You are going to create four functions which you can see in the code below:
private router = inject(Router);
private _ngZone = inject(NgZone);
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
this.supabase.auth.onAuthStateChange((event, session) => {
localStorage.setItem('session', JSON.stringify(session?.user));
if (session?.user) {
this._ngZone.run(() => {
this.router.navigate(['/chat']);
});
}
});
}
get isLoggedIn(): boolean {
const user = localStorage.getItem('session') as string;
return user === 'undefined' ? false : true;
}
async signInWithGoogle() {
await this.supabase.auth.signInWithOAuth({
provider: 'google',
});
}
async signOut() {
await this.supabase.auth.signOut();
}
}
The first function created is within the constructor. This is the onAuthStateChange
callback function which is derived from Supabase and allows us to listen to Auth changes. It accepts two parameters called event
and session
.
Here, two conditions were instantiated within the onAuthStateChange
callback function. They say that when the session?.user
exists, you proceed to set the value to the local storage, and then navigate the user to the dashboard using the Angular router (which has been imported and injected using the inject()
function).
The second function, isLoggedIn()
, is a getter function that returns a Boolean. It returns either true or false, depending on if it is able to retrieve the user session from localStorage
. This function will be used in the authentication guard which you’ll create later.
The third function, signInWithGoogle()
, allows the user log into the dashboard using the signInWithOAuth()
method provided by Supabase. This allows the user to log into the dashboard using a Google Gmail account.
The final function, signOut()
, allows users to logout of the dashboard by resetting the state of the user session to null.
With all these functions created, the final code base in the auth-service.ts
should look like this:
import { Injectable, NgZone, inject } from '@angular/core';
import { SupabaseClient, createClient } from '@supabase/supabase-js';
import { environment } from '../../environments/environment.development';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private supabase!: SupabaseClient;
private router = inject(Router);
private _ngZone = inject(NgZone);
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
this.supabase.auth.onAuthStateChange((event, session) => {
console.log('event', event);
console.log('session', session);
localStorage.setItem('session', JSON.stringify(session?.user));
if (session?.user) {
this._ngZone.run(() => {
this.router.navigate(['/chat']);
});
}
});
}
get isLoggedIn(): boolean {
const user = localStorage.getItem('session') as string;
return user === 'undefined' ? false : true;
}
async signInWithGoogle() {
await this.supabase.auth.signInWithOAuth({
provider: 'google',
});
}
async signOut() {
await this.supabase.auth.signOut();
}
}
You can now utilize these functions anywhere in the Angular project as a form of state management.
How to Integrate the Authentication Service Function in the Template
The first function we’ll use is the signInWithGoogle()
function. We’ll use it in the login-component.ts
file to allow users log into the application as you can see below:
import { Component, inject } from '@angular/core';
import { AuthService } from '../../services/auth-service';
@Component({
selector: 'app-login',
standalone: true,
imports: [],
templateUrl: './login-component.html',
styleUrl: './login-component.css',
})
export class LoginComponent {
private auth = inject(AuthService);
async handleAuth() {
const response = await this.auth.signInWithGoogle();
}
}
Above, you implemented three features:
Importing
AuthService
into theLoginComponent
Injecting
AuthService
using the Inject function into theLoginComponent
Creating the
handleAuth()
function that allows you call thesignInWithGoogle()
from theAuthService
file.
Now you can head to the login-component.html
file and call the handleAuth(
) function as below within the tag:
<section class="login-block">
<div class="container">
<div class="row">
<div class="col-md-12">
<a (click)="handleAuth()" class="btn btn-lg btn-google btn-block text-uppercase btn-outline" href="#"><img
src="https://res.cloudinary.com/dz4tt9omp/image/upload/v1712537582/google-logo.png"> Signup Using Googlea>
div>
div>
div>
section>
Before you test the implementation, you will need to set the URL configuration in the Supabase dashboard. The URL configuration allows the URLs that authentication providers permit to redirect and post authentication, including wildcards.
As you can see in the above image, the two Redirect URLs provided are localhost, since we are still currently creating the app in our local machine.
With this, you can test the Google OAuth 2.0 configuration by typing the localhost URL (http://localhost:4200) in the browser, clicking on the Signup Using Google button, and selecting a Gmail account you want to sign up/login with. Then you should get navigated to the Chat component.
How to Create Route Protection in Angular
To create route protection in Angular, you can use an in-built mechanism called a Route Guard. The Route Guard is used to control access to certain parts of the Angular application using certain conditions before a route is activated or accessible to the user.
In our case, you will be generating the Route Guard as a function (which is the default in our current version of Angular (20), instead of as a class) using the command below:
ng generate guard auth-guard
You will then see this prompt that asks “Which type of guard would you like to create?”:
Use the spacebar to select CanActivate
, and then press the Enter key to generate the Guard. Two files will be generated: the auth-guard.spec.ts
file (for testing), and the auth-guard.ts
file. Within the auth-guard.ts
file, you will see the boilerplate code below:
import { CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
return true;
};
You can start modifying the above template by importing the Angular Router, the AuthService
file that you created earlier, as well as the Inject function:
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './services/auth-service';
import { inject } from '@angular/core';
Next, use the isLoggedIn
getter that you created earlier in the AuthService
file (which returns a Boolean) to conditionally activate the Chat dashboard for the user based on their login status using the code below:
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './services/auth-service';
import { inject } from '@angular/core';
export const authGuard: CanActivateFn = (route, state) => {
if (inject(AuthService).isLoggedIn === false) {
inject(Router).navigate(['/login']);
return false;
} else {
return true;
}
};
To complete the Guard integration, head over to the app.routes.ts
file and import and inject the Authentication Guard as you can see below:
import { Routes } from '@angular/router';
import { authGuard } from './auth-guard';
export const routes: Routes = [
{
path: 'chat',
canActivate: [authGuard],
loadComponent: () =>
import('./pages/chat/chat-component').then((com) => com.ChatComponent),
},
{
path: 'login',
loadComponent: () =>
import('./pages/login/login-component').then((com) => com.LoginComponent),
},
{
path: '',
loadComponent: () =>
import('./pages/login/login-component').then((com) => com.LoginComponent),
},
];
With this, the route protection implementation is now complete and only authenticated users can view the dashboard.
How to Create and Setup the Users Table in Supabase using the SQL Editor
To create and setup the users table, use the schema below:
id (uuid)
full_name (text)
avatar_url (text)
You can use the SQL Editor in Supabase. The SQL Editor is the third item on the menu panel in the Supabase dashboard. Here you are going to type in the query below in the SQL Editor input field:
CREATE TABLE public.users (
id uuid not null references auth.users on delete cascade,
full_name text NULL,
avatar_url text NULL,
primary key (id)
);
You can now click on the Run button on the bottom right. You should get a message that says: Success. No rows returned, as you can see in the image below:
Now let’s go ahead and enable row level security, as well as the Supabase function and trigger.
How to Configure Row Level Security Policies in Supabase with the SQL Editor
Row Level Security (RLS) in Supabase allows you to control access to individual rows in your database tables based on custom logic. It’s one of the core features for building secure, multi-user applications with Supabase.
RLS lets you define SQL policies that determine which users can SELECT
, INSERT
, UPDATE
, or DELETE
specific rows in a table.
To enable RLS in the users table, type the command below in your SQL Editor:
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
For the purpose of this tutorial, you are going to create just two policies, which are:
The ability for users to access their own profile
The ability for users to update their own profile
The ability for users to access their own profile
To enable users access their own profile, head back to the SQL editor and create a new snippet with the following query:
CREATE POLICY "Permit Users to Access Their Profile"
ON public.users
FOR SELECT
USING ( auth.uid() = id );
With this query, users will be able to access their own profile as long as the authenticated user’s ID matches the id
of the column of the row.
The ability for users to update their own profile
To enable users update their own profile, head back to the SQL editor and create a new snippet with the following query:
CREATE POLICY "Permit Users to Update Their Profile"
ON public.users
FOR UPDATE
USING ( auth.uid() = id );
With the above query, users will be able to update their own profile as long as the authenticated user’s ID matches the id
of the column of the row.
How to Configure Supabase Functions in Supabase with the SQL Editor
Supabase Functions are serverless functions that can be deployed and run within your Supabase project using Supabase Edge Functions.
In this project, you will create a trigger function that automatically creates a new row in the users table whenever a new user is created in the auth.users
table.
CREATE
OR REPLACE FUNCTION public.user_profile() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.users (id, full_name,avatar_url)
VALUES
(
NEW.id,
NEW.raw_user_meta_data ->> 'full_name'::TEXT,
NEW.raw_user_meta_data ->> 'avatar_url'::TEXT
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
To summarize the above query:
You start by defining or replacing a function named
user_profile()
that will be used as a trigger.Next, the trigger inserts a new row into the
public.users
table, and then extracts thefull_name
andavatar_url
from the user's metadata as text.The inserted record is now returned when the trigger function is complete
Finally, you use the
SECURITY DEFINER
keyword so that the function can run with the privileges of the user who created it.
How to Configure Supabase Trigger in Supabase with the SQL Editor
A trigger in Supabase is a PostgreSQL feature used to automatically run a function in response to events on a table (SELECT, INSERT, UPDATE, or DELETE). It’s mostly used with Row Level Security or syncing data across tables.
In this project, you will create a Supabase trigger that automatically runs a function after a new user is created in the auth.users
table.
CREATE TRIGGER
create_user_trigger
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE PROCEDURE
public.user_profile();
To summarize the above query:
The first line creates a trigger named
create_user_trigger
.Next, the INSERT ON statement is activated when a user signs up and a new row is inserted into the
auth.users
tableThen the trigger runs once for every new user added in a new row.
Finally, the custom function
public.user_profile()
is called to perform some logic, typically inserting data into theusers
table.
With the above integration, you can now log into the dashboard with a new google account and view the users table. There you will see the data that contains the id, full_name, and avatar_url as you can see below:
How to Create and Setup the Chat Table in Supabase using the User Interface
To create the chat table, you will use the user interface in Supabase instead of the SQL Editor. To do this, you need to head to the Table Editor menu on the dashboard and click on the New Table button.
Once selected, a modal will popup which contains some input fields such as the table name, description, and columns. You can call the table name chat and omit the description for now since it’s optional. In the columns section, fill out the fields using the schema below:
id (uuid)
Created At (date)
text (text)
editable (boolean)
sender (uuid)
You can see the configuration for this in the table below:
Next up, you need to add a foreign key relation for the users table. To do this, you scroll to the bottom of the modal and click on the Add foreign key relation button. This will prompt another modal on top of the current modal. Here you can take the following steps:
Under the Select a table to reference to label, select the users table.
Under the public.chat label, select the sender option.
Under public.users label, select uuid.
Under the Action if referenced row is updated label, select Cascade.
Under the Action if referenced row is removed label, select Cascade as well.
If you’ve followed the above steps, you can now click on the save button, which successfully creates the chat table.
How to Create and Setup the Chat Table Policies in Supabase
The final step you need to perform for the chat table is to add a Row Level Security policy. You can do this by clicking the Add RLS policy button at the top of the chat table page.
A new page will appear. Then you can click on the Create policy button, which displays a modal.
The first policy you will create is the DELETE policy, which will have the configuration you can see in the image below:
From the above image, we made these four implementations:
First, we entered the policy name as “Delete by User ID“.
Next we selected the DELETE policy command clause.
Then under the targeted roles, we selected authenticated in the drop down select, to allow only authenticated users to perform delete operations.
Finally, under the USE OPTIONS ABOVE TO EDIT section, in line 7, we condition the query as
(auth.uid() = sender)
This allows only logged in users to delete their data.
You can now click on the Save policy button to complete the DELETE setup.
The second policy you will create is the INSERT policy, which will have the configuration you can see in the image below:
From the above image, four implementations were made:
First, we entered the policy name as “Insert for Authenticated Users“.
Next we selected the INSERT policy command clause.
Then under the targeted roles, we selected authenticated in the drop down to allow only authenticated users perform insert operations.
Finally, under the USE OPTIONS ABOVE TO EDIT section, in line 7, the query was conditioned as
((sender = auth.uid()) AND (created_at = now()))
. The first condition ensures that thesender
field in the inserted row matches the currently logged-in user's ID (from the Supabase JWT), while the second condition ensures that thecreated_at
field is exactly equal to the current timestamp at the time of insertion.
The third policy you will create is the SELECT policy, which will have the configuration you can see in the image below:
From the above image, we implemented four things:
First, we entered the policy name as “Read Data for Authenticated Users“.
Next we selected the SELECT policy command clause, pun intended