Mastering Supabase RPC with Custom PostgreSQL Functions
Supabase’s RPC (Remote Procedure Call) layer lets you invoke PostgreSQL functions straight from the client, turning the database into a tiny API hub. When you combine that with your own custom Postgres functions, you get a powerful, type‑safe way to enforce business logic without writing extra backend code. In this article we’ll walk through the why, the how, and a few best‑practice tips that keep your Supabase projects both fast and maintainable.
Why Use Supabase RPC for Custom Postgres Functions?
At its core, Supabase is a hosted Postgres instance with a thin JavaScript/REST wrapper. The RPC feature exposes any LANGUAGE sql or LANGUAGE plpgsql function as an HTTP endpoint, automatically handling auth tokens, row‑level security (RLS), and response formatting. This means you can:
- Encapsulate complex queries or multi‑step transactions in a single call.
- Leverage Postgres’s mature data‑type system for validation before the data ever touches your front‑end.
- Keep sensitive business rules on the server, reducing the attack surface.
Because the function runs inside the database, latency stays low—typically a few milliseconds—while you avoid the overhead of a separate microservice.
Creating Your First Custom Function
Start with a simple example: a function that returns the total sales for a given user. In Supabase’s SQL editor, paste the following:
create or replace function public.user_total_sales(user_id uuid)returns numeric
language sql
as $$
select coalesce(sum(amount), 0)
from sales
where sales.user_id = user_id;
$$;
Note the public. schema prefix; Supabase only exposes functions in schemas that are whitelisted (by default, public and storage). After saving, the function instantly appears in the Functions tab of the dashboard.
Calling the Function from Your Front‑End
Supabase’s JavaScript client offers a rpc method that maps directly to the function name. Here’s a typical usage in a React component:
import { supabase } from './supabaseClient';async function fetchTotalSales(userId) {
const { data, error } = await supabase
.rpc('user_total_sales', { user_id: userId });
if (error) console.error('RPC error:', error);
else return data;
}
The client automatically includes the current session’s JWT, so any RLS policies you’ve set on the sales table will be respected. If the user isn’t authorized, Supabase returns a 401 error before the function even runs.
Advanced Patterns: Transactional Workflows
One of the biggest advantages of custom functions is the ability to bundle multiple statements into a single, atomic transaction. Consider a “checkout” operation that needs to:
- Validate inventory.
- Create an order record.
- Deduct stock quantities.
A PL/pgSQL function can handle all three steps, rolling back automatically if any part fails:
create or replace function public.checkout(user_id uuid, cart jsonb)returns uuid
language plpgsql
as $$
declare
new_order_id uuid;
begin
-- Validate inventory
perform 1
from products p
where p.id = (cart->>'product_id')::uuid
and p.stock >= (cart->>'quantity')::int;
if not found then
raise exception 'Insufficient stock';
end if;
-- Insert order
insert into orders (user_id, items) values (user_id, cart)
returning id into new_order_id;
-- Update stock
update products
set stock = stock - (cart->>'quantity')::int
where id = (cart->>'product_id')::uuid;
return new_order_id;
end;
$$;
Calling supabase.rpc('checkout', { user_id, cart }) now either creates a complete order or throws an error, leaving the database untouched. This pattern eliminates race conditions that often plague client‑side orchestration.
Security Considerations
Even though the RPC layer respects RLS, you still need to be mindful of a few pitfalls:
- Parameter sanitization: PostgreSQL automatically binds parameters, but avoid string interpolation inside
EXECUTEstatements unless you quote safely. - Privilege escalation: By default, functions run with the caller’s privileges. If you need elevated rights (e.g., to modify rows the user can’t normally touch), create the function with
SECURITY DEFINERand restrict its usage via explicit RLS policies. - Rate limiting: Supabase doesn’t throttle RPC calls out of the box. If a function performs heavy computation, consider adding a
pg_sleepguard or moving the logic to a serverless function.
Testing and Debugging Tips
Because functions live inside the database, traditional unit‑test frameworks don’t apply directly. Here are a few practical approaches:
- Use
pgTAPfor inline SQL tests; you can run them from the Supabase CLI. - Leverage the SQL editor’s “Run” button with
SELECTstatements to inspect interim results. - Write a small wrapper endpoint in your front‑end that calls the function and logs the response—ideal for quick sanity checks during development.
Performance Best Practices
Custom functions are as fast as the queries they contain, but a few habits keep them snappy:
- Prefer set‑based operations over loops; even in PL/pgSQL, a
FORloop over thousands of rows can be slower than a singleUPDATE … FROMstatement. - Index columns used in
WHEREclauses inside the function. An index that speeds a regular SELECT will also accelerate the RPC call. - Return only the columns you need. Overly wide result sets increase network payload and can bloat client memory.
Deploying Changes Safely
When you modify a function that’s already in production, consider the following rollout strategy:
- Rename the existing function (e.g.,
user_total_sales_v1) and create a new version with the original name. - Update client code to call the new function name, or use a feature flag to switch gradually.
- After confirming stability, drop the old version.
This approach avoids breaking existing RPC calls while giving you a rollback path.
FAQ
How do I pass complex data structures to a Supabase RPC?
Supabase supports JSON, JSONB, and array types as parameters. Wrap your JavaScript object with JSON.stringify and declare the corresponding argument as jsonb in the function signature.
Can I use Supabase RPC for real‑time subscriptions?
Not directly. RPC calls are request‑response, but you can combine them with Supabase’s realtime feature: after an RPC modifies data, listen to the affected table’s changes via a WebSocket subscription.
What limits does Supabase place on RPC execution?
Supabase enforces the same timeout and memory limits as regular queries—typically a few seconds and the standard Postgres memory configuration. Heavy‑weight tasks should be offloaded to Edge Functions or external services.
Is it possible to version‑control my custom functions?
Yes. Store the SQL definitions in your repo and apply them with the Supabase CLI’s db push command. This ensures your database schema, including functions, stays in sync across environments.