惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Authentication in Ionic Angular with Supabase
Simon Grimm · 2022-11-08 · via Supabase Blog

Authentication in Ionic Angular with Supabase

Authentication of your apps is one of the most important topics, and by combining Angular logic with Supabase authentication functionality we can build powerful and secure applications in no time.

In this tutorial, we will create an Ionic Angular application with Supabase backend to build a simple realtime messaging app.

Along the way we will dive into:

  • User registration and login with email/password
  • Adding Row Level Security to protect our database
  • Angular guards and token handling logic
  • Magic link authentication for both web and native mobile apps

After going through this tutorial you will be able to create your own user authentication and secure your Ionic Angular app.

If you are not yet familiar with Ionic you can check out the Ionic Quickstart guide of the Ionic Academy or check out the Ionic Supabase integration video if you prefer video.

However, most of the logic is Angular based and therefore applies to any Angular web project as well.

You can find the full code of this tutorial on GitHub where you just need to insert your own Supabase instance and then create the tables with the included SQL file.

But before we dive into the app, let's set up our Supabase project!

First of all, we need a new Supabase project. If you don't have a Supabase account yet, you can get started for free!

In your dashboard, click "New Project" and leave it to the default settings, but make sure you keep a copy of your database password!

By default Supabase has the standard email/password authentication enabled, which you can find under the menu element Authentication and by scrolling down to the Auth Provider section.

Need to add another provider in the future? No problem!

Supabase offers a ton of providers that you can easily integrate so your users can sign up with their favorite provider.

On top of that, you can customize the auth settings and also the email templates that users see when they need to confirm their account, get a magic link or want to reset their password.

Feel free to play around with the settings, and once you're done let's continue with our database.

Supabase uses Postgres for the database, so we need to write some SQL to define our tables upfront (although you can easily change them later through the Supabase Web UI as well!)

We want to build a simple messaging app, so what we need is:

  • users: A table to keep track of all registered users
  • groups: Keep track of user-created chat groups
  • messages: All messages of our app

To create the tables, simply navigate to the SQL Editor menu item and click on + New query, paste in the SQL and hit RUN which hopefully executes without issues:


_19

create table users (

_19

id uuid not null primary key,

_19

email text

_19

);

_19

_19

create table groups (

_19

id bigint generated by default as identity primary key,

_19

creator uuid references public.users not null default auth.uid(),

_19

title text not null,

_19

created_at timestamp with time zone default timezone('utc'::text, now()) not null

_19

);

_19

_19

create table messages (

_19

id bigint generated by default as identity primary key,

_19

user_id uuid references public.users not null default auth.uid(),

_19

text text check (char_length(text) > 0),

_19

group_id bigint references groups on delete cascade not null,

_19

created_at timestamp with time zone default timezone('utc'::text, now()) not null

_19

);


After creating the tables we need to define policies to prevent unauthorized access to some of our data.

In this scenario we allow unauthenticated users to read group data - everything else like creating a group, or anything related to messages is only allowed for authenticated users.

Go ahead and also run the following query in the editor:


_25

-- Secure tables

_25

alter table users enable row level security;

_25

alter table groups enable row level security;

_25

alter table messages enable row level security;

_25

_25

-- User Policies

_25

create policy "Users can read the user email." on users

_25

for select using (true);

_25

_25

-- Group Policies

_25

create policy "Groups are viewable by everyone." on groups

_25

for select using (true);

_25

_25

create policy "Authenticated users can create groups." on groups for

_25

insert to authenticated with check (true);

_25

_25

create policy "The owner can delete a group." on groups for

_25

delete using ((select auth.uid()) = creator);

_25

_25

-- Message Policies

_25

create policy "Authenticated users can read messages." on messages

_25

for select to authenticated using (true);

_25

_25

create policy "Authenticated users can create messages." on messages

_25

for insert to authenticated with check (true);


Now we also add a cool function that will automatically add user data after registration into our table. This is necessary if you want to keep track of some user information, because the Supabase auth table is an internal table that we can't access that easily.

Go ahead and run another SQL query in the editor now to create our trigger:


_13

-- Function for handling new users

_13

create or replace function public.handle_new_user()

_13

returns trigger as $$

_13

begin

_13

insert into public.users (id, email)

_13

values (new.id, new.email);

_13

return new;

_13

end;

_13

$$ language plpgsql security definer;

_13

_13

create trigger on_auth_user_created

_13

after insert on auth.users

_13

for each row execute procedure public.handle_new_user();


To wrap this up we want to enable realtime functionality of our database so we can get new messages instantly without another query.

For this we can activate the publication for our messages table by running one last query:


_10

begin;

_10

-- remove the supabase_realtime publication

_10

drop publication if exists supabase_realtime;

_10

_10

-- re-create the supabase_realtime publication with no tables and only for insert

_10

create publication supabase_realtime with (publish = 'insert');

_10

commit;

_10

_10

-- add a table to the publication

_10

alter publication supabase_realtime add table messages;


If you now open the Table Editor menu item you should see your three tables, and you can also check their RLS policies right from the web!

But enough SQL for today, let's write some Angular code.

To get started with our Ionic app we can create a blank app without any additional pages and then install the Supabase Javascript client.

Besides that, we need some pages in our app for the different views, and services to keep our logic separated from the views. Finally we can also already generate a guard which we will use to protect internal pages later.

Go ahead now and run the following on your command line:


_15

ionic start supaAuth blank --type=angular

_15

npm install @supabase/supabase-js

_15

_15

# Add some pages

_15

ionic g page pages/login

_15

ionic g page pages/register

_15

ionic g page pages/groups

_15

ionic g page pages/messages

_15

_15

# Generate services

_15

ionic g service services/auth

_15

ionic g service services/data

_15

_15

# Add a guard to protect routes

_15

ionic g guard guards/auth --implements=CanActivate


Ionic (or the Angular CLI under the hood) will now create the routing entries for us, but we gonna fine tune them a bit.

First of all we want to pass a groupid to our messages page, and we also want to make sure that page is protected by the guard we created before.

Therefore bring up the src/app/app-routing.module.ts and change it to:


_36

import { AuthGuard } from './guards/auth.guard'

_36

import { NgModule } from '@angular/core'

