Use when creating or modifying PostgreSQL functions, triggers, or stored procedures for Supabase databases - ensures secure function design with SECURITY INVOKER, proper search_path configuration, and production-ready patterns
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate high-quality PostgreSQL functions following Supabase best practices.
Use this skill when:
Don't use this skill for:
Security context: PostgreSQL functions without proper configuration are vulnerable to:
These patterns prevent real-world exploits observed in production Supabase databases. Following these rules is mandatory for security.
Functions run with the permissions of the invoking user by default, ensuring safer access control.
create or replace function my_schema.example()
returns text
language plpgsql
security invoker -- Default to this
set search_path = ''
as $$
begin
return 'hello world';
end;
$$;
Only use SECURITY DEFINER when explicitly required and document the rationale.
Set search_path to empty string to prevent security risks from untrusted schema resolution:
set search_path = '';
Use fully qualified names for all database objects:
select sum(price * quantity)
from public.order_items -- Fully qualified
where order_id = calculate_total.order_id;
create or replace function public.function_name(param_name type)
returns return_type
language plpgsql
security invoker
set search_path = ''
as $$
declare
variable_name type;
begin
-- Function logic here
return variable_name;
end;
$$;
create or replace function public.calculate_total_price(order_id bigint)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
declare
total numeric;
begin
select sum(price * quantity)
into total
from public.order_items
where order_id = calculate_total_price.order_id;
return total;
end;
$$;
create or replace function my_schema.update_updated_at()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
new.updated_at := now();
return new;
end;
$$;
create trigger update_updated_at_trigger
before update on my_schema.my_table
for each row
execute function my_schema.update_updated_at();
create or replace function my_schema.safe_divide(numerator numeric, denominator numeric)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
begin
if denominator = 0 then
raise exception 'Division by zero is not allowed';
end if;
return numerator / denominator;
end;
$$;
create or replace function my_schema.full_name(first_name text, last_name text)
returns text
language sql
security invoker
set search_path = ''
immutable -- Enables better PostgreSQL optimization
as $$
select first_name || ' ' || last_name;
$$;
Choose the appropriate volatility level:
create or replace function public.calculate_total(order_id bigint)
returns numeric
language plpgsql
as $$
begin
-- SECURITY RISK: Unqualified table reference + no search_path
select sum(price * quantity) into total
from order_items -- Which schema? Attacker can create malicious table
where order_id = order_id; -- Also: ambiguous column reference
return total;
end;
$$;
Problems:
search_path = '' allows schema hijackingorder_items can resolve to attacker's tableorder_id = order_id always true)create or replace function public.calculate_total(order_id bigint)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
declare
total numeric;
begin
select sum(price * quantity)
into total
from public.order_items -- Fully qualified
where order_id = calculate_total.order_id; -- Qualified parameter
return total;
end;
$$;
create or replace function public.admin_delete_user(user_id uuid)
returns void
language plpgsql
security definer -- Dangerous: runs as function owner
set search_path = ''
as $$
begin
delete from public.users where id = user_id; -- Anyone can call this!
end;
$$;
Problems:
create or replace function public.delete_own_account()
returns void
language plpgsql
security invoker -- Runs as caller
set search_path = ''
as $$
begin
delete from public.users
where id = auth.uid(); -- Caller can only delete their own account
end;
$$;
Or with SECURITY DEFINER (if truly needed):
create or replace function public.admin_delete_user(user_id uuid)
returns void
language plpgsql
security definer -- Justified: needs elevated privileges
set search_path = ''
as $$
begin
-- REQUIRED: Access control check
if not exists (
select 1 from public.user_roles
where user_id = auth.uid() and role = 'admin'
) then
raise exception 'Access denied: admin role required';
end if;
delete from public.users where id = admin_delete_user.user_id;
end;
$$;
create or replace function public.get_table_data(table_name text)
returns json
language plpgsql
security invoker
set search_path = ''
as $$
begin
-- SQL INJECTION: Unsanitized input
return query execute 'select * from ' || table_name;
end;
$$;
create or replace function public.get_table_data(table_name text)
returns json
language plpgsql
security invoker
set search_path = ''
as $$
begin
-- Whitelist approach: only allow specific tables
if table_name not in ('orders', 'products', 'customers') then
raise exception 'Invalid table name';
end if;
return query execute format('select * from public.%I', table_name);
-- %I properly quotes identifiers
end;
$$;
CREATE TRIGGER statements when functions are designed as triggersSECURITY DEFINER is used if required