DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase PostgreSQL Functions — Move Complex Logic Server-Side with RPC

Supabase PostgreSQL Functions — Move Complex Logic Server-Side with RPC

Writing complex queries on the Flutter client multiplies network round-trips. PostgreSQL functions + RPC solve this.

Basics: Create a Custom RPC Function

-- supabase/migrations/20280812000000_create_get_user_stats.sql
CREATE OR REPLACE FUNCTION get_user_stats(p_user_id UUID)
RETURNS JSON AS $$
DECLARE
  result JSON;
BEGIN
  SELECT json_build_object(
    'post_count',     (SELECT COUNT(*) FROM posts WHERE user_id = p_user_id),
    'comment_count',  (SELECT COUNT(*) FROM comments WHERE user_id = p_user_id),
    'follower_count', (SELECT COUNT(*) FROM follows WHERE followee_id = p_user_id),
    'joined_at',      (SELECT created_at FROM profiles WHERE id = p_user_id)
  ) INTO result;
  RETURN result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Enter fullscreen mode Exit fullscreen mode

Calling from Flutter

final stats = await supabase.rpc('get_user_stats', params: {
  'p_user_id': supabase.auth.currentUser!.id,
});
print(stats['post_count']);     // 42
print(stats['follower_count']); // 128
Enter fullscreen mode Exit fullscreen mode

Functions That Return Sets

CREATE OR REPLACE FUNCTION search_posts(p_query TEXT, p_limit INT DEFAULT 10)
RETURNS SETOF posts AS $$
BEGIN
  RETURN QUERY
    SELECT *
    FROM posts
    WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', p_query)
    ORDER BY ts_rank(to_tsvector('english', title || ' ' || content),
                     plainto_tsquery('english', p_query)) DESC
    LIMIT p_limit;
END;
$$ LANGUAGE plpgsql STABLE;
Enter fullscreen mode Exit fullscreen mode
final List results = await supabase.rpc('search_posts', params: {
  'p_query': 'Flutter Riverpod',
  'p_limit': 20,
});
Enter fullscreen mode Exit fullscreen mode

Combining with RLS

-- SECURITY DEFINER bypasses RLS to fetch admin stats
CREATE OR REPLACE FUNCTION admin_get_daily_stats(p_date DATE)
RETURNS JSON AS $$
BEGIN
  -- Only service_role may call this function
  IF auth.role() != 'service_role' THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  RETURN json_build_object(
    'new_users',    (SELECT COUNT(*) FROM profiles WHERE created_at::date = p_date),
    'new_posts',    (SELECT COUNT(*) FROM posts WHERE created_at::date = p_date),
    'active_users', (SELECT COUNT(DISTINCT user_id) FROM posts WHERE created_at::date = p_date)
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Enter fullscreen mode Exit fullscreen mode

Summary

Use RPC when   → aggregating multiple tables / full-text search / permission branching
SECURITY DEFINER → admin-only (always verify the caller's role)
STABLE         → side-effect-free functions (query planner can optimize)
Flutter call   → supabase.rpc('func_name', params: {...})
Enter fullscreen mode Exit fullscreen mode

Moving logic server-side simplifies the client and reduces network round-trips.

Top comments (0)