Skip to main content
Jet is built on Supabase, providing a complete backend solution including authentication, PostgreSQL database, storage, real-time subscriptions, and serverless edge functions.

Overview

Supabase acts as the backend infrastructure, eliminating the need to build and maintain your own API server. The integration includes:
  • Authentication: Managed user accounts with JWT tokens
  • Database: PostgreSQL with Row Level Security (RLS)
  • Storage: File uploads and CDN delivery
  • Edge Functions: Serverless TypeScript functions
  • Real-time: WebSocket subscriptions (ready to use)

Client Configuration

The Supabase client is configured as an Angular injection token (src/app/injection-tokens/supabase-client.injection-token.ts:4):
import { InjectionToken } from '@angular/core';
import { createClient, SupabaseClient } from '@supabase/supabase-js';

export const SUPABASE_CLIENT: InjectionToken<SupabaseClient> = new InjectionToken<SupabaseClient>(
  'SUPABASE_CLIENT',
  {
    factory: () =>
      createClient(
        import.meta.env.NG_APP_SUPABASE_URL,
        import.meta.env.NG_APP_SUPABASE_PUBLISHABLE_OR_ANON_KEY,
        { auth: { throwOnError: true } },
      ),
    providedIn: 'root',
  },
);
The client is configured with throwOnError: true to ensure authentication errors are properly caught and handled.

Environment Variables

Configure your Supabase project credentials:
NG_APP_SUPABASE_URL=https://your-project-id.supabase.co
NG_APP_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your-anon-key
Never commit environment variables to version control. Use .env.local for local development.

Database Schema

Jet includes database migrations in supabase/migrations/ that set up the initial schema.

Profiles Table

User profiles extend Supabase’s auth system (05_tables.sql):
create table public.profiles (
  user_id uuid primary key references auth.users (id),
  avatar_url shared.url null,
  full_name text null check (length(full_name) <= 60),
  username text not null unique check (
    length(username) between 3 and 36
    and username ~ '^[a-z0-9_]+$'
  ),
  created_at timestamptz not null default now(),
  updated_at timestamptz null
);

Schema Features

user_id
uuid
required
Foreign key to auth.users - automatically links to Supabase Auth
avatar_url
text
URL to user’s profile picture (stored in Supabase Storage)
full_name
text
User’s display name (max 60 characters)
username
text
required
Unique username (3-36 chars, alphanumeric + underscore)
created_at
timestamptz
required
Profile creation timestamp (auto-generated)
updated_at
timestamptz
Last update timestamp

Row Level Security (RLS)

Profiles are protected with RLS policies (06_rls_policies.sql):
alter table public.profiles enable row level security;

grant select, update on table public.profiles to authenticated;

create policy "Allow authenticated to select own" on public.profiles
as permissive
for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Allow authenticated to update own" on public.profiles
as permissive
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Users can only view and update their own profile. This is enforced at the database level, not in application code.

Database Enums

Jet uses TypeScript enums that mirror database tables (src/app/enums/supabase-table.enum.ts:1):
export enum SupabaseTable {
  AppPermissions = 'app_permissions',
  AppPermissionsAppRoles = 'app_permissions_app_roles',
  AppRoles = 'app_roles',
  AppRolesUsers = 'app_roles_users',
  Profiles = 'profiles',
}
Additional enums for other Supabase resources:
  • SupabaseStorage - Storage bucket names
  • SupabaseEdgeFunction - Edge function names
  • SupabaseDatabaseFunction - Database function names

Using Table Enums

const { data, error } = await this.#supabaseClient
  .from(SupabaseTable.Profiles)
  .select('*')
  .eq('user_id', userId)
  .single();

Using the Supabase Client

Inject the Client

Inject the Supabase client in any service:
import { inject, Injectable } from '@angular/core';
import { SUPABASE_CLIENT } from '@jet/injection-tokens/supabase-client.injection-token';

@Injectable({ providedIn: 'root' })
export class ProfileService {
  readonly #supabaseClient = inject(SUPABASE_CLIENT);

  async getProfile(userId: string) {
    return this.#supabaseClient
      .from(SupabaseTable.Profiles)
      .select('*')
      .eq('user_id', userId)
      .single();
  }
}

Query Examples

const { data, error } = await supabase
  .from('profiles')
  .select('username, full_name, avatar_url')
  .eq('user_id', userId)
  .single();

Storage

Supabase Storage handles file uploads (avatars, documents, etc.).

Upload a File

const file = event.target.files[0];
const filePath = `${userId}/avatar.png`;

const { data, error } = await this.#supabaseClient
  .storage
  .from('avatars')
  .upload(filePath, file, {
    cacheControl: '3600',
    upsert: true
  });

Get Public URL

const { data } = this.#supabaseClient
  .storage
  .from('avatars')
  .getPublicUrl(filePath);

const avatarUrl = data.publicUrl;

Delete a File

const { error } = await this.#supabaseClient
  .storage
  .from('avatars')
  .remove([filePath]);

