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
Foreign key to auth.users - automatically links to Supabase Auth
URL to user’s profile picture (stored in Supabase Storage)
User’s display name (max 60 characters)
Unique username (3-36 chars, alphanumeric + underscore)
Profile creation timestamp (auto-generated)
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
Select
Insert
Update
Delete
const { data , error } = await supabase
. from ( 'profiles' )
. select ( 'username, full_name, avatar_url' )
. eq ( 'user_id' , userId )
. single ();
const { data , error } = await supabase
. from ( 'profiles' )
. insert ({
user_id: userId ,
username: 'johndoe' ,
full_name: 'John Doe'
})
. select ()
. single ();
const { data , error } = await supabase
. from ( 'profiles' )
. update ({ full_name: 'Jane Doe' })
. eq ( 'user_id' , userId )
. select ()
. single ();
const { error } = await supabase
. from ( 'profiles' )
. delete ()
. eq ( 'user_id' , userId );
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.
Use enums for table names
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:
Never expose the service role key in client code
Always validate user input in edge functions
Use RLS policies for all tables
Implement rate limiting for public endpoints
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