DEV Community

Cover image for ๐Ÿš€ Leveling Up with Supabase RPC โ€” My โ€œSell Honeyโ€ Transaction Journey
Egor
Egor

Posted on

๐Ÿš€ Leveling Up with Supabase RPC โ€” My โ€œSell Honeyโ€ Transaction Journey

This week, I implemented a new feature for our beekeeping management platform ๐Ÿ:
When a beekeeper sells honey, the system must deduct the sold amount from the batch table and record the sale in a separate sold_honey table โ€” atomically.

At first, I planned to try doing this with two Supabase SDK calls:

await supabase.from('honey_batches').update(...);
await supabase.from('sold_honey').insert(...);
Enter fullscreen mode Exit fullscreen mode

But then I realized the problem โ€”
What if the first succeeds but the second fails? ๐Ÿ˜ฌ
That would leave my data inconsistent (a nightmare for anything involving quantities or money).

So I thought a lot and finally discovered a better way: Supabase RPC (Remote Procedure Call).

I wrote a Postgres function (sell_honey()) that:

  • Locks the batch row (FOR UPDATE) to prevent race conditions
  • Validates available stock
  • Deducts weight from the honey batch
  • Inserts a record into sold_honey
  • Runs all of this in one transaction โ€” if anything fails, it rolls back automatically โœ…

The full Supabase RPC function is as below:

create or replace function public.sell_honey(
  p_honey_batch_id bigint,
  p_batch_reference_id text,
  p_weight_sold numeric,
  p_price_kilo numeric,
  p_buyer text
)
returns table(sold_id bigint, remaining_weight text)
language plpgsql
as $$
declare
  v_current_weight numeric;
  v_remaining numeric;
begin
  if p_weight_sold is null or p_weight_sold <= 0 then
    raise exception 'weight_sold must be a positive number';
  end if;

  -- Lock the batch row to prevent concurrent modifications
  select
    coalesce(nullif(regexp_replace(h.weight, '[^0-9\.]', '', 'g'), ''), '0')::numeric
  into v_current_weight
  from public.honey_batches h
  where h.id = p_honey_batch_id
  for update;

  if v_current_weight is null then
    raise exception 'Batch with id % not found', p_honey_batch_id;
  end if;

  if v_current_weight < p_weight_sold then
    raise exception 'Insufficient honey in batch. Available: % kg', v_current_weight;
  end if;

  -- compute remaining
  v_remaining := v_current_weight - p_weight_sold;

  -- Update honey_batches.weight (store as text to match existing schema)
  update public.honey_batches
  set weight = trim(to_char(v_remaining, 'FM9999999990.00'))
  where id = p_honey_batch_id;

  -- Insert sold record
  insert into public.sold_honey (
    honey_batch_id,
    batch_reference_id,
    weight_sold,
    price_kilo,
    buyer,
    created_at
  )
  values (
    p_honey_batch_id,
    p_batch_reference_id,
    p_weight_sold,
    p_price_kilo,
    p_buyer,
    now()
  )
  returning id into sold_id;

  -- Return sold id and remaining weight (as text)
  remaining_weight := trim(to_char(v_remaining, 'FM9999999990.00'));
  return next;
end;
$$;
Enter fullscreen mode Exit fullscreen mode

Then I integrated it in my Next.js frontend:

await supabase.rpc('sell_honey', {
  p_batch_id: id,
  p_weight_sold: 5.2,
  p_price_kilo: 18.0,
  p_buyer: 'John Bee',
});
Enter fullscreen mode Exit fullscreen mode

Supabase executes the function securely on Postgres โ€” no partial updates, no data corruption.

The cherry on top?

I learned how Row Level Security (RLS) interacts with RPCs โ€” and how to safely use security definer so the function runs with elevated privileges but still restricts direct table writes.


๐Ÿ” Lesson learned:
Supabase RPCs are an underrated superpower.
They turn your database into a reliable logic layer โ€” atomic, consistent, and secure.


๐Ÿ’ฌ Curious if others have used Supabase RPCs or security definer for production logic?
Would love to hear your experiences.

Thanks for kind reading!

Top comments (0)