_36

import { PreloadAllModules, RouterModule, Routes } from '@angular/router'

_36

_36

const routes: Routes = [

_36

{

_36

path: '',

_36

loadChildren: () => import('./pages/login/login.module').then((m) => m.LoginPageModule),

_36

},

_36

{

_36

path: 'register',

_36

loadChildren: () =>

_36

import('./pages/register/register.module').then((m) => m.RegisterPageModule),

_36

},

_36

{

_36

path: 'groups',

_36

loadChildren: () => import('./pages/groups/groups.module').then((m) => m.GroupsPageModule),

_36

},

_36

{

_36

path: 'groups/:groupid',

_36

loadChildren: () =>

_36

import('./pages/messages/messages.module').then((m) => m.MessagesPageModule),

_36

canActivate: [AuthGuard],

_36

},

_36

{

_36

path: '',

_36

redirectTo: 'home',

_36

pathMatch: 'full',

_36

},

_36

]

_36

_36

@NgModule({

_36

imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],

_36

exports: [RouterModule],

_36

})

_36

export class AppRoutingModule {}


Now all paths are correct and the app starts on the login page, and the messages page can only be activated if that guard returns true - which it does by default, so we will take care of its implementation later.

To connect our app properly to Supabase we now need to grab the project URL and the public anon key from the settings page of your Supabase project.

You can find those values in your Supabase project by clicking on the Settings icon and then navigating to API where it shows your Project API keys.

This information now goes straight into the src/environments/environment.ts of our Ionic project:


_10

export const environment = {

_10

production: false,

_10

supabaseUrl: 'https://YOUR-APP.supabase.co',

_10

supabaseKey: 'YOUR-ANON-KEY',

_10

}


By the way: The anon key is safe to use in a frontend project since we have enabled RLS on our database tables!

Building the Public Pages of our App#

The big first step is to create the "outside" pages which allow a user to perform different operations:

Before we dive into the UI of these pages we should define all the required logic in a service to easily inject it into our pages.

Preparing our Supabase authentication service#

Our service should call expose all the functions for registration and login but also handle the state of the current user with a BehaviorSubject so we can easily emit new values later when the user session changes.

We are also loading the session once "by hand" using getUser() since the onAuthStateChange event is usually not broadcasted when the app loads, and we want to load a stored session right when the app starts.

The relevant functions for our user authentication are all part of the supabase.auth object, which makes it easy to find all relevant (and even some unknown) features.

Additionally, we expose our current user as an Observable to the outside and add some helper functions to get the current user ID synchronously.

Now move on by changing the src/app/services/auth.service.ts to this:


_79

/* eslint-disable @typescript-eslint/naming-convention */

_79

import { Injectable } from '@angular/core'

_79

import { Router } from '@angular/router'

_79

import { isPlatform } from '@ionic/angular'

_79

import { createClient, SupabaseClient, User } from '@supabase/supabase-js'

_79

import { BehaviorSubject, Observable } from 'rxjs'

_79

import { environment } from '../../environments/environment'

_79

_79

@Injectable({

_79

providedIn: 'root',

_79

})

_79

export class AuthService {

_79

private supabase: SupabaseClient

_79

private currentUser: BehaviorSubject<User | boolean> = new BehaviorSubject(null)

_79

_79

constructor(private router: Router) {

_79

this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)

_79

_79

this.supabase.auth.onAuthStateChange((event, sess) => {

_79

if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {

_79

console.log('SET USER')

_79

_79

this.currentUser.next(sess.user)

_79

} else {

_79

this.currentUser.next(false)

_79

}

_79

})

_79

_79

// Trigger initial session load

_79

this.loadUser()

_79

}

_79

_79

async loadUser() {

_79

if (this.currentUser.value) {

_79

// User is already set, no need to do anything else

_79

return

_79

}

_79

const user = await this.supabase.auth.getUser()

_79

_79

if (user.data.user) {

_79

this.currentUser.next(user.data.user)

_79

} else {

_79

this.currentUser.next(false)

_79

}

_79

}

_79

_79

signUp(credentials: { email; password }) {

_79

return this.supabase.auth.signUp(credentials)

_79

}

_79

_79

signIn(credentials: { email; password }) {

_79

return this.supabase.auth.signInWithPassword(credentials)

_79

}

_79

_79

sendPwReset(email) {

_79

return this.supabase.auth.resetPasswordForEmail(email)

_79

}

_79

_79

async signOut() {

_79

await this.supabase.auth.signOut()

_79

this.router.navigateByUrl('/', { replaceUrl: true })

_79

}

_79

_79

getCurrentUser(): Observable<User | boolean> {

_79

return this.currentUser.asObservable()

_79

}

_79

_79

getCurrentUserId(): string {

_79

if (this.currentUser.value) {

_79

return (this.currentUser.value as User).id

_79

} else {

_79

return null

_79

}

_79

}

_79

_79

signInWithEmail(email: string) {

_79

return this.supabase.auth.signInWithOtp({ email })

_79

}

_79

}


That's enough logic for our pages, so let's put that code to use.

Creating the Login Page#

Although we first need to register a user, we begin with the login page. We can even "register" a user from here since we will offer the easiest sign-in option with magic link authentication that only requires an email, and a user entry will be added to our users table thanks to our trigger function.

To create a decent UX we will add a reactive form with Angular, for which we first need to import the ReactiveFormsModule into the src/app/pages/login/login.module.ts:


_15

import { NgModule } from '@angular/core'

_15

import { CommonModule } from '@angular/common'

_15

import { FormsModule, ReactiveFormsModule } from '@angular/forms'

_15

_15

import { IonicModule } from '@ionic/angular'

_15

_15

import { LoginPageRoutingModule } from './login-routing.module'

_15

_15

import { LoginPage } from './login.page'

_15

_15

@NgModule({

_15

imports: [CommonModule, FormsModule, IonicModule, LoginPageRoutingModule, ReactiveFormsModule],

_15

declarations: [LoginPage],

_15

})

_15

export class LoginPageModule {}


Now we can define the form from code and add all required functions to our page, which becomes super easy thanks to our previous implementation of the service.

