Use SupaHost with Angular
Connect an Angular app to a SupaHost project with supabase-js. Environment files, an injectable Supabase service with a session signal, a component that queries a table, sign-in and a route guard.
An Angular app runs in the browser and talks to your SupaHost project with supabase-js and the public anon key. Wrapping the client in an injectable service keeps one instance for the whole app and gives components a simple way to read the current session.
The examples use standalone components, signals and the recipes table from the quickstart.
Install
npx @angular/cli@latest new recipe-box
cd recipe-box
npm install @supabase/supabase-js
npx ng generate environments
Add your project keys
Put the Project URL and anon key from the Connect tab (subtab App) in your environment files:
// src/environments/environment.development.ts
export const environment = {
supabaseUrl: 'https://app-one.acme.supahost.space',
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIs...',
}
Add the production values to src/environments/environment.ts. Both files are compiled into the browser bundle, which is fine for the anon key and never for service_role.
Create a Supabase service
// src/app/supabase.service.ts
import { Injectable, signal } from '@angular/core'
import { createClient, type Session, type SupabaseClient } from '@supabase/supabase-js'
import { environment } from '../environments/environment'
@Injectable({ providedIn: 'root' })
export class SupabaseService {
readonly client: SupabaseClient = createClient(
environment.supabaseUrl,
environment.supabaseAnonKey
)
readonly session = signal<Session | null>(null)
constructor() {
this.client.auth.getSession().then(({ data }) => this.session.set(data.session))
this.client.auth.onAuthStateChange((_event, session) => this.session.set(session))
}
signIn(email: string, password: string) {
return this.client.auth.signInWithPassword({ email, password })
}
signUp(email: string, password: string) {
return this.client.auth.signUp({
email,
password,
options: { emailRedirectTo: window.location.origin },
})
}
signOut() {
return this.client.auth.signOut()
}
}
Query a table
// src/app/recipes.component.ts
import { Component, inject, signal, type OnInit } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { SupabaseService } from './supabase.service'
type Recipe = { id: number; title: string; minutes: number | null }
@Component({
selector: 'app-recipes',
standalone: true,
imports: [FormsModule],
template: `
@if (supabase.session()) {
<form (ngSubmit)="add()">
<input [(ngModel)]="newTitle" name="title" placeholder="New recipe" required />
<button>Add</button>
</form>
}
@if (error()) { <p>{{ error() }}</p> }
<ul>
@for (r of recipes(); track r.id) {
<li>{{ r.title }}</li>
}
</ul>
`,
})
export class RecipesComponent implements OnInit {
readonly supabase = inject(SupabaseService)
readonly recipes = signal<Recipe[]>([])
readonly error = signal('')
newTitle = ''
async ngOnInit() {
await this.load()
}
async load() {
const { data, error } = await this.supabase.client
.from('recipes')
.select('id, title, minutes')
.order('created_at', { ascending: false })
if (error) this.error.set(error.message)
else this.recipes.set(data)
}
async add() {
const { error } = await this.supabase.client.from('recipes').insert({ title: this.newTitle })
if (error) this.error.set(error.message)
else {
this.newTitle = ''
await this.load()
}
}
}
Reading works signed out through the quickstart's select policy. Inserting needs a signed-in user, since the insert policy is for authenticated and checks user_id.
Add sign-in
// src/app/sign-in.component.ts
import { Component, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { SupabaseService } from './supabase.service'
@Component({
selector: 'app-sign-in',
standalone: true,
imports: [FormsModule],
template: `
@if (supabase.session(); as s) {
<p>Signed in as {{ s.user.email }}</p>
<button (click)="supabase.signOut()">Sign out</button>
} @else {
<form (ngSubmit)="signIn()">
<input [(ngModel)]="email" name="email" type="email" required />
<input [(ngModel)]="password" name="password" type="password" required />
<button type="submit">Sign in</button>
<button type="button" (click)="signUp()">Create account</button>
</form>
<p>{{ message() }}</p>
}
`,
})
export class SignInComponent {
readonly supabase = inject(SupabaseService)
readonly message = signal('')
email = ''
password = ''
async signIn() {
const { error } = await this.supabase.signIn(this.email, this.password)
this.message.set(error ? error.message : '')
}
async signUp() {
const { error } = await this.supabase.signUp(this.email, this.password)
this.message.set(error ? error.message : 'Check your inbox to confirm your email.')
}
}
Guard routes
A functional guard redirects signed-out visitors:
// src/app/auth.guard.ts
import { inject } from '@angular/core'
import { Router, type CanActivateFn } from '@angular/router'
import { SupabaseService } from './supabase.service'
export const authGuard: CanActivateFn = async () => {
// inject() only works before the first await.
const supabase = inject(SupabaseService)
const router = inject(Router)
const { data } = await supabase.client.auth.getSession()
return data.session ? true : router.createUrlTree(['/sign-in'])
}
Use it with { path: 'account', component: AccountComponent, canActivate: [authGuard] }. Guards only shape the UI; Row Level Security is what protects the data.
If you use Angular SSR, the service above runs in the browser only. For server-rendered pages that need the user, read the session from cookies with @supabase/ssr; see Server-side auth and SSR.
Allow your redirect URLs
On the project's Auth tab, set Site URL and add your origins under Redirect URLs, then click Save Auth settings:
http://localhost:4200/**
https://recipes.example.com/**