TechLead
Lesson 10 of 22
5 min read
Supabase

Supabase Database Functions & Triggers

Master PostgreSQL functions, triggers, and stored procedures in Supabase for automation and data integrity

Database Functions & Triggers

PostgreSQL functions and triggers let you run server-side logic directly in your database. This is powerful for enforcing business rules, automating tasks, and keeping data consistent without relying on application code. Supabase gives you full access to PL/pgSQL, PostgreSQL's procedural language.

🚀 Use Cases

  • Auto-timestamps: Automatically set updated_at on row changes
  • Audit logs: Record every insert, update, and delete
  • Slug generation: Create URL slugs from titles automatically
  • Cascade operations: Update related records when data changes
  • Computed columns: Derive values from other columns

Creating Database Functions

Basic Function

-- A simple function that returns a greeting
CREATE OR REPLACE FUNCTION hello(name text)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN 'Hello, ' || name || '!';
END;
$$;

-- Call it from Supabase client
-- const { data } = await supabase.rpc('hello', { name: 'World' })

Auto-Update Timestamps

-- Function to auto-update the updated_at column
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;

-- Create the trigger on your table
CREATE TRIGGER set_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at();

-- Now every UPDATE on 'posts' automatically sets updated_at

Audit Log Trigger

Track every change to critical tables with an automatic audit log.

-- Create an audit log table
CREATE TABLE audit_log (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  table_name text NOT NULL,
  operation text NOT NULL,
  old_data jsonb,
  new_data jsonb,
  changed_by uuid DEFAULT auth.uid(),
  changed_at timestamptz DEFAULT NOW()
);

-- Generic audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
  INSERT INTO audit_log (table_name, operation, old_data, new_data)
  VALUES (
    TG_TABLE_NAME,
    TG_OP,
    CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END,
    CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END
  );
  RETURN COALESCE(NEW, OLD);
END;
$$;

-- Attach to any table
CREATE TRIGGER audit_posts
  AFTER INSERT OR UPDATE OR DELETE ON posts
  FOR EACH ROW EXECUTE FUNCTION audit_trigger();

Auto-Generate Slugs

-- Function to generate a URL-safe slug from a title
CREATE OR REPLACE FUNCTION generate_slug()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.slug = LOWER(
    REGEXP_REPLACE(
      REGEXP_REPLACE(NEW.title, '[^a-zA-Z0-9\s-]', '', 'g'),
      '\s+', '-', 'g'
    )
  );
  RETURN NEW;
END;
$$;

CREATE TRIGGER set_slug
  BEFORE INSERT ON articles
  FOR EACH ROW
  WHEN (NEW.slug IS NULL)
  EXECUTE FUNCTION generate_slug();

Calling Functions from the Client

// Call an RPC function from your app
const { data, error } = await supabase.rpc('get_user_stats', {
  user_id: 'some-uuid'
})

// Function that returns a set of rows
const { data: leaderboard } = await supabase
  .rpc('get_leaderboard', { limit_count: 10 })

⚠️ SECURITY DEFINER vs SECURITY INVOKER

SECURITY DEFINER runs with the privileges of the function creator (bypasses RLS). SECURITY INVOKER (default) runs with the caller's privileges. Use DEFINER carefully and always set search_path = '' to prevent search path attacks.

💡 Key Takeaways

  • • Use triggers for automatic side effects like timestamps and audit logs
  • • PL/pgSQL functions run inside the database for maximum performance
  • • Call functions from the client with supabase.rpc()
  • • Be careful with SECURITY DEFINER — it bypasses RLS
  • • Triggers fire automatically on INSERT, UPDATE, or DELETE operations

📚 Learn More

Continue Learning