Right inside the constructor of the login page, we will also subscribe to the getCurrentUser() Observable and if we do have a valid user token, we can directly route the user forward to the groups overview page.

On login, we now only need to show some loading spinner and call the according function of our service, and since we already listen to the user in our constructor we don't even need to add any more logic for routing at this point and only show an alert in case something goes wrong.

Go ahead by changing the src/app/pages/login/login.page.ts to this now:


_61

import { AuthService } from './../../services/auth.service'

_61

import { Component } from '@angular/core'

_61

import { FormBuilder, Validators } from '@angular/forms'

_61

import { Router } from '@angular/router'

_61

import { LoadingController, AlertController } from '@ionic/angular'

_61

_61

@Component({

_61

selector: 'app-login',

_61

templateUrl: './login.page.html',

_61

styleUrls: ['./login.page.scss'],

_61

})

_61

export class LoginPage {

_61

credentials = this.fb.nonNullable.group({

_61

email: ['', Validators.required],

_61

password: ['', Validators.required],

_61

})

_61

_61

constructor(

_61

private fb: FormBuilder,

_61

private authService: AuthService,

_61

private loadingController: LoadingController,

_61

private alertController: AlertController,

_61

private router: Router

_61

) {

_61

this.authService.getCurrentUser().subscribe((user) => {

_61

if (user) {

_61

this.router.navigateByUrl('/groups', { replaceUrl: true })

_61

}

_61

})

_61

}

_61

_61

get email() {

_61

return this.credentials.controls.email

_61

}

_61

_61

get password() {

_61

return this.credentials.controls.password

_61

}

_61

_61

async login() {

_61

const loading = await this.loadingController.create()

_61

await loading.present()

_61

_61

this.authService.signIn(this.credentials.getRawValue()).then(async (data) => {

_61

await loading.dismiss()

_61

_61

if (data.error) {

_61

this.showAlert('Login failed', data.error.message)

_61

}

_61

})

_61

}

_61

_61

async showAlert(title, msg) {

_61

const alert = await this.alertController.create({

_61

header: title,

_61

message: msg,

_61

buttons: ['OK'],

_61

})

_61

await alert.present()

_61

}

_61

}


Additionally, we need a function to reset the password and trigger the magic link authentication.

In both cases, we can use an Ionic alert with one input field. This field can be accessed inside the handler of a button, and so we pass the value to the according function of our service and show another message after successfully submitting the request.

Go ahead with our login page and now also add these two functions:


_79

async forgotPw() {

_79

const alert = await this.alertController.create({

_79

header: "Receive a new password",

_79

message: "Please insert your email",

_79

inputs: [

_79

{

_79

type: "email",

_79

name: "email",

_79

},

_79

],

_79

buttons: [

_79

{

_79

text: "Cancel",

_79

role: "cancel",

_79

},

_79

{

_79

text: "Reset password",

_79

handler: async (result) => {

_79

const loading = await this.loadingController.create();

_79

await loading.present();

_79

const { data, error } = await this.authService.sendPwReset(

_79

result.email

_79

);

_79

await loading.dismiss();

_79

_79

if (error) {

_79

this.showAlert("Failed", error.message);

_79

} else {

_79

this.showAlert(

_79

"Success",

_79

"Please check your emails for further instructions!"

_79

);

_79

}

_79

},

_79

},

_79

],

_79

});

_79

await alert.present();

_79

}

_79

_79

async getMagicLink() {

_79

const alert = await this.alertController.create({

_79

header: "Get a Magic Link",

_79

message: "We will send you a link to magically log in!",

_79

inputs: [

_79

{

_79

type: "email",

_79

name: "email",

_79

},

_79

],

_79

buttons: [

_79

{

_79

text: "Cancel",

_79

role: "cancel",

_79

},

_79

{

_79

text: "Get Magic Link",

_79

handler: async (result) => {

_79

const loading = await this.loadingController.create();

_79

await loading.present();

_79

const { data, error } = await this.authService.signInWithEmail(

_79

result.email

_79

);

_79

await loading.dismiss();

_79

_79

if (error) {

_79

this.showAlert("Failed", error.message);

_79

} else {

_79

this.showAlert(

_79

"Success",

_79

"Please check your emails for further instructions!"

_79

);

_79

}

_79

},

_79

},

_79

],

_79

});

_79

await alert.present();

_79

}


That's enough to handle everything, so now we just need a simple UI for our form and buttons.

Since recent Ionic versions, we can use the new error slot of the Ionic item, which we can use to present specific error messages in case one field of our reactive form is invalid.

We can easily access the email and password control since we exposed them with their own get function in our class before!

Below the form, we simply stack all of our buttons to trigger the actions and give them different colors.

Bring up the src/app/pages/login/login.page.html now and change it to:


_63

<ion-header>

_63

<ion-toolbar color="primary">

_63

<ion-title>Supa Chat</ion-title>

_63

</ion-toolbar>

_63

</ion-header>

_63

_63

<ion-content scrollY="false">

_63

<ion-card>

_63

<ion-card-content>

_63

<form (ngSubmit)="login()" [formGroup]="credentials">

_63

<ion-item>

_63

<ion-label position="stacked">Your Email</ion-label>

_63

<ion-input

_63

type="email"

_63

inputmode="email"

_63

placeholder="Email"

_63

formControlName="email"

_63

></ion-input>

_63

<ion-note slot="error" *ngIf="(email.dirty || email.touched) && email.errors"

_63

>Please insert your email</ion-note

_63

>

_63

</ion-item>

_63

<ion-item>

_63

<ion-label position="stacked">Password</ion-label>

_63

<ion-input type="password" placeholder="Password" formControlName="password"></ion-input>

_63

<ion-note slot="error" *ngIf="(password.dirty || password.touched) && password.errors"

_63

>Please insert your password</ion-note

_63

>

_63

</ion-item>

_63

<ion-button type="submit" expand="block" strong="true" [disabled]="!credentials.valid"

_63

>Sign in</ion-button

_63

>

_63

_63

<div class="ion-margin-top">

_63

<ion-button

_63

type="button"

_63

expand="block"

_63

color="primary"

_63

fill="outline"

_63

routerLink="register"

_63

>

_63

<ion-icon name="person-outline" slot="start"></ion-icon>

_63

