@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.
Files changed (133) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +73 -47
  3. package/package.json +4 -4
  4. package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
  5. package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
  6. package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
  7. package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
  8. package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
  9. package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
  10. package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
  11. package/skills/supabase-architecture-variants/SKILL.md +395 -216
  12. package/skills/supabase-architecture-variants/references/errors.md +11 -0
  13. package/skills/supabase-architecture-variants/references/examples.md +12 -0
  14. package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
  15. package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
  16. package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
  17. package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
  18. package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
  19. package/skills/supabase-ci-integration/SKILL.md +315 -67
  20. package/skills/supabase-ci-integration/references/errors.md +10 -0
  21. package/skills/supabase-ci-integration/references/examples.md +36 -0
  22. package/skills/supabase-ci-integration/references/implementation.md +54 -0
  23. package/skills/supabase-common-errors/SKILL.md +320 -62
  24. package/skills/supabase-common-errors/references/errors.md +53 -0
  25. package/skills/supabase-common-errors/references/examples.md +23 -0
  26. package/skills/supabase-cost-tuning/SKILL.md +365 -131
  27. package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
  28. package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
  29. package/skills/supabase-cost-tuning/references/errors.md +11 -0
  30. package/skills/supabase-cost-tuning/references/examples.md +15 -0
  31. package/skills/supabase-data-handling/SKILL.md +378 -145
  32. package/skills/supabase-data-handling/references/errors.md +11 -0
  33. package/skills/supabase-data-handling/references/examples.md +27 -0
  34. package/skills/supabase-data-handling/references/implementation.md +223 -0
  35. package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
  36. package/skills/supabase-debug-bundle/SKILL.md +267 -73
  37. package/skills/supabase-debug-bundle/references/errors.md +12 -0
  38. package/skills/supabase-debug-bundle/references/examples.md +24 -0
  39. package/skills/supabase-debug-bundle/references/implementation.md +54 -0
  40. package/skills/supabase-deploy-integration/SKILL.md +258 -147
  41. package/skills/supabase-deploy-integration/references/errors.md +11 -0
  42. package/skills/supabase-deploy-integration/references/examples.md +21 -0
  43. package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
  44. package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
  45. package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
  46. package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
  47. package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
  48. package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
  49. package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
  50. package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
  51. package/skills/supabase-hello-world/SKILL.md +160 -54
  52. package/skills/supabase-incident-runbook/SKILL.md +453 -131
  53. package/skills/supabase-incident-runbook/references/errors.md +11 -0
  54. package/skills/supabase-incident-runbook/references/examples.md +10 -0
  55. package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
  56. package/skills/supabase-install-auth/SKILL.md +186 -50
  57. package/skills/supabase-install-auth/references/examples.md +102 -0
  58. package/skills/supabase-known-pitfalls/SKILL.md +411 -241
  59. package/skills/supabase-known-pitfalls/references/errors.md +11 -0
  60. package/skills/supabase-known-pitfalls/references/examples.md +12 -0
  61. package/skills/supabase-load-scale/SKILL.md +346 -217
  62. package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
  63. package/skills/supabase-load-scale/references/errors.md +11 -0
  64. package/skills/supabase-load-scale/references/examples.md +26 -0
  65. package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
  66. package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
  67. package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
  68. package/skills/supabase-local-dev-loop/SKILL.md +272 -73
  69. package/skills/supabase-local-dev-loop/references/errors.md +11 -0
  70. package/skills/supabase-local-dev-loop/references/examples.md +21 -0
  71. package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
  72. package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
  73. package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
  74. package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
  75. package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
  76. package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
  77. package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
  78. package/skills/supabase-multi-env-setup/SKILL.md +393 -152
  79. package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
  80. package/skills/supabase-multi-env-setup/references/errors.md +11 -0
  81. package/skills/supabase-multi-env-setup/references/examples.md +11 -0
  82. package/skills/supabase-observability/SKILL.md +318 -196
  83. package/skills/supabase-observability/references/alert-configuration.md +40 -0
  84. package/skills/supabase-observability/references/errors.md +11 -0
  85. package/skills/supabase-observability/references/examples.md +13 -0
  86. package/skills/supabase-observability/references/metrics-collection.md +65 -0
  87. package/skills/supabase-performance-tuning/SKILL.md +304 -160
  88. package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
  89. package/skills/supabase-performance-tuning/references/errors.md +11 -0
  90. package/skills/supabase-performance-tuning/references/examples.md +13 -0
  91. package/skills/supabase-policy-guardrails/SKILL.md +248 -221
  92. package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
  93. package/skills/supabase-policy-guardrails/references/errors.md +11 -0
  94. package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
  95. package/skills/supabase-policy-guardrails/references/examples.md +10 -0
  96. package/skills/supabase-prod-checklist/SKILL.md +474 -84
  97. package/skills/supabase-prod-checklist/references/errors.md +63 -0
  98. package/skills/supabase-prod-checklist/references/examples.md +153 -0
  99. package/skills/supabase-prod-checklist/references/implementation.md +113 -0
  100. package/skills/supabase-rate-limits/SKILL.md +311 -98
  101. package/skills/supabase-rate-limits/references/errors.md +11 -0
  102. package/skills/supabase-rate-limits/references/examples.md +46 -0
  103. package/skills/supabase-rate-limits/references/implementation.md +66 -0
  104. package/skills/supabase-reference-architecture/SKILL.md +249 -182
  105. package/skills/supabase-reference-architecture/references/errors.md +29 -0
  106. package/skills/supabase-reference-architecture/references/examples.md +116 -0
  107. package/skills/supabase-reference-architecture/references/key-components.md +244 -0
  108. package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
  109. package/skills/supabase-reliability-patterns/SKILL.md +229 -234
  110. package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
  111. package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
  112. package/skills/supabase-reliability-patterns/references/errors.md +11 -0
  113. package/skills/supabase-reliability-patterns/references/examples.md +11 -0
  114. package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
  115. package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
  116. package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
  117. package/skills/supabase-sdk-patterns/SKILL.md +388 -99
  118. package/skills/supabase-sdk-patterns/references/errors.md +11 -0
  119. package/skills/supabase-sdk-patterns/references/examples.md +45 -0
  120. package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
  121. package/skills/supabase-security-basics/SKILL.md +282 -102
  122. package/skills/supabase-security-basics/references/errors.md +10 -0
  123. package/skills/supabase-security-basics/references/examples.md +70 -0
  124. package/skills/supabase-security-basics/references/implementation.md +39 -0
  125. package/skills/supabase-upgrade-migration/SKILL.md +248 -66
  126. package/skills/supabase-upgrade-migration/references/errors.md +10 -0
  127. package/skills/supabase-upgrade-migration/references/examples.md +51 -0
  128. package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
  129. package/skills/supabase-webhooks-events/SKILL.md +412 -138
  130. package/skills/supabase-webhooks-events/references/errors.md +55 -0
  131. package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
  132. package/skills/supabase-webhooks-events/references/examples.md +133 -0
  133. 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
