RPC & Stored Procedures
Anything you can’t express as a single REST read/write — multi-step logic, aggregations, atomic operations, custom search — belongs in a PostgreSQL function that you call over RPC:
POST /v1/rest/<project_id>/rpc/<function_name>The function runs in one transaction, inside the same RLS context as any other data-plane request, so it’s both atomic and secure.
Define a function
Author it in the SQL Editor or via exec_sql, and
capture it as a migration:
create or replace function increment_views(post_id uuid)returns integerlanguage sqlas $$ update posts set views = views + 1 where id = post_id returning views$$;Call it
Arguments go in the JSON body, keyed by parameter name:
curl -X POST "http://localhost:39001/v1/rest/<id>/rpc/increment_views" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{"post_id":"3f2b…"}'const { data } = await db.rpc('increment_views', { post_id: postId })db.rpc("increment_views", {"post_id": post_id}).execute()final views = await db.rpc('increment_views', params: {'post_id': postId});Returning sets
A function returning setof / table(...) comes back as a JSON array, just like a
table read:
create or replace function search_posts(q text, lim int default 10)returns setof postslanguage sql stableas $$ select * from posts where title ilike '%' || q || '%' order by created_at desc limit lim$$;const { data: results } = await db.rpc('search_posts', { q: 'launch', lim: 5 })Atomic multi-table operations
The classic reason to use RPC — do several writes atomically:
create or replace function place_order(p_user uuid, p_items jsonb)returns uuidlanguage plpgsqlas $$declare v_order uuid;begin insert into orders (user_id) values (p_user) returning id into v_order; insert into order_items (order_id, sku, qty) select v_order, (i->>'sku'), (i->>'qty')::int from jsonb_array_elements(p_items) i; return v_order;end$$;If anything in the function fails, the entire operation rolls back — no partial orders.
Security: SECURITY DEFINER vs INVOKER
- By default a function runs as
SECURITY INVOKER— with the caller’s role and RLS context. This is what you usually want: the function respects the user’s permissions. SECURITY DEFINERruns as the function owner and can bypass RLS. Use it deliberately and sparingly, validate every argument, and set a safesearch_path(set search_path = public) to avoid hijacking. It’s the tool for controlled privilege escalation (e.g. letting ananonuser create a signup record without exposing the table).
The privileged exec_sql RPC
AnvilBase exposes a virtual RPC, rpc/exec_sql, that runs arbitrary SQL. It
requires the service_role scope and bypasses RLS — it powers the console SQL
editor and admin tooling.
# server-side ONLYcurl -X POST "http://localhost:39001/v1/rest/<id>/rpc/exec_sql" \ -H "apikey: $SERVICE_KEY" -H "Content-Type: application/json" \ -d '{"sql":"select count(*) from posts"}'Never expose
exec_sql(or theservice_rolekey) to clients. See SQL Editor and API Keys & Scopes.
Tips
- Mark read-only functions
stable(orimmutable) so the planner can optimize and so they’re safe to call with theanonkey. - Return
voidfor fire-and-forget mutations, orreturningthe useful value. - Functions are part of your schema — put them in migrations so they promote to staging/prod with everything else.
Next: Auth → Overview.