Create Account

_63

</ion-button>

_63

_63

<ion-button type="button" expand="block" color="secondary" (click)="forgotPw()">

_63

<ion-icon name="key-outline" slot="start"></ion-icon>

_63

Forgot password?

_63

</ion-button>

_63

_63

<ion-button type="button" expand="block" color="tertiary" (click)="getMagicLink()">

_63

<ion-icon name="mail-outline" slot="start"></ion-icon>

_63

Get a Magic Link

_63

</ion-button>

_63

<ion-button type="button" expand="block" color="warning" routerLink="groups">

_63

<ion-icon name="arrow-forward" slot="start"></ion-icon>

_63

Start without account

_63

</ion-button>

_63

</div>

_63

</form>

_63

</ion-card-content>

_63

</ion-card>

_63

</ion-content>


To give our login a bit nicer touch, let's also add a background image and some additional padding by adding the following to the src/app/pages/login/login.page.scss:


_10

ion-content {

_10

--padding-top: 20%;

_10

--padding-start: 5%;

_10

--padding-end: 5%;

_10

--background:

_10

linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.7)),

_10

url('https://images.unsplash.com/photo-1508964942454-1a56651d54ac?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1035&q=80')

_10

no-repeat;

_10

}


At this point you can already try our authentication by using the magic link button, so run the Ionic app with ionic serve for the web preview and then enter your email.

Make sure you use a valid email since you do need to click on the link in the email. If everything works correctly you should receive an email like this quickly:

Keep in mind that you can easily change those email templates inside the settings of your Supabase project.

If you now inspect the link and copy the URL, you should see an URL that points to your Supabase project and after some tokens there is a query param &redirect_to=http://localhost:8100 which directly brings a user back into our local running app!

This will be even more important later when we implement magic link authentication for native apps so stick around until the end.

Creating the Registration Page#

Some users will still prefer the good old registration, so let's provide them with a decent page for that.

The setup is almost the same as for the login, so we start again by adding the ReactiveFormsModule to the src/app/pages/register/register.module.ts:


_15

import { NgModule } from '@angular/core'

_15

import { CommonModule } from '@angular/common'

_15

import { FormsModule, ReactiveFormsModule } from '@angular/forms'

_15

_15

import { IonicModule } from '@ionic/angular'

_15

_15

import { RegisterPageRoutingModule } from './register-routing.module'

_15

_15

import { RegisterPage } from './register.page'

_15

_15

@NgModule({

_15

imports: [CommonModule, FormsModule, IonicModule, RegisterPageRoutingModule, ReactiveFormsModule],

_15

declarations: [RegisterPage],

_15

})

_15

export class RegisterPageModule {}


Now we define our form just like before, and in the createAccount() we use the functionality of our initially created service to sign up a user.

Bring up the src/app/pages/register/register.page.ts and change it to:


_57

import { Component } from '@angular/core'

_57

import { Validators, FormBuilder } from '@angular/forms'

_57

import { LoadingController, AlertController, NavController } from '@ionic/angular'

_57

import { AuthService } from 'src/app/services/auth.service'

_57

_57

@Component({

_57

selector: 'app-register',

_57

templateUrl: './register.page.html',

_57

styleUrls: ['./register.page.scss'],

_57

})

_57

export class RegisterPage {

_57

credentials = this.fb.nonNullable.group({

_57

email: ['', [Validators.required, Validators.email]],

_57

password: ['', [Validators.required, Validators.minLength(6)]],

_57

})

_57

_57

constructor(

_57

private fb: FormBuilder,

_57

private authService: AuthService,

_57

private loadingController: LoadingController,

_57

private alertController: AlertController,

_57

private navCtrl: NavController

_57

) {}

_57

_57

get email() {

_57

return this.credentials.controls.email

_57

}

_57

_57

get password() {

_57

return this.credentials.controls.password

_57

}

_57

_57

async createAccount() {

_57

const loading = await this.loadingController.create()

_57

await loading.present()

_57

_57

this.authService.signUp(this.credentials.getRawValue()).then(async (data) => {

_57

await loading.dismiss()

_57

_57

if (data.error) {

_57

this.showAlert('Registration failed', data.error.message)

_57

} else {

_57

this.showAlert('Signup success', 'Please confirm your email now!')

_57

this.navCtrl.navigateBack('')

_57

}

_57

})

_57

}

_57

_57

async showAlert(title, msg) {

_57

const alert = await this.alertController.create({

_57

header: title,

_57

message: msg,

_57

buttons: ['OK'],

_57

})

_57

await alert.present()

_57

}

_57

}


The view for that page follows the same structure as the login, so let's continue with the src/app/pages/register/register.page.html now:


_46

<ion-header>

_46

<ion-toolbar color="primary">

_46

<ion-buttons slot="start">

_46

<ion-back-button defaultHref="/"></ion-back-button>

_46

</ion-buttons>

_46

<ion-title>Supa Chat</ion-title>

_46

</ion-toolbar>

_46

</ion-header>

_46

_46

<ion-content scrollY="false">

_46

<ion-card>

_46

<ion-card-content>

_46

<form (ngSubmit)="createAccount()" [formGroup]="credentials">

_46

<ion-item>

_46

<ion-label position="stacked">Your Email</ion-label>

_46

<ion-input

_46

type="email"

_46

inputmode="email"

_46

placeholder="Email"

_46

formControlName="email"

_46

></ion-input>

_46

<ion-note slot="error" *ngIf="(email.dirty || email.touched) && email.errors"

_46

>Please insert a valid email</ion-note

_46

>

_46

</ion-item>

_46

<ion-item>

_46

<ion-label position="stacked">Password</ion-label>

_46

<ion-input type="password" placeholder="Password" formControlName="password"></ion-input>

_46

<ion-note

_46

slot="error"

_46

*ngIf="(password.dirty || password.touched) && password.errors?.required"

_46

>Please insert a password</ion-note

_46

>

_46

<ion-note

_46

slot="error"

_46

*ngIf="(password.dirty || password.touched) && password.errors?.minlength"

_46

>Minlength 6 characters</ion-note

_46

>

_46

</ion-item>

_46

<ion-button type="submit" expand="block" strong="true" [disabled]="!credentials.valid"