- Implement Supabase webhook signature validation and event handling.
5
- Use when setting up webhook endpoints, implementing signature verification,
6
- or handling Supabase event notifications securely.
7
- Trigger with phrases like "supabase webhook", "supabase events",
8
- "supabase webhook signature", "handle supabase events", "supabase notifications".
9
- allowed-tools: Read, Write, Edit, Bash(curl:*)
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
- ## Prerequisites
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
- ## Webhook Endpoint Setup
40
+ ## Prerequisites
27
41
 
28
- ### Express.js
29
- ```typescript
30
- import express from 'express';
31
- import crypto from 'crypto';
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
- const app = express();
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
- // IMPORTANT: Raw body needed for signature verification
36
- app.post('/webhooks/supabase',
37
- express.raw({ type: 'application/json' }),
38
- async (req, res) => {
39
- const signature = req.headers['x-supabase-signature'] as string;
40
- const timestamp = req.headers['x-supabase-timestamp'] as string;
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
- if (!verifySupabaseSignature(req.body, signature, timestamp)) {
43
- return res.status(401).json({ error: 'Invalid signature' });
44
- }
131
+ ### Inspect pg_net Responses
45
132
 
46
- const event = JSON.parse(req.body.toString());
47
- await handleSupabaseEvent(event);
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
- res.status(200).json({ received: true });
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
- function verifySupabaseSignature(
58
- payload: Buffer,
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
- timestamp: string
61
- ): boolean {
62
- const secret = process.env.SUPABASE_WEBHOOK_SECRET!;
63
-
64
- // Reject old timestamps (replay attack protection)
65
- const timestampAge = Date.now() - parseInt(timestamp) * 1000;
66
- if (timestampAge > 300000) { // 5 minutes
67
- console.error('Webhook timestamp too old');
68
- return false;
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
- // Compute expected signature
72
- const signedPayload = `${timestamp}.${payload.toString()}`;
73
- const expectedSignature = crypto
74
- .createHmac('sha256', secret)
75
- .update(signedPayload)
76
- .digest('hex');
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
- ## Event Handler Pattern
250
+ ### Idempotent Event Processing
87
251
 
88
- ```typescript
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
- interface SupabaseEvent {
92
- id: string;
93
- type: SupabaseEventType;
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
- const eventHandlers: Record<SupabaseEventType, (data: any) => Promise<void>> = {
99
- 'resource.created': async (data) => { /* handle */ },
100
- 'resource.updated': async (data) => { /* handle */ },
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
- async function handleSupabaseEvent(event: SupabaseEvent): Promise<void> {
105
- const handler = eventHandlers[event.type];
262
+ const supabase = createClient(
263
+ Deno.env.get("SUPABASE_URL")!,
264
+ Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
265
+ );
106
266
 
107
- if (!handler) {
108
- console.log(`Unhandled event type: ${event.type}`);
109
- return;
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
- try {
113
- await handler(event.data);
114
- console.log(`Processed ${event.type}: ${event.id}`);
115
- } catch (error) {
116
- console.error(`Failed to process ${event.type}: ${event.id}`, error);
117
- throw error; // Rethrow to trigger retry
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
- ## Idempotency Handling
310
+ ## Step 3 — Postgres LISTEN/NOTIFY and Realtime as Event Source
123
311
 
124
- ```typescript
125
- import { Redis } from 'ioredis';
312
+ ### Postgres LISTEN/NOTIFY for Lightweight Pub/Sub
126
313
 
127
- const redis = new Redis(process.env.REDIS_URL);
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
- async function isEventProcessed(eventId: string): Promise<boolean> {
130
- const key = `supabase:event:${eventId}`;
131
- const exists = await redis.exists(key);
132
- return exists === 1;
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
- async function markEventProcessed(eventId: string): Promise<void> {
136
- const key = `supabase:event:${eventId}`;
137
- await redis.set(key, '1', 'EX', 86400 * 7); // 7 days TTL
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
- ## Webhook Testing
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
- ```bash
144
- # Use Supabase CLI to send test events
145
- supabase functions invoke webhook-handler
345
+ await client.query("LISTEN db_changes");
146
346
 
147
- # Or use webhook.site for debugging
148
- curl -X POST https://webhook.site/your-uuid \
149
- -H "Content-Type: application/json" \
150
- -d '{"type": "resource.created", "data": {}}'
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
- ## Instructions
353
+ ### Realtime `postgres_changes` as Client-Side Event Source
154
354
 
155
- ### Step 1: Register Webhook Endpoint
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
- ### Step 2: Implement Signature Verification
159
- Use the signature verification code to validate incoming webhooks.
357
+ ```typescript
358
+ import { createClient } from "@supabase/supabase-js";
160
359
 
161
- ### Step 3: Handle Events
162
- Implement handlers for each event type your application needs.
360
+ const supabase = createClient(
361
+ process.env.SUPABASE_URL!,
362
+ process.env.SUPABASE_ANON_KEY!
363
+ );
163
364
 
164
- ### Step 4: Add Idempotency
165
- Prevent duplicate processing with event ID tracking.
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
- - Secure webhook endpoint
169
- - Signature validation enabled
170
- - Event handlers implemented
171
- - Replay attack protection active
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
- | Issue | Cause | Solution |
175
- |-------|-------|----------|
176
- | Invalid signature | Wrong secret | Verify webhook secret |
177
- | Timestamp rejected | Clock drift | Check server time sync |
178
- | Duplicate events | Missing idempotency | Implement event ID tracking |
179
- | Handler timeout | Slow processing | Use async queue |
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
- ### Testing Webhooks Locally
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
- # Send test webhook
189
- curl -X POST https://your-ngrok-url/webhooks/supabase \
190
- -H "Content-Type: application/json" \
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
- - [Supabase Webhooks Guide](https://supabase.com/docs/webhooks)
196
- - [Webhook Security Best Practices](https://supabase.com/docs/webhooks/security)
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
- For performance optimization, see `supabase-performance-tuning`.
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)*