supabase-webhooks-events
Implement Supabase webhook signature validation and event handling. Use when setting up webhook endpoints, implementing signature verification, or handling Supabase event notifications securely. Trigger with phrases like "supabase webhook", "supabase events", "supabase webhook signature", "handle supabase events", "supabase notifications".
Install
mkdir -p .claude/skills/supabase-webhooks-events && curl -L -o skill.zip "https://mcp.directory/api/skills/download/4808" && unzip -o skill.zip -d .claude/skills/supabase-webhooks-events && rm skill.zipInstalls to .claude/skills/supabase-webhooks-events
About this skill
Supabase Webhooks & Database Events
Overview
Supabase offers four complementary event mechanisms: Database Webhooks (trigger-based HTTP calls via pg_net), supabase_functions.http_request() (call Edge Functions from triggers), Postgres LISTEN/NOTIFY (lightweight pub/sub), and Realtime postgres_changes (client-side event subscriptions). This skill covers all four patterns with production-ready code including signature verification, idempotency, and retry handling.
Prerequisites
- Supabase project (local or hosted) with
supabaseCLI installed pg_netextension enabled: Dashboard > Database > Extensions > search "pg_net" > Enable@supabase/supabase-jsv2+ installed for client-side patterns- Edge Functions deployed for webhook receiver patterns
Step 1 — Database Webhooks with pg_net and Trigger Functions
Database webhooks fire HTTP requests when rows change. Under the hood, Supabase uses the pg_net extension to make async, non-blocking HTTP calls from within PostgreSQL.
Enable pg_net and Create the Trigger Function
-- Enable the pg_net extension (one-time)
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
-- Trigger function: POST to an Edge Function on every new order
CREATE OR REPLACE FUNCTION public.notify_order_created()
RETURNS trigger AS $$
BEGIN
PERFORM net.http_post(
url := 'https://<project-ref>.supabase.co/functions/v1/on-order-created',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key', true)
),
body := jsonb_build_object(
'table', TG_TABLE_NAME,
'type', TG_OP,
'record', row_to_json(NEW)::jsonb,
'old_record', CASE WHEN TG_OP = 'UPDATE' THEN row_to_json(OLD)::jsonb ELSE NULL END
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Attach Triggers for INSERT, UPDATE, DELETE
-- Fire on new rows
CREATE TRIGGER on_order_created
AFTER INSERT ON public.orders
FOR EACH ROW EXECUTE FUNCTION public.notify_order_created();
-- Fire on status changes only (conditional trigger)
CREATE OR REPLACE FUNCTION public.notify_order_status_changed()
RETURNS trigger AS $$
BEGIN
IF OLD.status IS DISTINCT FROM NEW.status THEN
PERFORM net.http_post(
url := 'https://<project-ref>.supabase.co/functions/v1/on-status-change',
headers := '{"Content-Type": "application/json"}'::jsonb,
body := jsonb_build_object(
'order_id', NEW.id,
'old_status', OLD.status,
'new_status', NEW.status,
'changed_at', now()
)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_order_status_changed
AFTER UPDATE ON public.orders
FOR EACH ROW EXECUTE FUNCTION public.notify_order_status_changed();
Using supabase_functions.http_request() (Built-in Helper)
Supabase provides a built-in wrapper that simplifies calling Edge Functions from triggers without managing headers manually:
-- This is the function Supabase auto-creates for Dashboard-configured webhooks
-- You can also call it directly in your own trigger functions
CREATE TRIGGER on_profile_updated
AFTER UPDATE ON public.profiles
FOR EACH ROW
EXECUTE FUNCTION supabase_functions.http_request(
'https://<project-ref>.supabase.co/functions/v1/on-profile-update',
'POST',
'{"Content-Type": "application/json"}',
'{}', -- params
'5000' -- timeout ms
);
Inspect pg_net Responses
-- Check recent HTTP responses (retained for 6 hours)
SELECT id, status_code, content, created
FROM net._http_response
ORDER BY created DESC
LIMIT 10;
-- Find failed requests
SELECT id, status_code, content
FROM net._http_response
WHERE status_code >= 400
ORDER BY created DESC;
Step 2 — Edge Function Webhook Receivers with Signature Verification
Webhook Receiver with Signature Verification
// supabase/functions/on-order-created/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
interface WebhookPayload {
type: "INSERT" | "UPDATE" | "DELETE";
table: string;
record: Record<string, unknown>;
old_record: Record<string, unknown> | null;
}
// Verify webhook signature to prevent spoofing
async function verifySignature(
body: string,
signature: string,
secret: string
): Promise<boolean> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signed = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
const expected = Array.from(new Uint8Array(signed))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// Constant-time comparison
if (signature.length !== expected.length) return false;
let mismatch = 0;
for (let i = 0; i < signature.length; i++) {
mismatch |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
}
return mismatch === 0;
}
serve(async (req) => {
// Verify signature if webhook secret is configured
const webhookSecret = Deno.env.get("WEBHOOK_SECRET");
const rawBody = await req.text();
if (webhookSecret) {
const signature = req.headers.get("x-webhook-signature") ?? "";
const valid = await verifySignature(rawBody, signature, webhookSecret);
if (!valid) {
return new Response(JSON.stringify({ error: "Invalid signature" }), {
status: 401,
});
}
}
const payload: WebhookPayload = JSON.parse(rawBody);
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
// Route by event type
switch (payload.type) {
case "INSERT": {
console.log(`New ${payload.table} row:`, payload.record.id);
// Example: log event, send notification, update related table
await supabase.from("audit_log").insert({
table_name: payload.table,
action: "INSERT",
record_id: payload.record.id,
payload: payload.record,
});
break;
}
case "UPDATE": {
console.log(`Updated ${payload.table}:`, payload.record.id);
// Compare old and new to detect specific field changes
if (payload.old_record?.status !== payload.record.status) {
await supabase.from("notifications").insert({
user_id: payload.record.user_id,
message: `Status changed to ${payload.record.status}`,
});
}
break;
}
case "DELETE": {
console.log(`Deleted from ${payload.table}:`, payload.old_record?.id);
break;
}
}
return new Response(JSON.stringify({ received: true }), {
headers: { "Content-Type": "application/json" },
});
});
Idempotent Event Processing
Webhooks may be delivered more than once. Use an idempotency table to prevent duplicate processing:
// supabase/functions/idempotent-handler/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
const payload = await req.json();
const eventId = `${payload.table}:${payload.type}:${payload.record.id}`;
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Check if already processed (upsert pattern)
const { data: existing } = await supabase
.from("processed_events")
.select("id")
.eq("event_id", eventId)
.maybeSingle();
if (existing) {
return new Response(
JSON.stringify({ skipped: true, reason: "already processed" }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// --- Your business logic here ---
console.log(`Processing event: ${eventId}`);
// Mark as processed (with TTL for cleanup)
await supabase.from("processed_events").insert({
event_id: eventId,
processed_at: new Date().toISOString(),
});
return new Response(JSON.stringify({ processed: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
-- Idempotency table
CREATE TABLE public.processed_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_id text UNIQUE NOT NULL,
processed_at timestamptz DEFAULT now()
);
-- Auto-cleanup old records (run via pg_cron or scheduled function)
DELETE FROM public.processed_events
WHERE processed_at < now() - interval '7 days';
Step 3 — Postgres LISTEN/NOTIFY and Realtime as Event Source
Postgres LISTEN/NOTIFY for Lightweight Pub/Sub
LISTEN/NOTIFY is PostgreSQL's built-in pub/sub. It does not persist messages and is best for ephemeral notifications between database functions or connected clients:
-- Trigger function that emits a NOTIFY on row change
CREATE OR REPLACE FUNCTION public.notify_changes()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'db_changes',
json_build_object(
'table', TG_TABLE_NAME,
'op', TG_OP,
'id', COALESCE(NEW.id, OLD.id)
)::text
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_notify
AFTER INSERT OR UPDATE OR DELETE ON public.orders
FOR EACH ROW EXECUTE FUNCTION public.notify_changes();
// Listen from a Node.js backend using pg driver
import { Client } from "pg";
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query("LISTEN db_changes");
client.on("notification", (msg) => {
const payload = JSON.parse(msg.payload!);
console.log(`${payload.op} on ${payload.table}: id=${payload.id}`);
});
Realtime postgres_changes as Client-Side Event Source
Supabase Realtime lets frontend clients subscribe to database changes without polling. Enable Realtime on your tab
Content truncated.
More by jeremylongshore
View all skills by jeremylongshore →You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
drawio-diagrams-enhanced
jgtolentino
Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.
ui-ux-pro-max
nextlevelbuilder
"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
godot
bfollington
This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.
nano-banana-pro
garg-aayush
Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.
fastapi-templates
wshobson
Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.
Related MCP Servers
Browse all serversProtect your MCP with AIM Guard—advanced threat detection software for unauthorized access, botnet, and malware detectio
Spec-Driven Development integrates with IBM DOORS software to track software licenses, automate requirements, and enforc
Integrate notifications into your workflow with DingTalk. Send messages, updates, and team alerts via secure webhook con
Break down complex problems with Sequential Thinking, a structured tool and step by step math solver for dynamic, reflec
Build persistent semantic networks for enterprise & engineering data management. Enable data persistence and memory acro
Boost productivity with Task Master: an AI-powered tool for project management and agile development workflows, integrat
Stay ahead of the MCP ecosystem
Get weekly updates on new skills and servers.