supabase functions (not edge)
Dennis kinuthia
Posted on August 28, 2024
Supabase
An open source alternative to firebase offering
- database
- realtime
- auth
- functions
- edge-functions
But wait,if they already have functions why do they need edge functions?
Supabase Functions: Your PostgreSQL Toolbox
Supabase functions, also known as database functions, are essentially PostgreSQL stored procedures. They are executable blocks of SQL code that can be called from within SQL queries.
Edge Functions: Beyond the Database
In contrast, Edge functions are server-side TypeScript functions that run on the Deno runtime. They are similar to Firebase Cloud Functions but offer a more flexible and open-source alternative.
Supabase: A PostgreSQL Platform
Beyond its role as an open-source alternative to Firebase, Supabase has evolved into a comprehensive PostgreSQL platform. It provides first-class support for PostgreSQL functions, integrating them seamlessly into its built-in utilities and allowing you to create and manage custom functions directly from the Supabase dashboard.
Structure of a basic postgres functon
CREATE FUNCTION my_function() RETURNS int AS $$
BEGIN
RETURN 42;
END;
$$ LANGUAGE sql;
Breakdown:
-
CREATE FUNCTION
: This keyword indicates that we're defining a new function. -
my_function()
: This is the name of the function. You can choose any meaningful name you prefer. -
RETURNS int
: This specifies the return type of the function. In this case, the function will return an integer value. -
AS $$
: This begins the function body, enclosed within double dollar signs ($$
) to delimit it. -
BEGIN
: This marks the start of the function's executable code. -
RETURN 42;
: This statement specifies the value that the function will return. In this case, it's the integer 42. -
END;
: This marks the end of the function's executable code. -
$$ LANGUAGE sql;
: This specifies the language in which the function is written. In this case, it's SQL.
Purpose:
This function defines a simple SQL function named my_function
that returns the integer value 42. It's a basic example to demonstrate the structure and syntax of a function definition in PostgreSQL.
Key points to remember:
- You can replace
my_function
with any desired function name. - The return type can be any valid data type, such as
text
,boolean
,date
, or a user-defined type. - The function body can contain complex logic, including conditional statements, loops, and calls to other functions.
The
$$
delimiters are used to enclose the function body in a language-independent manner.Postgres functions can also be called by postgres
TRIGGERS
which are like functions but react to specific events likeinsert
,update
ordelete
on a tableto execute this function
SELECT my_function();
- to list this function ```sql
SELECT
proname AS function_name,
prokind AS function_type
FROM pg_proc
WHERE proname = 'my_function';
- to drop this function
```sql
DROP FUNCTION my_function();
Supabase postgres functions
Built-in functions
Supabase makes use of postgres functions to perform certain tasks within your database.
short list of exampales includes
-- list all the supabase functions
SELECT
proname AS function_name,
prokind AS function_type
FROM pg_proc;
-- filter for the session supabase functions function
SELECT
proname AS function_name,
prokind AS function_type
FROM pg_proc
WHERE proname ILIKE '%session%';
-- selects the curremt jwt
select auth.jwt()
-- select what role is callig the function (anon or authenticated)
select auth.role();
-- select the session user
select session_use;
Supabase functions view on the dashboard
To view some of these functions in Supabase, you can check under database > functions
Useful Supabase PostgreSQL Functions
Creating a user_profile
Table on User Signup
Supabase stores user data in the auth.users
table, which is private and should not be accessed or modified directly. A recommended approach is to create a public users
or user_profiles
table and link it to the auth.users
table.
While this can be done using client-side SDKs by chaining a create user request with a successful signup request, it's more reliable and efficient to handle it on the Supabase side. This can be achieved using a combination of a TRIGGER
and a FUNCTION
.
-- create the user_profiles table
CREATE TABLE user_profiles (
id uuid PRIMARY KEY,
FOREIGN KEY (id) REFERENCES auth.users(id),
name text,
email text
);
-- create a function that returns a trigger on auth.users
CREATE
OR REPLACE FUNCTION public.create_public_user_profile_table()
RETURNS TRIGGER AS $$
BEGIN INSERT INTO public.user_profiles (id,name,email)
VALUES
(
NEW.id,
NEW.raw_user_meta_data ->> 'name',
NEW.raw_user_meta_data ->> 'email'
-- other fields accessible here
-- NEW.raw_user_meta_data ->> 'name',
-- NEW.raw_user_meta_data ->> 'picture',
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- create the trigger that executes the function on every new user rowcteation(signup)
CREATE TRIGGER create_public_user_profiles_trigger
AFTER INSERT ON auth.users FOR EACH ROW WHEN (
NEW.raw_user_meta_data IS NOT NULL
)
EXECUTE FUNCTION public.create_public_user_profile_table ();
let { data: user_profiles, error } = await supabase
.from('user_profiles')
.select('*')
- Add custom claims on jwt creation (RBAC) supabse has a detailed article and video on this .
we need 2 tabbles
-
public.roles
andpublic.role_permissions
-- Custom types
create type public.app_permission as enum ('channels.delete', 'channels.update', 'messages.update', 'messages.delete');
create type public.app_role as enum ('admin', 'moderator');
-- USER ROLES
create table public.user_roles (
id bigint generated by default as identity primary key,
user_id uuid references public.users on delete cascade not null,
role app_role not null,
unique (user_id, role)
);
comment on table public.user_roles is 'Application roles for each user.';
-- ROLE PERMISSIONS
create table public.role_permissions (
id bigint generated by default as identity primary key,
role app_role not null,
permission app_permission not null,
unique (role, permission)
);
comment on table public.role_permissions is 'Application permissions for each role.';
example of user role
id | user_id | role |
---|---|---|
1 | user-1 | admin |
2 | user-2 | moderator |
example of a role permission table
id | role | permission |
---|---|---|
1 | admin | channels.update |
2 | admin | messages.update |
3 | admin | messages.delete |
4 | admin | messages.delete |
5 | moderator | channels.update |
6 | moderator | messages.update |
user with user_id = user-1 will have admin and moderator roles and can delete channels and messages
users with user_id = user-2 can only update channels and messages with the moderator role
-- Create the auth hook function
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
stable
as $$
declare
claims jsonb;
user_role public.app_role;
begin
-- Fetch the user role in the user_roles table
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
claims := event->'claims';
if user_role is not null then
-- Set the claim
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
else
claims := jsonb_set(claims, '{user_role}', 'null');
end if;
-- Update the 'claims' object in the original event
event := jsonb_set(event, '{claims}', claims);
-- Return the modified or original event
return event;
end;
$$;
grant usage on schema public to supabase_auth_admin;
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
revoke execute
on function public.custom_access_token_hook
from authenticated, anon, public;
grant all
on table public.user_roles
to supabase_auth_admin;
revoke all
on table public.user_roles
from authenticated, anon, public;
create policy "Allow auth admin to read user roles" ON public.user_roles
as permissive for select
to supabase_auth_admin
using (true)
then create a function that will be called to authorize on RLS policies
create or replace function public.authorize(
requested_permission app_permission
)
returns boolean as $$
declare
bind_permissions int;
user_role public.app_role;
begin
-- Fetch user role once and store it to reduce number of calls
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
select count(*)
into bind_permissions
from public.role_permissions
where role_permissions.permission = requested_permission
and role_permissions.role = user_role;
return bind_permissions > 0;
end;
$$ language plpgsql stable security definer set search_path = '';
-- example RLS policies
create policy "Allow authorized delete access" on public.channels for delete using ( (SELECT authorize('channels.delete')) );
create policy "Allow authorized delete access" on public.messages for delete using ( (SELECT authorize('messages.delete')) );
Improved Text:
Creating RPC Endpoints
Supabase functions can be invoked using the rpc
function. This is especially useful for writing custom SQL queries when the built-in PostgreSQL APIs are insufficient, such as calculating vector cosine similarity using pg_vector
.
create or replace function match_documents (
query_embedding vector(384),
match_threshold float,
match_count int
)
returns table (
id bigint,
title text,
body text,
similarity float
)
language sql stable
as $$
select
documents.id,
documents.title,
documents.body,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where 1 - (documents.embedding <=> query_embedding) > match_threshold
order by (documents.embedding <=> query_embedding) asc
limit match_count;
$$;
and call it client side
const { data: documents } = await supabaseClient.rpc('match_documents', {
query_embedding: embedding, // Pass the embedding you want to compare
match_threshold: 0.78, // Choose an appropriate threshold for your data
match_count: 10, // Choose the number of matches
})
Improved Text:
Filtering Out Columns
To prevent certain columns from being modified on the client, create a simple function that triggers on every insert. This function can omit any extra fields the user might send in the request.
-- check if user with roles authenticated or anon submitted an updatedat column and replace it with the current time , if not (thta is an admin) allow it
CREATE
or REPLACE function public.omit_updated__at () returns trigger as
$$ BEGIN
IF auth.role() IS NOT NULL AND auth.role() IN ('anon', 'authenticated')
THEN NEW.updated_at = now();
END IF;
RETURN NEW;
END; $$ language plpgsql;
Summary
With a little experimentation, you can unlock the power of Supabase functions and their AI-powered SQL editor. This lowers the barrier to entry for the niche knowledge required to get this working.
Why choose Supabase functions?
- Extend Supabase's API: Supabase can only expose so much through its API. Postgres, however, is a powerful database. Any action you can perform with SQL statements can be wrapped in a function and called from the client or by a trigger.
- Reduce the need for dedicated backends: Supabase functions can fill the simple gaps left by the client SDKs, allowing you to focus on shipping.
- Avoid vendor lock-in: Supabase functions are just Postgres. If you ever need to move to another hosting provider, these functionalities will continue to work.
Posted on August 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.