_46

>Create my account</ion-button

_46

>

_46

</form>

_46

</ion-card-content>

_46

</ion-card>

_46

</ion-content>


And just like before we want to have the background image so also bring in the same snippet for styling the page into the src/app/pages/register/register.page.scss:


_10

ion-content {

_10

--padding-top: 20%;

_10

--padding-start: 5%;

_10

--padding-end: 5%;

_10

--background:

_10

linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.7)),

_10

url('https://images.unsplash.com/photo-1508964942454-1a56651d54ac?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1035&q=80')

_10

no-repeat;

_10

}


As a result, we have a clean registration page with decent error messages!

Give the default registration process a try with another email, and you should see another user inside the Authentication area of your Supabase project as well as inside the users table inside the Table Editor.

Before we make the magic link authentication work on mobile devices, let's focus on the internal pages and functionality of our app.

Implementing the Groups Page#

The groups screen is the first "inside" screen, but it's not protected by default: On the login page we have the option to start without an account, so unauthorized users can enter this page - but they should only be allowed to see the chat groups, nothing more.

Authenticated users should have controls to create a new group and to sign out, but before we get to the UI we need a way to interact with our Supabase tables.

Adding a Data Service#

We already generated a service in the beginning, and here we can add the logic to create a connection to Supabase and a first function to create a new row in our groups table and to load all groups.

Creating a group requires just a title, and we can gather the user ID from our authentication service to then call the insert() function from the Supabase client to create a new record that we then return to the caller.

When we want to get a list of groups, we can use select() but since we have a foreign key that references the users table, we need to join that information so instead of just having the creator field we end up getting the actual email for that ID instead!

Go ahead now and start the src/app/services/data.service.ts like this:


_43

/* eslint-disable @typescript-eslint/naming-convention */

_43

import { Injectable } from '@angular/core'

_43

import { SupabaseClient, createClient } from '@supabase/supabase-js'

_43

import { Subject } from 'rxjs'

_43

import { environment } from 'src/environments/environment'

_43

_43

const GROUPS_DB = 'groups'

_43

const MESSAGES_DB = 'messages'

_43

_43

export interface Message {

_43

created_at: string

_43

group_id: number

_43

id: number

_43

text: string

_43

user_id: string

_43

}

_43

_43

@Injectable({

_43

providedIn: 'root',

_43

})

_43

export class DataService {

_43

private supabase: SupabaseClient

_43

_43

constructor() {

_43

this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)

_43

}

_43

_43

getGroups() {

_43

return this.supabase

_43

.from(GROUPS_DB)

_43

.select(`title,id, users:creator ( email )`)

_43

.then((result) => result.data)

_43

}

_43

_43

async createGroup(title) {

_43

const newgroup = {

_43

creator: (await this.supabase.auth.getUser()).data.user.id,

_43

title,

_43

}

_43

_43

return this.supabase.from(GROUPS_DB).insert(newgroup).select().single()

_43

}

_43

}


Nothing fancy, so let's move on to the UI of the groups page.

Creating the Groups Page#

When we enter our page, we first load all groups through our service using the ionViewWillEnter Ionic lifecycle event.

The function to create a group follows the same logic as our alerts before where we have one input field that can be accessed in the handler of a button.

At that point, we will create a new group with the information, but also reload our list of groups and then navigate a user directly into the new group by using the ID of that created record.

This will then bring a user to the messages page since we defined the route "/groups/:groupid" initially in our routing!

Now go ahead and bring up the src/app/pages/groups/groups.page.ts and change it to:


_75

import { Router } from '@angular/router'

_75

import { AuthService } from './../../services/auth.service'

_75

import { AlertController, NavController, LoadingController } from '@ionic/angular'

_75

import { DataService } from './../../services/data.service'

_75

import { Component, OnInit } from '@angular/core'

_75

_75

@Component({

_75

selector: 'app-groups',

_75

templateUrl: './groups.page.html',

_75

styleUrls: ['./groups.page.scss'],

_75

})

_75

export class GroupsPage implements OnInit {

_75

user = this.authService.getCurrentUser()

_75

groups = []

_75

_75

constructor(

_75

private authService: AuthService,

_75

private data: DataService,

_75

private alertController: AlertController,

_75

private loadingController: LoadingController,

_75

private navController: NavController,

_75

private router: Router

_75

) {}

_75

_75

ngOnInit() {}

_75

_75

async ionViewWillEnter() {

_75

this.groups = await this.data.getGroups()

_75

}

_75

_75

async createGroup() {

_75

const alert = await this.alertController.create({

_75

header: 'Start Chat Group',

_75

message: 'Enter a name for your group. Note that all groups are public in this app!',

_75

inputs: [

_75

{

_75

type: 'text',

_75

name: 'title',

_75

placeholder: 'My cool group',

_75

},

_75

],

_75

buttons: [

_75

{

_75

text: 'Cancel',

_75

role: 'cancel',

_75

},

_75

{

_75

text: 'Create group',

_75

handler: async (data) => {

_75

const loading = await this.loadingController.create()

_75

await loading.present()

_75

_75

const newGroup = await this.data.createGroup(data.title)

_75

if (newGroup) {

_75

this.groups = await this.data.getGroups()

_75

await loading.dismiss()

_75

_75

this.router.navigateByUrl(`/groups/${newGroup.data.id}`)

_75

}

_75

},

_75

},

_75

],

_75

})

_75

_75

await alert.present()

_75

}

_75

_75

signOut() {

_75

this.authService.signOut()

_75

}

_75

_75

openLogin() {

_75

this.navController.navigateBack('/')

_75

}

_75

}


The UI of that page is rather simple since we can iterate those groups in a list and create an item with their title and the creator email easily.

Because unauthenticated users can enter this page as well we add checks to the user Observable to the buttons so only authenticated users see the FAB at the bottom and have the option to sign out!

Remember that protecting the UI of our page is just one piece of the puzzle, real security is implemented at the server level!

In our case, we did this through the RLS we defined in the beginning.

Continue with the src/app/pages/groups/groups.page.html now and change it to:


_28

<ion-header>

_28

<ion-toolbar color="primary">

_28

<ion-title>Supa Chat Groups</ion-title>

