Skip to content

Database Functions as RPC

A Postgres function is automatically exposed as a callable RPC endpoint, so logic that needs to run atomically or as multiple steps can live in the database instead of being composed from the client.

.from() queries are great for direct table and view access, but some operations do not fit a single select/insert/update. Multi-step logic, logic that must run as one atomic unit, or logic you want to keep out of application code entirely, is often easier and safer to express as a Postgres function written in plpgsql or sql.

create function public.increment_view_count(post_id uuid)
returns void
language sql
as $$
update public.posts
set view_count = view_count + 1
where id = post_id;
$$;

PostgREST exposes functions as /rpc/<function_name>

Section titled “PostgREST exposes functions as /rpc/<function_name>”

Just like it does for tables, PostgREST introspects your schema for functions and exposes each one as a POST endpoint under /rpc:

Terminal window
POST /rest/v1/rpc/increment_view_count

From supabase-js, you call it with .rpc(), passing the function name and an object of argument names to values:

const { data, error } = await supabase.rpc('increment_view_count', {
post_id: '3b8f1c1e-7f2e-4b7a-9c1a-8b2f6a2d9e10',
});
if (error) {
console.error('failed to increment view count:', error.message);
}

Why this beats a client-side read-then-write

Section titled “Why this beats a client-side read-then-write”

Imagine incrementing the same counter from the client instead: read the current view_count, add one, write it back. Under concurrent requests, two clients can read the same starting value before either writes, and one increment is silently lost — a classic race condition. The Postgres function above does the read and write in a single atomic update statement executed by the database itself, so concurrent calls never race each other.

This is the general shape of when RPC earns its place: .from() queries are for direct table/view access, and .rpc() is for functions — custom logic, potentially multi-statement or transactional, that genuinely belongs inside the database. Reach for RPC when the logic needs that guarantee, not as a default replacement for ordinary queries.

flowchart LR
  fn["create function increment_view_count(post_id uuid)"] --> introspect["PostgREST introspects functions"]
  introspect --> endpoint["POST /rest/v1/rpc/increment_view_count"]
  endpoint --> call["supabase.rpc('increment_view_count', { post_id })"]
  call --> atomic["Single atomic UPDATE in Postgres"]
A Postgres function exposed as a callable RPC endpoint
How does a Postgres function become callable over the Data API?
Why is an atomic view-count increment a good candidate for an RPC function rather than a client-side read-then-write?
What is the conceptual difference between .from() queries and .rpc() calls?
From supabase-js, how do you call a Postgres function named increment_view_count with an argument?