@intentsolutionsio/supabase-pack 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +73 -47
- package/package.json +4 -4
- package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
- package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
- package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
- package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
- package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
- package/skills/supabase-architecture-variants/SKILL.md +395 -216
- package/skills/supabase-architecture-variants/references/errors.md +11 -0
- package/skills/supabase-architecture-variants/references/examples.md +12 -0
- package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
- package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
- package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
- package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
- package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
- package/skills/supabase-ci-integration/SKILL.md +315 -67
- package/skills/supabase-ci-integration/references/errors.md +10 -0
- package/skills/supabase-ci-integration/references/examples.md +36 -0
- package/skills/supabase-ci-integration/references/implementation.md +54 -0
- package/skills/supabase-common-errors/SKILL.md +320 -62
- package/skills/supabase-common-errors/references/errors.md +53 -0
- package/skills/supabase-common-errors/references/examples.md +23 -0
- package/skills/supabase-cost-tuning/SKILL.md +365 -131
- package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
- package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
- package/skills/supabase-cost-tuning/references/errors.md +11 -0
- package/skills/supabase-cost-tuning/references/examples.md +15 -0
- package/skills/supabase-data-handling/SKILL.md +378 -145
- package/skills/supabase-data-handling/references/errors.md +11 -0
- package/skills/supabase-data-handling/references/examples.md +27 -0
- package/skills/supabase-data-handling/references/implementation.md +223 -0
- package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
- package/skills/supabase-debug-bundle/SKILL.md +267 -73
- package/skills/supabase-debug-bundle/references/errors.md +12 -0
- package/skills/supabase-debug-bundle/references/examples.md +24 -0
- package/skills/supabase-debug-bundle/references/implementation.md +54 -0
- package/skills/supabase-deploy-integration/SKILL.md +258 -147
- package/skills/supabase-deploy-integration/references/errors.md +11 -0
- package/skills/supabase-deploy-integration/references/examples.md +21 -0
- package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
- package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
- package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
- package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
- package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
- package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
- package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
- package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
- package/skills/supabase-hello-world/SKILL.md +160 -54
- package/skills/supabase-incident-runbook/SKILL.md +453 -131
- package/skills/supabase-incident-runbook/references/errors.md +11 -0
- package/skills/supabase-incident-runbook/references/examples.md +10 -0
- package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
- package/skills/supabase-install-auth/SKILL.md +186 -50
- package/skills/supabase-install-auth/references/examples.md +102 -0
- package/skills/supabase-known-pitfalls/SKILL.md +411 -241
- package/skills/supabase-known-pitfalls/references/errors.md +11 -0
- package/skills/supabase-known-pitfalls/references/examples.md +12 -0
- package/skills/supabase-load-scale/SKILL.md +346 -217
- package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
- package/skills/supabase-load-scale/references/errors.md +11 -0
- package/skills/supabase-load-scale/references/examples.md +26 -0
- package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
- package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
- package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
- package/skills/supabase-local-dev-loop/SKILL.md +272 -73
- package/skills/supabase-local-dev-loop/references/errors.md +11 -0
- package/skills/supabase-local-dev-loop/references/examples.md +21 -0
- package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
- package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
- package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
- package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
- package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
- package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
- package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
- package/skills/supabase-multi-env-setup/SKILL.md +393 -152
- package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
- package/skills/supabase-multi-env-setup/references/errors.md +11 -0
- package/skills/supabase-multi-env-setup/references/examples.md +11 -0
- package/skills/supabase-observability/SKILL.md +318 -196
- package/skills/supabase-observability/references/alert-configuration.md +40 -0
- package/skills/supabase-observability/references/errors.md +11 -0
- package/skills/supabase-observability/references/examples.md +13 -0
- package/skills/supabase-observability/references/metrics-collection.md +65 -0
- package/skills/supabase-performance-tuning/SKILL.md +304 -160
- package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
- package/skills/supabase-performance-tuning/references/errors.md +11 -0
- package/skills/supabase-performance-tuning/references/examples.md +13 -0
- package/skills/supabase-policy-guardrails/SKILL.md +248 -221
- package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
- package/skills/supabase-policy-guardrails/references/errors.md +11 -0
- package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
- package/skills/supabase-policy-guardrails/references/examples.md +10 -0
- package/skills/supabase-prod-checklist/SKILL.md +474 -84
- package/skills/supabase-prod-checklist/references/errors.md +63 -0
- package/skills/supabase-prod-checklist/references/examples.md +153 -0
- package/skills/supabase-prod-checklist/references/implementation.md +113 -0
- package/skills/supabase-rate-limits/SKILL.md +311 -98
- package/skills/supabase-rate-limits/references/errors.md +11 -0
- package/skills/supabase-rate-limits/references/examples.md +46 -0
- package/skills/supabase-rate-limits/references/implementation.md +66 -0
- package/skills/supabase-reference-architecture/SKILL.md +249 -182
- package/skills/supabase-reference-architecture/references/errors.md +29 -0
- package/skills/supabase-reference-architecture/references/examples.md +116 -0
- package/skills/supabase-reference-architecture/references/key-components.md +244 -0
- package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
- package/skills/supabase-reliability-patterns/SKILL.md +229 -234
- package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
- package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
- package/skills/supabase-reliability-patterns/references/errors.md +11 -0
- package/skills/supabase-reliability-patterns/references/examples.md +11 -0
- package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
- package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
- package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
- package/skills/supabase-sdk-patterns/SKILL.md +388 -99
- package/skills/supabase-sdk-patterns/references/errors.md +11 -0
- package/skills/supabase-sdk-patterns/references/examples.md +45 -0
- package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
- package/skills/supabase-security-basics/SKILL.md +282 -102
- package/skills/supabase-security-basics/references/errors.md +10 -0
- package/skills/supabase-security-basics/references/examples.md +70 -0
- package/skills/supabase-security-basics/references/implementation.md +39 -0
- package/skills/supabase-upgrade-migration/SKILL.md +248 -66
- package/skills/supabase-upgrade-migration/references/errors.md +10 -0
- package/skills/supabase-upgrade-migration/references/examples.md +51 -0
- package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
- package/skills/supabase-webhooks-events/SKILL.md +412 -138
- package/skills/supabase-webhooks-events/references/errors.md +55 -0
- package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
- package/skills/supabase-webhooks-events/references/examples.md +133 -0
- package/skills/supabase-webhooks-events/references/signature-verification.md +165 -0
|
@@ -1,199 +1,473 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: supabase-webhooks-events
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
description: 'Implement Supabase database webhooks, pg_net async HTTP, LISTEN/NOTIFY,
|
|
4
|
+
|
|
5
|
+
and Edge Function event handlers with signature verification.
|
|
6
|
+
|
|
7
|
+
Use when setting up database webhooks for INSERT/UPDATE/DELETE events,
|
|
8
|
+
|
|
9
|
+
sending HTTP requests from PostgreSQL triggers, handling Realtime
|
|
10
|
+
|
|
11
|
+
postgres_changes as an event source, or building event-driven architectures.
|
|
12
|
+
|
|
13
|
+
Trigger with phrases like "supabase webhook", "database events",
|
|
14
|
+
|
|
15
|
+
"pg_net trigger", "supabase LISTEN NOTIFY", "webhook signature verify",
|
|
16
|
+
|
|
17
|
+
"supabase event-driven", "supabase_functions.http_request".
|
|
18
|
+
|
|
19
|
+
'
|
|
20
|
+
allowed-tools: Read, Write, Edit, Bash(supabase:*), Bash(curl:*), Bash(psql:*), Grep
|
|
10
21
|
version: 1.0.0
|
|
11
22
|
license: MIT
|
|
12
23
|
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
24
|
+
tags:
|
|
25
|
+
- saas
|
|
26
|
+
- supabase
|
|
27
|
+
- webhooks
|
|
28
|
+
- events
|
|
29
|
+
- triggers
|
|
30
|
+
- pg_net
|
|
31
|
+
- realtime
|
|
32
|
+
compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
|
|
13
33
|
---
|
|
14
|
-
|
|
15
|
-
# Supabase Webhooks & Events
|
|
34
|
+
# Supabase Webhooks & Database Events
|
|
16
35
|
|
|
17
36
|
## Overview
|
|
18
|
-
Securely handle Supabase webhooks with signature validation and replay protection.
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
- Supabase webhook secret configured
|
|
22
|
-
- HTTPS endpoint accessible from internet
|
|
23
|
-
- Understanding of cryptographic signatures
|
|
24
|
-
- Redis or database for idempotency (optional)
|
|
38
|
+
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.
|
|
25
39
|
|
|
26
|
-
##
|
|
40
|
+
## Prerequisites
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
- Supabase project (local or hosted) with `supabase` CLI installed
|
|
43
|
+
- `pg_net` extension enabled: Dashboard > Database > Extensions > search "pg_net" > Enable
|
|
44
|
+
- `@supabase/supabase-js` v2+ installed for client-side patterns
|
|
45
|
+
- Edge Functions deployed for webhook receiver patterns
|
|
46
|
+
|
|
47
|
+
## Step 1 — Database Webhooks with `pg_net` and Trigger Functions
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
### Enable pg_net and Create the Trigger Function
|
|
52
|
+
|
|
53
|
+
```sql
|
|
54
|
+
-- Enable the pg_net extension (one-time)
|
|
55
|
+
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
|
|
56
|
+
|
|
57
|
+
-- Trigger function: POST to an Edge Function on every new order
|
|
58
|
+
CREATE OR REPLACE FUNCTION public.notify_order_created()
|
|
59
|
+
RETURNS trigger AS $$
|
|
60
|
+
BEGIN
|
|
61
|
+
PERFORM net.http_post(
|
|
62
|
+
url := 'https://<project-ref>.supabase.co/functions/v1/on-order-created',
|
|
63
|
+
headers := jsonb_build_object(
|
|
64
|
+
'Content-Type', 'application/json',
|
|
65
|
+
'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key', true)
|
|
66
|
+
),
|
|
67
|
+
body := jsonb_build_object(
|
|
68
|
+
'table', TG_TABLE_NAME,
|
|
69
|
+
'type', TG_OP,
|
|
70
|
+
'record', row_to_json(NEW)::jsonb,
|
|
71
|
+
'old_record', CASE WHEN TG_OP = 'UPDATE' THEN row_to_json(OLD)::jsonb ELSE NULL END
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
RETURN NEW;
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
77
|
+
```
|
|
32
78
|
|
|
33
|
-
|
|
79
|
+
### Attach Triggers for INSERT, UPDATE, DELETE
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- Fire on new rows
|
|
83
|
+
CREATE TRIGGER on_order_created
|
|
84
|
+
AFTER INSERT ON public.orders
|
|
85
|
+
FOR EACH ROW EXECUTE FUNCTION public.notify_order_created();
|
|
86
|
+
|
|
87
|
+
-- Fire on status changes only (conditional trigger)
|
|
88
|
+
CREATE OR REPLACE FUNCTION public.notify_order_status_changed()
|
|
89
|
+
RETURNS trigger AS $$
|
|
90
|
+
BEGIN
|
|
91
|
+
IF OLD.status IS DISTINCT FROM NEW.status THEN
|
|
92
|
+
PERFORM net.http_post(
|
|
93
|
+
url := 'https://<project-ref>.supabase.co/functions/v1/on-status-change',
|
|
94
|
+
headers := '{"Content-Type": "application/json"}'::jsonb,
|
|
95
|
+
body := jsonb_build_object(
|
|
96
|
+
'order_id', NEW.id,
|
|
97
|
+
'old_status', OLD.status,
|
|
98
|
+
'new_status', NEW.status,
|
|
99
|
+
'changed_at', now()
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
END IF;
|
|
103
|
+
RETURN NEW;
|
|
104
|
+
END;
|
|
105
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
106
|
+
|
|
107
|
+
CREATE TRIGGER on_order_status_changed
|
|
108
|
+
AFTER UPDATE ON public.orders
|
|
109
|
+
FOR EACH ROW EXECUTE FUNCTION public.notify_order_status_changed();
|
|
110
|
+
```
|
|
34
111
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
112
|
+
### Using `supabase_functions.http_request()` (Built-in Helper)
|
|
113
|
+
|
|
114
|
+
Supabase provides a built-in wrapper that simplifies calling Edge Functions from triggers without managing headers manually:
|
|
115
|
+
|
|
116
|
+
```sql
|
|
117
|
+
-- This is the function Supabase auto-creates for Dashboard-configured webhooks
|
|
118
|
+
-- You can also call it directly in your own trigger functions
|
|
119
|
+
CREATE TRIGGER on_profile_updated
|
|
120
|
+
AFTER UPDATE ON public.profiles
|
|
121
|
+
FOR EACH ROW
|
|
122
|
+
EXECUTE FUNCTION supabase_functions.http_request(
|
|
123
|
+
'https://<project-ref>.supabase.co/functions/v1/on-profile-update',
|
|
124
|
+
'POST',
|
|
125
|
+
'{"Content-Type": "application/json"}',
|
|
126
|
+
'{}', -- params
|
|
127
|
+
'5000' -- timeout ms
|
|
128
|
+
);
|
|
129
|
+
```
|
|
41
130
|
|
|
42
|
-
|
|
43
|
-
return res.status(401).json({ error: 'Invalid signature' });
|
|
44
|
-
}
|
|
131
|
+
### Inspect pg_net Responses
|
|
45
132
|
|
|
46
|
-
|
|
47
|
-
|
|
133
|
+
```sql
|
|
134
|
+
-- Check recent HTTP responses (retained for 6 hours)
|
|
135
|
+
SELECT id, status_code, content, created
|
|
136
|
+
FROM net._http_response
|
|
137
|
+
ORDER BY created DESC
|
|
138
|
+
LIMIT 10;
|
|
48
139
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
140
|
+
-- Find failed requests
|
|
141
|
+
SELECT id, status_code, content
|
|
142
|
+
FROM net._http_response
|
|
143
|
+
WHERE status_code >= 400
|
|
144
|
+
ORDER BY created DESC;
|
|
52
145
|
```
|
|
53
146
|
|
|
54
|
-
## Signature Verification
|
|
147
|
+
## Step 2 — Edge Function Webhook Receivers with Signature Verification
|
|
148
|
+
|
|
149
|
+
### Webhook Receiver with Signature Verification
|
|
55
150
|
|
|
56
151
|
```typescript
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
// supabase/functions/on-order-created/index.ts
|
|
153
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
154
|
+
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
|
155
|
+
|
|
156
|
+
interface WebhookPayload {
|
|
157
|
+
type: "INSERT" | "UPDATE" | "DELETE";
|
|
158
|
+
table: string;
|
|
159
|
+
record: Record<string, unknown>;
|
|
160
|
+
old_record: Record<string, unknown> | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Verify webhook signature to prevent spoofing
|
|
164
|
+
async function verifySignature(
|
|
165
|
+
body: string,
|
|
59
166
|
signature: string,
|
|
60
|
-
|
|
61
|
-
): boolean {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
secret: string
|
|
168
|
+
): Promise<boolean> {
|
|
169
|
+
const encoder = new TextEncoder();
|
|
170
|
+
const key = await crypto.subtle.importKey(
|
|
171
|
+
"raw",
|
|
172
|
+
encoder.encode(secret),
|
|
173
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
174
|
+
false,
|
|
175
|
+
["sign"]
|
|
176
|
+
);
|
|
177
|
+
const signed = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
178
|
+
const expected = Array.from(new Uint8Array(signed))
|
|
179
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
180
|
+
.join("");
|
|
181
|
+
// Constant-time comparison
|
|
182
|
+
if (signature.length !== expected.length) return false;
|
|
183
|
+
let mismatch = 0;
|
|
184
|
+
for (let i = 0; i < signature.length; i++) {
|
|
185
|
+
mismatch |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
186
|
+
}
|
|
187
|
+
return mismatch === 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
serve(async (req) => {
|
|
191
|
+
// Verify signature if webhook secret is configured
|
|
192
|
+
const webhookSecret = Deno.env.get("WEBHOOK_SECRET");
|
|
193
|
+
const rawBody = await req.text();
|
|
194
|
+
|
|
195
|
+
if (webhookSecret) {
|
|
196
|
+
const signature = req.headers.get("x-webhook-signature") ?? "";
|
|
197
|
+
const valid = await verifySignature(rawBody, signature, webhookSecret);
|
|
198
|
+
if (!valid) {
|
|
199
|
+
return new Response(JSON.stringify({ error: "Invalid signature" }), {
|
|
200
|
+
status: 401,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
69
203
|
}
|
|
70
204
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Timing-safe comparison
|
|
79
|
-
return crypto.timingSafeEqual(
|
|
80
|
-
Buffer.from(signature),
|
|
81
|
-
Buffer.from(expectedSignature)
|
|
205
|
+
const payload: WebhookPayload = JSON.parse(rawBody);
|
|
206
|
+
|
|
207
|
+
const supabase = createClient(
|
|
208
|
+
Deno.env.get("SUPABASE_URL")!,
|
|
209
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
|
210
|
+
{ auth: { autoRefreshToken: false, persistSession: false } }
|
|
82
211
|
);
|
|
83
|
-
|
|
212
|
+
|
|
213
|
+
// Route by event type
|
|
214
|
+
switch (payload.type) {
|
|
215
|
+
case "INSERT": {
|
|
216
|
+
console.log(`New ${payload.table} row:`, payload.record.id);
|
|
217
|
+
|
|
218
|
+
// Example: log event, send notification, update related table
|
|
219
|
+
await supabase.from("audit_log").insert({
|
|
220
|
+
table_name: payload.table,
|
|
221
|
+
action: "INSERT",
|
|
222
|
+
record_id: payload.record.id,
|
|
223
|
+
payload: payload.record,
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "UPDATE": {
|
|
228
|
+
console.log(`Updated ${payload.table}:`, payload.record.id);
|
|
229
|
+
// Compare old and new to detect specific field changes
|
|
230
|
+
if (payload.old_record?.status !== payload.record.status) {
|
|
231
|
+
await supabase.from("notifications").insert({
|
|
232
|
+
user_id: payload.record.user_id,
|
|
233
|
+
message: `Status changed to ${payload.record.status}`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "DELETE": {
|
|
239
|
+
console.log(`Deleted from ${payload.table}:`, payload.old_record?.id);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new Response(JSON.stringify({ received: true }), {
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
});
|
|
247
|
+
});
|
|
84
248
|
```
|
|
85
249
|
|
|
86
|
-
|
|
250
|
+
### Idempotent Event Processing
|
|
87
251
|
|
|
88
|
-
|
|
89
|
-
type SupabaseEventType = 'resource.created' | 'resource.updated' | 'resource.deleted';
|
|
252
|
+
Webhooks may be delivered more than once. Use an idempotency table to prevent duplicate processing:
|
|
90
253
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
data: Record<string, any>;
|
|
95
|
-
created: string;
|
|
96
|
-
}
|
|
254
|
+
```typescript
|
|
255
|
+
// supabase/functions/idempotent-handler/index.ts
|
|
256
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
97
257
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'resource.deleted': async (data) => { /* handle */ }
|
|
102
|
-
};
|
|
258
|
+
serve(async (req) => {
|
|
259
|
+
const payload = await req.json();
|
|
260
|
+
const eventId = `${payload.table}:${payload.type}:${payload.record.id}`;
|
|
103
261
|
|
|
104
|
-
|
|
105
|
-
|
|
262
|
+
const supabase = createClient(
|
|
263
|
+
Deno.env.get("SUPABASE_URL")!,
|
|
264
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
|
265
|
+
);
|
|
106
266
|
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
267
|
+
// Check if already processed (upsert pattern)
|
|
268
|
+
const { data: existing } = await supabase
|
|
269
|
+
.from("processed_events")
|
|
270
|
+
.select("id")
|
|
271
|
+
.eq("event_id", eventId)
|
|
272
|
+
.maybeSingle();
|
|
273
|
+
|
|
274
|
+
if (existing) {
|
|
275
|
+
return new Response(
|
|
276
|
+
JSON.stringify({ skipped: true, reason: "already processed" }),
|
|
277
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
278
|
+
);
|
|
110
279
|
}
|
|
111
280
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
281
|
+
// --- Your business logic here ---
|
|
282
|
+
console.log(`Processing event: ${eventId}`);
|
|
283
|
+
|
|
284
|
+
// Mark as processed (with TTL for cleanup)
|
|
285
|
+
await supabase.from("processed_events").insert({
|
|
286
|
+
event_id: eventId,
|
|
287
|
+
processed_at: new Date().toISOString(),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return new Response(JSON.stringify({ processed: true }), {
|
|
291
|
+
status: 200,
|
|
292
|
+
headers: { "Content-Type": "application/json" },
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```sql
|
|
298
|
+
-- Idempotency table
|
|
299
|
+
CREATE TABLE public.processed_events (
|
|
300
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
301
|
+
event_id text UNIQUE NOT NULL,
|
|
302
|
+
processed_at timestamptz DEFAULT now()
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
-- Auto-cleanup old records (run via pg_cron or scheduled function)
|
|
306
|
+
DELETE FROM public.processed_events
|
|
307
|
+
WHERE processed_at < now() - interval '7 days';
|
|
120
308
|
```
|
|
121
309
|
|
|
122
|
-
##
|
|
310
|
+
## Step 3 — Postgres LISTEN/NOTIFY and Realtime as Event Source
|
|
123
311
|
|
|
124
|
-
|
|
125
|
-
import { Redis } from 'ioredis';
|
|
312
|
+
### Postgres LISTEN/NOTIFY for Lightweight Pub/Sub
|
|
126
313
|
|
|
127
|
-
|
|
314
|
+
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:
|
|
128
315
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
316
|
+
```sql
|
|
317
|
+
-- Trigger function that emits a NOTIFY on row change
|
|
318
|
+
CREATE OR REPLACE FUNCTION public.notify_changes()
|
|
319
|
+
RETURNS trigger AS $$
|
|
320
|
+
BEGIN
|
|
321
|
+
PERFORM pg_notify(
|
|
322
|
+
'db_changes',
|
|
323
|
+
json_build_object(
|
|
324
|
+
'table', TG_TABLE_NAME,
|
|
325
|
+
'op', TG_OP,
|
|
326
|
+
'id', COALESCE(NEW.id, OLD.id)
|
|
327
|
+
)::text
|
|
328
|
+
);
|
|
329
|
+
RETURN COALESCE(NEW, OLD);
|
|
330
|
+
END;
|
|
331
|
+
$$ LANGUAGE plpgsql;
|
|
134
332
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
333
|
+
CREATE TRIGGER orders_notify
|
|
334
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.orders
|
|
335
|
+
FOR EACH ROW EXECUTE FUNCTION public.notify_changes();
|
|
139
336
|
```
|
|
140
337
|
|
|
141
|
-
|
|
338
|
+
```typescript
|
|
339
|
+
// Listen from a Node.js backend using pg driver
|
|
340
|
+
import { Client } from "pg";
|
|
341
|
+
|
|
342
|
+
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
|
343
|
+
await client.connect();
|
|
142
344
|
|
|
143
|
-
|
|
144
|
-
# Use Supabase CLI to send test events
|
|
145
|
-
supabase functions invoke webhook-handler
|
|
345
|
+
await client.query("LISTEN db_changes");
|
|
146
346
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
347
|
+
client.on("notification", (msg) => {
|
|
348
|
+
const payload = JSON.parse(msg.payload!);
|
|
349
|
+
console.log(`${payload.op} on ${payload.table}: id=${payload.id}`);
|
|
350
|
+
});
|
|
151
351
|
```
|
|
152
352
|
|
|
153
|
-
|
|
353
|
+
### Realtime `postgres_changes` as Client-Side Event Source
|
|
154
354
|
|
|
155
|
-
|
|
156
|
-
Configure your webhook URL in the Supabase dashboard.
|
|
355
|
+
Supabase Realtime lets frontend clients subscribe to database changes without polling. Enable Realtime on your table first (Dashboard > Database > Replication).
|
|
157
356
|
|
|
158
|
-
|
|
159
|
-
|
|
357
|
+
```typescript
|
|
358
|
+
import { createClient } from "@supabase/supabase-js";
|
|
160
359
|
|
|
161
|
-
|
|
162
|
-
|
|
360
|
+
const supabase = createClient(
|
|
361
|
+
process.env.SUPABASE_URL!,
|
|
362
|
+
process.env.SUPABASE_ANON_KEY!
|
|
363
|
+
);
|
|
163
364
|
|
|
164
|
-
|
|
165
|
-
|
|
365
|
+
// Subscribe to all changes on the orders table
|
|
366
|
+
const channel = supabase
|
|
367
|
+
.channel("orders-events")
|
|
368
|
+
.on(
|
|
369
|
+
"postgres_changes",
|
|
370
|
+
{
|
|
371
|
+
event: "*", // or 'INSERT' | 'UPDATE' | 'DELETE'
|
|
372
|
+
schema: "public",
|
|
373
|
+
table: "orders",
|
|
374
|
+
filter: "status=eq.pending", // optional: RLS-style filter
|
|
375
|
+
},
|
|
376
|
+
(payload) => {
|
|
377
|
+
console.log("Change type:", payload.eventType);
|
|
378
|
+
console.log("New row:", payload.new);
|
|
379
|
+
console.log("Old row:", payload.old);
|
|
380
|
+
|
|
381
|
+
// React to the change
|
|
382
|
+
switch (payload.eventType) {
|
|
383
|
+
case "INSERT":
|
|
384
|
+
showToast(`New order #${payload.new.id}`);
|
|
385
|
+
break;
|
|
386
|
+
case "UPDATE":
|
|
387
|
+
updateOrderInUI(payload.new);
|
|
388
|
+
break;
|
|
389
|
+
case "DELETE":
|
|
390
|
+
removeOrderFromUI(payload.old.id);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
.subscribe((status) => {
|
|
396
|
+
console.log("Subscription status:", status);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Cleanup when done
|
|
400
|
+
// await supabase.removeChannel(channel);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Event-Driven Architecture: Combining Patterns
|
|
404
|
+
|
|
405
|
+
Use database triggers for server-side workflows and Realtime for client-side UI updates:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
┌──────────────┐ INSERT ┌──────────────────┐
|
|
409
|
+
│ Client │ ──────────────► │ orders table │
|
|
410
|
+
│ (browser) │ └────────┬─────────┘
|
|
411
|
+
│ │ │
|
|
412
|
+
│ Realtime ◄──┼──── postgres_changes ────┤
|
|
413
|
+
│ (UI update) │ │
|
|
414
|
+
└──────────────┘ │ AFTER INSERT trigger
|
|
415
|
+
▼
|
|
416
|
+
┌──────────────────┐
|
|
417
|
+
│ pg_net HTTP POST │
|
|
418
|
+
│ → Edge Function │
|
|
419
|
+
└────────┬─────────┘
|
|
420
|
+
│
|
|
421
|
+
▼
|
|
422
|
+
┌──────────────────┐
|
|
423
|
+
│ Send email │
|
|
424
|
+
│ Update inventory │
|
|
425
|
+
│ Log to audit │
|
|
426
|
+
└──────────────────┘
|
|
427
|
+
```
|
|
166
428
|
|
|
167
429
|
## Output
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
-
|
|
430
|
+
|
|
431
|
+
After implementing these patterns you will have:
|
|
432
|
+
|
|
433
|
+
- Database trigger functions calling Edge Functions via `pg_net` on row changes
|
|
434
|
+
- Conditional triggers that fire only when specific columns change
|
|
435
|
+
- Edge Function webhook receivers with HMAC signature verification
|
|
436
|
+
- Idempotent event processing preventing duplicate side effects
|
|
437
|
+
- LISTEN/NOTIFY channels for lightweight inter-service communication
|
|
438
|
+
- Realtime subscriptions for live client-side UI updates
|
|
439
|
+
- An event-driven architecture combining server and client patterns
|
|
172
440
|
|
|
173
441
|
## Error Handling
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
442
|
+
|
|
443
|
+
| Error | Cause | Fix |
|
|
444
|
+
|-------|-------|-----|
|
|
445
|
+
| `pg_net` returns 404 | Edge Function not deployed or wrong URL | Run `supabase functions deploy <name>` and verify the URL matches |
|
|
446
|
+
| Webhook not firing | Trigger not attached or table not in publication | Check `SELECT * FROM pg_trigger WHERE tgrelid = 'orders'::regclass;` |
|
|
447
|
+
| Duplicate events processed | No idempotency layer | Add `processed_events` table with unique `event_id` constraint |
|
|
448
|
+
| Realtime not receiving | Table not added to Realtime publication | Dashboard > Database > Replication > enable the table |
|
|
449
|
+
| `net._http_response` shows 401 | Invalid or missing auth header | Verify `service_role_key` is set in `app.settings` or vault |
|
|
450
|
+
| NOTIFY payload truncated | Payload exceeds 8000 bytes | Send only IDs in NOTIFY, fetch full record in the listener |
|
|
451
|
+
| Auth hook errors | Function raises exception | Check Dashboard > Logs > Auth; ensure function returns valid JSONB |
|
|
452
|
+
| Trigger silently fails | `SECURITY DEFINER` without `search_path` | Add `SET search_path = public, extensions;` to function |
|
|
180
453
|
|
|
181
454
|
## Examples
|
|
182
455
|
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
# Use ngrok to expose local server
|
|
186
|
-
ngrok http 3000
|
|
456
|
+
See [examples.md](references/examples.md) for local webhook testing with ngrok and curl.
|
|
187
457
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
-d '{"type": "test", "data": {}}'
|
|
192
|
-
```
|
|
458
|
+
See [signature-verification.md](references/signature-verification.md) for Node.js HMAC signature verification.
|
|
459
|
+
|
|
460
|
+
See [event-handler-pattern.md](references/event-handler-pattern.md) for a typed event dispatcher pattern.
|
|
193
461
|
|
|
194
462
|
## Resources
|
|
195
|
-
|
|
196
|
-
- [
|
|
463
|
+
|
|
464
|
+
- [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) — configure via Dashboard or SQL
|
|
465
|
+
- [pg_net Extension](https://supabase.com/docs/guides/database/extensions/pg_net) — async HTTP from PostgreSQL
|
|
466
|
+
- [Edge Functions](https://supabase.com/docs/guides/functions) — Deno-based serverless handlers
|
|
467
|
+
- [Realtime postgres_changes](https://supabase.com/docs/guides/realtime/postgres-changes) — client-side subscriptions
|
|
468
|
+
- [Auth Hooks](https://supabase.com/docs/guides/auth/auth-hooks) — custom JWT claims and login events
|
|
469
|
+
- [supabase-js Reference](https://supabase.com/docs/reference/javascript/subscribe) — `channel().on()` API
|
|
197
470
|
|
|
198
471
|
## Next Steps
|
|
199
|
-
|
|
472
|
+
|
|
473
|
+
For performance optimization of triggers and queries, see `supabase-performance-tuning`. For production hardening including RLS policies on webhook-accessed tables, see `supabase-security-basics`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Error Handling Reference
|
|
2
|
+
|
|
3
|
+
## Database Trigger Errors
|
|
4
|
+
|
|
5
|
+
| Issue | Cause | Solution |
|
|
6
|
+
|-------|-------|----------|
|
|
7
|
+
| Trigger not firing | Trigger disabled or not attached | `ALTER TABLE orders ENABLE TRIGGER on_order_created;` and verify with `SELECT tgname, tgenabled FROM pg_trigger WHERE tgrelid = 'orders'::regclass;` |
|
|
8
|
+
| `pg_net` 404 response | Edge Function URL wrong or not deployed | Run `supabase functions deploy <name>`, check URL matches project ref |
|
|
9
|
+
| `pg_net` 401 response | Missing or invalid Authorization header | Set service role key via `ALTER DATABASE postgres SET app.settings.service_role_key = 'your-key';` |
|
|
10
|
+
| Trigger silently fails | `SECURITY DEFINER` without explicit `search_path` | Add `SET search_path = public, extensions;` to the function definition |
|
|
11
|
+
| `net._http_response` empty | Extension not enabled or wrong schema | `CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;` |
|
|
12
|
+
| Payload too large for NOTIFY | NOTIFY payload exceeds 8000 bytes | Send only record IDs, fetch full data in the listener |
|
|
13
|
+
| Duplicate trigger execution | Multiple triggers on same event | Review with `SELECT * FROM pg_trigger WHERE tgrelid = 'orders'::regclass;` |
|
|
14
|
+
|
|
15
|
+
## Edge Function Errors
|
|
16
|
+
|
|
17
|
+
| Issue | Cause | Solution |
|
|
18
|
+
|-------|-------|----------|
|
|
19
|
+
| Invalid signature (401) | Webhook secret mismatch or encoding issue | Verify both sides use the same secret and UTF-8 encoding |
|
|
20
|
+
| Function timeout | Processing takes too long (default 60s) | Offload heavy work to a queue; return 200 immediately |
|
|
21
|
+
| Duplicate processing | No idempotency check | Add `processed_events` table with unique `event_id` constraint |
|
|
22
|
+
| JSON parse error | Malformed payload from trigger | Wrap `JSON.parse()` in try/catch, return 400 with details |
|
|
23
|
+
| CORS errors on Realtime | Browser blocks WebSocket | Ensure Supabase URL is correct; Realtime uses WSS, not HTTP |
|
|
24
|
+
|
|
25
|
+
## Realtime Subscription Errors
|
|
26
|
+
|
|
27
|
+
| Issue | Cause | Solution |
|
|
28
|
+
|-------|-------|----------|
|
|
29
|
+
| No events received | Table not in Realtime publication | Dashboard > Database > Replication > toggle table on |
|
|
30
|
+
| Subscription status `CHANNEL_ERROR` | RLS policy blocks the subscription | Ensure anon/authenticated role has SELECT on the table |
|
|
31
|
+
| Stale data after reconnect | Client missed events during disconnect | Re-fetch data on `SUBSCRIBED` status callback |
|
|
32
|
+
| Too many channels | Client opening channels without cleanup | Call `supabase.removeChannel(channel)` when unmounting |
|
|
33
|
+
|
|
34
|
+
## Debugging Commands
|
|
35
|
+
|
|
36
|
+
```sql
|
|
37
|
+
-- List all triggers on a table
|
|
38
|
+
SELECT tgname, tgenabled, tgtype, pg_get_triggerdef(oid)
|
|
39
|
+
FROM pg_trigger
|
|
40
|
+
WHERE tgrelid = 'public.orders'::regclass
|
|
41
|
+
AND NOT tgisinternal;
|
|
42
|
+
|
|
43
|
+
-- Check pg_net response log
|
|
44
|
+
SELECT id, status_code, content, created
|
|
45
|
+
FROM net._http_response
|
|
46
|
+
WHERE status_code >= 400
|
|
47
|
+
ORDER BY created DESC
|
|
48
|
+
LIMIT 20;
|
|
49
|
+
|
|
50
|
+
-- Verify pg_net extension is active
|
|
51
|
+
SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_net';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|