_28

<ion-buttons slot="end">

_28

<ion-button (click)="signOut()" *ngIf="user | async">

_28

<ion-icon name="log-out-outline" slot="icon-only"></ion-icon>

_28

</ion-button>

_28

_28

<ion-button (click)="openLogin()" *ngIf="(user | async) === false"> Sign in </ion-button>

_28

</ion-buttons>

_28

</ion-toolbar>

_28

</ion-header>

_28

_28

<ion-content>

_28

<ion-list>

_28

<ion-item *ngFor="let group of groups" [routerLink]="[group.id]" button>

_28

<ion-label

_28

>{{group.title }}

_28

<p>By {{group.users.email}}</p>

_28

</ion-label>

_28

</ion-item>

_28

</ion-list>

_28

<ion-fab vertical="bottom" horizontal="end" slot="fixed" *ngIf="user | async">

_28

<ion-fab-button (click)="createGroup()">

_28

<ion-icon name="add"></ion-icon>

_28

</ion-fab-button>

_28

</ion-fab>

_28

</ion-content>


At this point, you should be able to create your own chat groups, and you should be brought to the page automatically or also enter it from the list manually afterward.

Inside the ULR you should see the ID of that group - and that's all we need to retrieve information about it and build a powerful chat view now!

Building the Chat Page with Realtime Feature#

We left out realtime features on the groups page so we manually need to load the lists again, but only to keep the tutorial a bit shorter.

Because now on our messages page we want to have that functionality, and because we enabled the publication for the messages table through SQL in the beginning we are already prepared.

To get started we need some more functions in our service, and we add another realtimeChannel variable.

Additionally, we now want to retrieve group information by ID (from the URL!), add messages to the messages table and retrieve the last 25 messages.

All of that is pretty straightforward, and the only fancy function is listenToGroup() which returns an Observable of changes.

We can create this on our own by handling postgres_changes events on the messages table. Inside the callback function, we can handle all CRUD events, but in our case, we will (for simplicity) only handle the case of an added record.

That means when a message is added to the table, we want to return that new record to whoever is subscribed to this channel - but because a message has the user as a foreign key we first need to make another call to the messages table to retrieve the right information and then emit the new value on our Subject.

For all of that bring up the src/app/services/data.service.ts and change it to:


_109

/* eslint-disable @typescript-eslint/naming-convention */

_109

import { Injectable } from '@angular/core'

_109

import { SupabaseClient, createClient, RealtimeChannel } from '@supabase/supabase-js'

_109

import { Subject } from 'rxjs'

_109

import { environment } from 'src/environments/environment'

_109

_109

const GROUPS_DB = 'groups'

_109

const MESSAGES_DB = 'messages'

_109

_109

export interface Message {

_109

created_at: string

_109

group_id: number

_109

id: number

_109

text: string

_109

user_id: string

_109

}

_109

_109

@Injectable({

_109

providedIn: 'root',

_109

})

_109

export class DataService {

_109

private supabase: SupabaseClient

_109

// ADD

_109

private realtimeChannel: RealtimeChannel

_109

_109

constructor() {

_109

this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)

_109

}

_109

_109

getGroups() {

_109

return this.supabase

_109

.from(GROUPS_DB)

_109

.select(`title,id, users:creator ( email )`)

_109

.then((result) => result.data)

_109

}

_109

_109

async createGroup(title) {

_109

const newgroup = {

_109

creator: (await this.supabase.auth.getUser()).data.user.id,

_109

title,

_109

}

_109

_109

return this.supabase.from(GROUPS_DB).insert(newgroup).select().single()

_109

}

_109

_109

// ADD NEW FUNCTIONS

_109

getGroupById(id) {

_109

return this.supabase

_109

.from(GROUPS_DB)

_109

.select(`created_at, title, id, users:creator ( email, id )`)

_109

.match({ id })

_109

.single()

_109

.then((result) => result.data)

_109

}

_109

_109

async addGroupMessage(groupId, message) {

_109

const newMessage = {

_109

text: message,

_109

user_id: (await this.supabase.auth.getUser()).data.user.id,

_109

group_id: groupId,

_109

}

_109

_109

return this.supabase.from(MESSAGES_DB).insert(newMessage)

_109

}

_109

_109

getGroupMessages(groupId) {

_109

return this.supabase

_109

.from(MESSAGES_DB)

_109

.select(`created_at, text, id, users:user_id ( email, id )`)

_109

.match({ group_id: groupId })

_109

.limit(25) // Limit to 25 messages for our app

_109

.then((result) => result.data)

_109

}

_109

_109

listenToGroup(groupId) {

_109

const changes = new Subject()

_109

_109

this.realtimeChannel = this.supabase

_109

.channel('public:messages')

_109

.on(

_109

'postgres_changes',

_109

{ event: '*', schema: 'public', table: 'messages' },

_109

async (payload) => {

_109

console.log('DB CHANGE: ', payload)

_109

_109

if (payload.new && (payload.new as Message).group_id === +groupId) {

_109

const msgId = (payload.new as any).id

_109

_109

const msg = await this.supabase

_109

.from(MESSAGES_DB)

_109

.select(`created_at, text, id, users:user_id ( email, id )`)

_109

.match({ id: msgId })

_109

.single()

_109

.then((result) => result.data)

_109

changes.next(msg)

_109

}

_109

}

_109

)

_109

.subscribe()

_109

_109

return changes.asObservable()

_109

}

_109

_109

unsubscribeGroupChanges() {

_109

if (this.realtimeChannel) {

_109

this.supabase.removeChannel(this.realtimeChannel)

_109

}

_109

}

_109

}


By handling the realtime logic here and only returning an Observable we make it super easy for our view.

The next step is to load the group information by accessing the groupid from the URL, then getting the last 25 messages and finally subscribing to listenToGroup() and pushing every new message into our local messages array.

After the view is initialized we can also scroll to the bottom of our ion-content to show the latest message.

Finally, we need to make sure we end our realtime listening when we leave the page or the page is destroyed.

Bring up the src/app/pages/messages/messages.page.ts and change it to:


_54

import { AuthService } from './../../services/auth.service'

_54

import { DataService } from './../../services/data.service'

_54

import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'

_54

