DEV Community

Cover image for supabase functions (not edge)
Dennis kinuthia
Dennis kinuthia

Posted on

supabase functions (not edge)

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?

why the extra parts

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;
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. CREATE FUNCTION: This keyword indicates that we're defining a new function.
  2. my_function(): This is the name of the function. You can choose any meaningful name you prefer.
  3. RETURNS int: This specifies the return type of the function. In this case, the function will return an integer value.
  4. AS $$: This begins the function body, enclosed within double dollar signs ($$) to delimit it.
  5. BEGIN: This marks the start of the function's executable code.
  6. RETURN 42;: This statement specifies the value that the function will return. In this case, it's the integer 42.
  7. END;: This marks the end of the function's executable code.
  8. $$ 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 like insert, update or delete on a table

  • to execute this function

SELECT my_function();
Enter fullscreen mode Exit fullscreen mode
  • to list this function
SELECT
    proname AS function_name,
    prokind AS function_type
FROM pg_proc
WHERE proname = 'my_function';
Enter fullscreen mode Exit fullscreen mode
  • to drop this function
DROP FUNCTION my_function();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Supabase functions view on the dashboard
To view some of these functions in Supabase, you can check under database > functions

supabase functions view on the dashboard

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 ();

Enter fullscreen mode Exit fullscreen mode
let { data: user_profiles, error } = await supabase
  .from('user_profiles')
  .select('*')
Enter fullscreen mode Exit fullscreen mode

we need 2 tabbles

  • public.roles and public.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.';
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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')) );


Enter fullscreen mode Exit fullscreen mode

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;
$$;

Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)