Edge Functions

Supabase Edge Functions are serverless TypeScript functions deployed globally.

Project Structure

supabase/
├── functions/
│   ├── _shared/
│   │   ├── clients/
│   │   │   └── supabase-admin.client.ts
│   │   └── enums/
│   │       └── supabase-table.enum.ts
│   └── your-function/
│       └── index.ts

Calling Edge Functions

const { data, error } = await this.#supabaseClient.functions.invoke(
  'your-function',
  {
    body: { key: 'value' }
  }
);

Admin Client

Edge functions use the admin client for elevated permissions (supabase/functions/_shared/clients/supabase-admin.client.ts):
import { createClient } from '@supabase/supabase-js';

export const supabaseAdmin = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
  }
);
The service role key bypasses RLS. Only use it in edge functions, never in client code.

Authentication Integration

The UserService wraps Supabase Auth (src/app/services/user/user.service.ts:23):
@Injectable()
export class UserService {
  readonly #supabaseClient = inject(SUPABASE_CLIENT);
  readonly #user: WritableSignal<null | User>;

  public constructor() {
    this.#user = signal(null);

    // Listen to auth state changes
    this.#supabaseClient.auth.onAuthStateChange(
      (_authChangeEvent: AuthChangeEvent, authSession: AuthSession | null): void => {
        this.#user.set(authSession?.user ?? null);
      },
    );
  }

  public get user(): Signal<null | User> {
    return this.#user.asReadonly();
  }
}

Auth Methods

// Sign in with password
public signInWithPassword(email: string, password: string) {
  return this.#supabaseClient.auth.signInWithPassword({ email, password });
}

// Sign in with OAuth
public signInWithOauth(oauthProvider: OauthProvider) {
  return this.#supabaseClient.auth.signInWithOAuth({
    provider: oauthProvider,
    options: { 
      redirectTo: this.#getRedirectUrlWithReturnUrl(),
      skipBrowserRedirect: true 
    },
  });
}

// Sign out
public signOut() {
  return this.#supabaseClient.auth.signOut();
}

// Get current session
public getClaims() {
  return this.#supabaseClient.auth.getClaims();
}

Real-time Subscriptions

Supabase provides real-time updates via WebSockets:
const channel = this.#supabaseClient
  .channel('profiles-changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'profiles',
      filter: `user_id=eq.${userId}`
    },
    (payload) => {
      console.log('Profile updated:', payload.new);
    }
  )
  .subscribe();

// Cleanup
channel.unsubscribe();

Type Safety

Generate TypeScript types from your database schema:
supabase gen types typescript --project-id your-project-id > src/types/database.types.ts
Use generated types with the client:
import { Database } from '@jet/types/database.types';

const supabase = createClient<Database>(
  import.meta.env.NG_APP_SUPABASE_URL,
  import.meta.env.NG_APP_SUPABASE_PUBLISHABLE_OR_ANON_KEY
);

Local Development

Run Supabase locally with Docker:
# Start Supabase
supabase start

# View local dashboard
supabase status

# Stop Supabase
supabase stop
Local Supabase runs on http://localhost:54321 by default.

Database Migrations

Create a Migration

supabase migration new add_posts_table

Apply Migrations

# Local
supabase db reset

# Remote
supabase db push

Migration Example

-- Create posts table
create table public.posts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) not null,
  title text not null,
  content text,
  created_at timestamptz default now()
);

-- Enable RLS
alter table public.posts enable row level security;

-- Allow users to read all posts
create policy "Posts are viewable by everyone"
on public.posts for select
using (true);

-- Allow users to create their own posts
create policy "Users can create their own posts"
on public.posts for insert
with check (auth.uid() = user_id);

Best Practices

Enable Row Level Security on all tables and create explicit policies. Never rely on client-side authorization alone.
Define TypeScript enums for table names, storage buckets, and functions to prevent typos and enable autocomplete.
Always check for errors in Supabase responses and provide user feedback.
Use .select() with specific columns instead of * to reduce data transfer.
Create database indexes on frequently queried columns for better performance.
Implement complex business logic in PostgreSQL functions for better performance and security.

Security Considerations

Critical Security Rules:
  1. Never expose the service role key in client code
  2. Always validate user input in edge functions
  3. Use RLS policies for all tables
  4. Implement rate limiting for public endpoints
  5. Sanitize file uploads to prevent malicious content

Troubleshooting

  • Verify environment variables are set correctly
  • Check Supabase project status in dashboard
  • Ensure network allows connections to Supabase
  • Check if RLS is enabled on the table
  • Verify policies allow the operation
  • Test policies with different user roles
  • Review policy using statements
  • Regenerate types after schema changes
  • Ensure @supabase/supabase-js is up to date
  • Check column names match database schema

Next Steps

Authentication

Learn about Supabase Auth integration

Deployment

Deploy to production with Supabase

Build docs developers (and LLMs) love