import { ActivatedRoute } from '@angular/router'

_54

import { IonContent } from '@ionic/angular'

_54

_54

@Component({

_54

selector: 'app-messages',

_54

templateUrl: './messages.page.html',

_54

styleUrls: ['./messages.page.scss'],

_54

})

_54

export class MessagesPage implements OnInit, AfterViewInit, OnDestroy {

_54

@ViewChild(IonContent) content: IonContent

_54

group = null

_54

messages = []

_54

currentUserId = null

_54

messageText = ''

_54

_54

constructor(

_54

private route: ActivatedRoute,

_54

private data: DataService,

_54

private authService: AuthService

_54

) {}

_54

_54

async ngOnInit() {

_54

const groupid = this.route.snapshot.paramMap.get('groupid')

_54

this.group = await this.data.getGroupById(groupid)

_54

this.currentUserId = this.authService.getCurrentUserId()

_54

this.messages = await this.data.getGroupMessages(groupid)

_54

this.data.listenToGroup(groupid).subscribe((msg) => {

_54

this.messages.push(msg)

_54

setTimeout(() => {

_54

this.content.scrollToBottom(200)

_54

}, 100)

_54

})

_54

}

_54

_54

ngAfterViewInit(): void {

_54

setTimeout(() => {

_54

this.content.scrollToBottom(200)

_54

}, 300)

_54

}

_54

_54

loadMessages() {}

_54

_54

async sendMessage() {

_54

await this.data.addGroupMessage(this.group.id, this.messageText)

_54

this.messageText = ''

_54

}

_54

_54

ngOnDestroy(): void {

_54

this.data.unsubscribeGroupChanges()

_54

}

_54

}


That's all we need to handle realtime logic and add new messages to our Supabase table!

Sometimes life can be that easy.

For the view of that page, we need to distinguish between messages we sent, and messages sent from other users.

We can achieve this by comparing the user ID of a message with our currently authenticated user id, and we position our messages with an offset of 2 so they appear on the right hand side of the screen with a slightly different styling.

For this, open up the src/app/pages/messages/messages.page.html and change it to:


_48

<ion-header>

_48

<ion-toolbar color="primary">

_48

<ion-buttons slot="start">

_48

<ion-back-button defaultHref="/groups"></ion-back-button>

_48

</ion-buttons>

_48

<ion-title>{{ group?.title}}</ion-title>

_48

</ion-toolbar>

_48

</ion-header>

_48

_48

<ion-content class="ion-padding">

_48

<ion-row *ngFor="let message of messages">

_48

<ion-col size="10" *ngIf="message.users.id !== currentUserId" class="message other-message">

_48

<span>{{ message.text }} </span>

_48

_48

<div class="time ion-text-right"><br />{{ message.created_at | date:'shortTime' }}</div>

_48

</ion-col>

_48

_48

<ion-col

_48

offset="2"

_48

size="10"

_48

*ngIf="message.users.id === currentUserId"

_48

class="message my-message"

_48

>

_48

<span>{{ message.text }} </span>

_48

<div class="time ion-text-right"><br />{{ message.created_at | date:'shortTime' }}</div>

_48

</ion-col>

_48

</ion-row>

_48

</ion-content>

_48

_48

<ion-footer>

_48

<ion-toolbar color="light">

_48

<ion-row class="ion-align-items-center">

_48

<ion-col size="10">

_48

<ion-textarea

_48

class="message-input"

_48

autoGrow="true"

_48

rows="1"

_48

[(ngModel)]="messageText"

_48

></ion-textarea>

_48

</ion-col>

_48

<ion-col size="2" class="ion-text-center">

_48

<ion-button fill="clear" (click)="sendMessage()">

_48

<ion-icon slot="icon-only" name="send-outline" color="primary" size="large"></ion-icon>

_48

</ion-button>

_48

</ion-col>

_48

</ion-row>

_48

</ion-toolbar>

_48

</ion-footer>


For an even more advanced chat UI chat out the examples of Built with Ionic!

Now we can add the finishing touches to that screen with some CSS to give the page a background pattern image and styling for the messages inside the src/app/pages/messages/messages.page.scss:


_44

ion-content {

_44

--background: url('../../../assets/pattern.png') no-repeat;

_44

}

_44

_44

.message-input {

_44

border: 1px solid #c3c3c3;

_44

border-radius: 20px;

_44

background: #fff;

_44

box-shadow: 2px 2px 5px 0px rgb(0 0 0 / 5%);

_44

}

_44

_44

ion-textarea {

_44

--padding-start: 20px;

_44

--padding-top: 4px;

_44

--padding-bottom: 4px;

_44

_44

min-height: 30px;

_44

}

_44

_44

.message {

_44

padding: 10px !important;

_44

border-radius: 10px !important;

_44

margin-bottom: 8px !important;

_44

_44

img {

_44

width: 100%;

_44

}

_44

}

_44

_44

.my-message {

_44

background: #dbf7c5;

_44

color: #000;

_44

}

_44

_44

.other-message {

_44

background: #fff;

_44

color: #000;

_44

}

_44

_44

.time {

_44

color: #cacaca;

_44

float: right;

_44

font-size: small;

_44

}


You can find the full code of this tutorial on GitHub including the file for that pattern so your page looks almost like WhatsApp!

Now we have a well-working Ionic app with Supabase authentication and database integration, but there are two small but important additions we still need to make.

Protecting internal Pages#

Right now everyone could access the messages page, but we wanted to make this page only available for authenticated users.

To protect the page (and all other pages you might want to protect) we now implement the guard that we generated in the beginning.

That guard will check the Observable of our service, filter out the initial state and then see if a user is allowed to access a page or not.

Bring up our src/app/guards/auth.guard.ts and change it to this:


_38

import { AuthService } from './../services/auth.service'

_38

import { Injectable } from '@angular/core'

_38

import { ActivatedRouteSnapshot, CanActivate, Router, UrlTree } from '@angular/router'

_38

import { Observable } from 'rxjs'

_38

import { filter, map, take } from 'rxjs/operators'

_38

import { ToastController } from '@ionic/angular'

_38

_38

@Injectable({

_38

providedIn: 'root',

_38

})

_38

