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/**

Next steps

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us