export class AuthGuard implements CanActivate {

_38

constructor(

_38

private auth: AuthService,

_38

private router: Router,

_38

private toastController: ToastController

_38

) {}

_38

_38

canActivate(route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> {

_38

return this.auth.getCurrentUser().pipe(

_38

filter((val) => val !== null), // Filter out initial Behavior subject value

_38

take(1), // Otherwise the Observable doesn't complete!

_38

map((isAuthenticated) => {

_38

if (isAuthenticated) {

_38

return true

_38

} else {

_38

this.toastController

_38

.create({

_38

message: 'You are not allowed to access this!',

_38

duration: 2000,

_38

})

_38

.then((toast) => toast.present())

_38

_38

return this.router.createUrlTree(['/groups'])

_38

}

_38

})

_38

)

_38

}

_38

}


In case the user is not allowed to activate a page, we display a toast and at the same time route to the groups page since that page is visible to everyone. Normally you might even bring users simply back to the login screen if you wanted to protect all internal pages of your Ionic app.

We already applied this guard to our routing in the beginning, but now it finally serves the real purpose!

At last, we come to a challenging topic, which is handling the magic link on a mobile phone.

The problem is, that the link that a user receives has a callback to a URL, but if you open that link in your email client on a phone it's not opening your native app!

But we can change this by defining a custom URL scheme for our app like "supachat://", and then use that URL as the callback URL for magic link authentication.

First, make sure you add the native platforms with Capacitor to your project:


_10

ionic build

_10

ionic cap add ios

_10

ionic cap add android


Inside the new native projects we need to define the URL scheme, so for iOS bring up the ios/App/App/Info.plist and insert another block:


_10

<key>CFBundleURLTypes</key>

_10

<array>

_10

<dict>

_10

<key>CFBundleURLSchemes</key>

_10

<array>

_10

<string>supachat</string>

_10

</array>

_10

</dict>

_10

</array>


For Android, we first define a new string inside the android/app/src/main/res/values/strings.xml:


_10

<string name="custom_url_scheme">supachat</string>


Now we can update the android/app/src/main/AndroidManifest.xml and add an intent-filter inside which uses the custom_url_scheme value:


_10

<intent-filter android:autoVerify="true">

_10

<action android:name="android.intent.action.VIEW" />

_10

<category android:name="android.intent.category.DEFAULT" />

_10

<category android:name="android.intent.category.BROWSABLE" />

_10

<data android:scheme="@string/custom_url_scheme" />

_10

</intent-filter>


By default, Supabase will use the host for the redirect URL, which works great if the request comes from a website.

That means we only want to change the behavior for native apps, so we can use the isPlatform() check in our app and use "supachat://login"as the redirect URL instead.

For this, bring up the src/app/services/auth.service.ts and update our signInWithEmail and add another new function:


_14

signInWithEmail(email: string) {

_14

const redirectTo = isPlatform("capacitor")

_14

? "supachat://login"

_14

: `${window.location.origin}/groups`;

_14

_14

return this.supabase.auth.signInWithOtp({

_14

email,

_14

options: { emailRedirectTo: redirectTo },

_14

});

_14

}

_14

_14

async setSession(access_token, refresh_token) {

_14

return this.supabase.auth.setSession({ access_token, refresh_token });

_14

}


This second function is required since we need to manually set our session based on the other tokens of the magic link URL.

If we now click on the link in an email, it will open the browser the first time but then asks if we want to open our native app.

This is cool, but it's not loading the user information correctly, but we can easily do this manually!

Eventually, our app is opened with an URL that looks like this:


_10

supachat://login#access_token=A-TOKEN&expires_in=3600&refresh_token=REF-TOKEN&token_type=bearer&type=magiclink


To set our session we now simply need to extract the access_token and refresh_token from that URL, and we can do this by adding a listener to the appUrlOpen event of the Capacitor app plugin.

Once we got that information we can call the setSession function that we just added to our service, and then route the user forward to the groups page!

To achieve this, bring up the src/app/app.component.ts and change it to:


_35

import { AuthService } from 'src/app/services/auth.service'

_35

import { Router } from '@angular/router'

_35

import { Component, NgZone } from '@angular/core'

_35

import { App, URLOpenListenerEvent } from '@capacitor/app'

_35

_35

@Component({

_35

selector: 'app-root',

_35

templateUrl: 'app.component.html',

_35

styleUrls: ['app.component.scss'],

_35

})

_35

export class AppComponent {

_35

constructor(

_35

private zone: NgZone,

_35

private router: Router,

_35

private authService: AuthService

_35

) {

_35

this.setupListener()

_35

}

_35

_35

setupListener() {

_35

App.addListener('appUrlOpen', async (data: URLOpenListenerEvent) => {

_35

console.log('app opened with URL: ', data)

_35

_35

const openUrl = data.url

_35

const access = openUrl.split('#access_token=').pop().split('&')[0]

_35

const refresh = openUrl.split('&refresh_token=').pop().split('&')[0]

_35

_35

await this.authService.setSession(access, refresh)

_35

_35

this.zone.run(() => {

_35

this.router.navigateByUrl('/groups', { replaceUrl: true })

_35

})

_35

})

_35

}

_35

}


If you would run the app now it still wouldn't work, because we haven't added our custom URL scheme as an allowed URL for redirecting to!

To finish this open your Supabase project again and go to the Settings of the Authentication menu entry where you can add a domain under Redirect URLs:

This was the last missing piece, and now you even got seamless Supabase authentication with magic links working inside your iOS and Android app!

We've come a long way and covered everything from setting up tables, to defining policies to protect data, and handling authentication in Ionic Angular applications.

You can find the full code of this tutorial on GitHub where you just need to insert your own Supabase instance and then create the tables with the included SQL file, plus updating the authentication settings as we did in the tutorial

Although we can now use magic link auth, something probably even better fitting for native apps would be phone auth with Twilio that's also easily possible with Supabase - just like tons of other authentication providers!

Protecting your Ionic Angular app with Supabase is a breeze, and through the security rules, you can make sure your data and tables are protected in the best possible way.

If you enjoyed the tutorial, you can find many more tutorials on my YouTube channel where I help web developers build awesome mobile apps.

Until next time and happy coding with Supabase!