@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Event Handler Pattern
|
|
2
|
+
|
|
3
|
+
## Typed Event Dispatcher
|
|
4
|
+
|
|
5
|
+
A registry-based pattern for routing Supabase webhook events to typed handlers. This eliminates switch statements and makes adding new event handlers declarative.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// types.ts
|
|
9
|
+
type EventType = "INSERT" | "UPDATE" | "DELETE";
|
|
10
|
+
|
|
11
|
+
interface WebhookPayload<T = Record<string, unknown>> {
|
|
12
|
+
type: EventType;
|
|
13
|
+
table: string;
|
|
14
|
+
schema: string;
|
|
15
|
+
record: T;
|
|
16
|
+
old_record: T | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type EventHandler<T = Record<string, unknown>> = (
|
|
20
|
+
payload: WebhookPayload<T>
|
|
21
|
+
) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
interface HandlerRegistration {
|
|
24
|
+
table: string;
|
|
25
|
+
event: EventType | "*";
|
|
26
|
+
handler: EventHandler;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Dispatcher Implementation
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// event-dispatcher.ts
|
|
34
|
+
class EventDispatcher {
|
|
35
|
+
private handlers: HandlerRegistration[] = [];
|
|
36
|
+
|
|
37
|
+
on(table: string, event: EventType | "*", handler: EventHandler): void {
|
|
38
|
+
this.handlers.push({ table, event, handler });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async dispatch(payload: WebhookPayload): Promise<void> {
|
|
42
|
+
const matched = this.handlers.filter(
|
|
43
|
+
(h) =>
|
|
44
|
+
h.table === payload.table &&
|
|
45
|
+
(h.event === "*" || h.event === payload.type)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (matched.length === 0) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`No handler for ${payload.type} on ${payload.table}`
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run all matched handlers concurrently
|
|
56
|
+
const results = await Promise.allSettled(
|
|
57
|
+
matched.map((h) => h.handler(payload))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
for (const result of results) {
|
|
61
|
+
if (result.status === "rejected") {
|
|
62
|
+
console.error("Handler failed:", result.reason);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Register handlers
|
|
73
|
+
const dispatcher = new EventDispatcher();
|
|
74
|
+
|
|
75
|
+
dispatcher.on("orders", "INSERT", async (payload) => {
|
|
76
|
+
await sendOrderConfirmationEmail(payload.record.email);
|
|
77
|
+
await updateInventoryCount(payload.record.product_id, -1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
dispatcher.on("orders", "UPDATE", async (payload) => {
|
|
81
|
+
if (payload.old_record?.status !== payload.record.status) {
|
|
82
|
+
await notifyStatusChange(payload.record);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
dispatcher.on("profiles", "*", async (payload) => {
|
|
87
|
+
await syncToExternalCRM(payload.record);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// In your Edge Function serve() handler:
|
|
91
|
+
serve(async (req) => {
|
|
92
|
+
const payload: WebhookPayload = await req.json();
|
|
93
|
+
await dispatcher.dispatch(payload);
|
|
94
|
+
return new Response(JSON.stringify({ ok: true }));
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Benefits
|
|
99
|
+
|
|
100
|
+
- **Type safety**: each handler receives typed payloads
|
|
101
|
+
- **Separation of concerns**: one handler per side effect
|
|
102
|
+
- **Testable**: handlers are pure async functions, easy to unit test
|
|
103
|
+
- **Extensible**: add new handlers without modifying dispatch logic
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
## Testing Webhooks Locally
|
|
4
|
+
|
|
5
|
+
### 1. Start Supabase Locally
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
supabase start
|
|
9
|
+
# Note the API URL and service_role key from output
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### 2. Expose Local Edge Function with ngrok
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Serve the Edge Function locally
|
|
16
|
+
supabase functions serve on-order-created --env-file .env.local
|
|
17
|
+
|
|
18
|
+
# In another terminal, expose it
|
|
19
|
+
ngrok http 54321
|
|
20
|
+
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 3. Create the Trigger Pointing to ngrok
|
|
24
|
+
|
|
25
|
+
```sql
|
|
26
|
+
-- In local psql or Supabase SQL editor
|
|
27
|
+
CREATE OR REPLACE FUNCTION public.test_webhook()
|
|
28
|
+
RETURNS trigger AS $$
|
|
29
|
+
BEGIN
|
|
30
|
+
PERFORM net.http_post(
|
|
31
|
+
url := 'https://abc123.ngrok.io/functions/v1/on-order-created',
|
|
32
|
+
headers := '{"Content-Type": "application/json"}'::jsonb,
|
|
33
|
+
body := jsonb_build_object('type', TG_OP, 'record', row_to_json(NEW)::jsonb)
|
|
34
|
+
);
|
|
35
|
+
RETURN NEW;
|
|
36
|
+
END;
|
|
37
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
38
|
+
|
|
39
|
+
CREATE TRIGGER test_webhook_trigger
|
|
40
|
+
AFTER INSERT ON public.orders
|
|
41
|
+
FOR EACH ROW EXECUTE FUNCTION public.test_webhook();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. Trigger the Event
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Insert a row to fire the webhook
|
|
48
|
+
curl -X POST 'http://localhost:54321/rest/v1/orders' \
|
|
49
|
+
-H "apikey: <anon-key>" \
|
|
50
|
+
-H "Authorization: Bearer <anon-key>" \
|
|
51
|
+
-H "Content-Type: application/json" \
|
|
52
|
+
-d '{"product_id": 1, "quantity": 2, "status": "pending"}'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 5. Verify in ngrok Dashboard
|
|
56
|
+
|
|
57
|
+
Open `http://localhost:4040` to see the webhook request in ngrok's inspector.
|
|
58
|
+
|
|
59
|
+
## Manual Webhook Testing with curl
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Simulate an INSERT webhook payload
|
|
63
|
+
curl -X POST http://localhost:54321/functions/v1/on-order-created \
|
|
64
|
+
-H "Content-Type: application/json" \
|
|
65
|
+
-H "Authorization: Bearer <service-role-key>" \
|
|
66
|
+
-d '{
|
|
67
|
+
"type": "INSERT",
|
|
68
|
+
"table": "orders",
|
|
69
|
+
"schema": "public",
|
|
70
|
+
"record": {"id": 42, "product_id": 1, "quantity": 2, "status": "pending"},
|
|
71
|
+
"old_record": null
|
|
72
|
+
}'
|
|
73
|
+
|
|
74
|
+
# Simulate an UPDATE webhook payload
|
|
75
|
+
curl -X POST http://localhost:54321/functions/v1/on-order-created \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
-H "Authorization: Bearer <service-role-key>" \
|
|
78
|
+
-d '{
|
|
79
|
+
"type": "UPDATE",
|
|
80
|
+
"table": "orders",
|
|
81
|
+
"schema": "public",
|
|
82
|
+
"record": {"id": 42, "product_id": 1, "quantity": 2, "status": "shipped"},
|
|
83
|
+
"old_record": {"id": 42, "product_id": 1, "quantity": 2, "status": "pending"}
|
|
84
|
+
}'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Realtime Subscription Test (Browser Console)
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// Paste in browser console with Supabase JS loaded
|
|
91
|
+
const { createClient } = supabase;
|
|
92
|
+
const client = createClient(
|
|
93
|
+
"http://localhost:54321",
|
|
94
|
+
"<anon-key>"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
client
|
|
98
|
+
.channel("test")
|
|
99
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "orders" },
|
|
100
|
+
(payload) => console.log("Realtime event:", payload)
|
|
101
|
+
)
|
|
102
|
+
.subscribe((status) => console.log("Status:", status));
|
|
103
|
+
|
|
104
|
+
// Now insert a row via curl or the dashboard — you should see the event logged
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Auth Hook Test (Custom JWT Claims)
|
|
108
|
+
|
|
109
|
+
```sql
|
|
110
|
+
-- Create a user role mapping
|
|
111
|
+
INSERT INTO public.user_roles (user_id, role) VALUES
|
|
112
|
+
('<user-uuid>', 'admin');
|
|
113
|
+
|
|
114
|
+
-- Create the custom claims hook
|
|
115
|
+
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
|
|
116
|
+
RETURNS jsonb AS $$
|
|
117
|
+
DECLARE
|
|
118
|
+
user_role text;
|
|
119
|
+
BEGIN
|
|
120
|
+
SELECT role INTO user_role
|
|
121
|
+
FROM public.user_roles
|
|
122
|
+
WHERE user_id = (event->>'user_id')::uuid;
|
|
123
|
+
|
|
124
|
+
event := jsonb_set(event, '{claims,user_role}', to_jsonb(COALESCE(user_role, 'user')));
|
|
125
|
+
RETURN event;
|
|
126
|
+
END;
|
|
127
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
128
|
+
|
|
129
|
+
-- Enable in Dashboard > Auth > Hooks > Custom Access Token
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Signature Verification
|
|
2
|
+
|
|
3
|
+
## Why Verify Webhook Signatures
|
|
4
|
+
|
|
5
|
+
Any public Edge Function URL can receive HTTP requests from anyone. Without signature verification, an attacker could send fake webhook payloads to trigger unintended side effects (creating orders, modifying data, sending emails). Always verify the HMAC signature before processing.
|
|
6
|
+
|
|
7
|
+
## Deno (Edge Functions)
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Deno-native using Web Crypto API (no npm dependencies)
|
|
11
|
+
async function verifyWebhookSignature(
|
|
12
|
+
rawBody: string,
|
|
13
|
+
signature: string,
|
|
14
|
+
secret: string
|
|
15
|
+
): Promise<boolean> {
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
|
|
18
|
+
// Import the secret as an HMAC key
|
|
19
|
+
const key = await crypto.subtle.importKey(
|
|
20
|
+
"raw",
|
|
21
|
+
encoder.encode(secret),
|
|
22
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
23
|
+
false,
|
|
24
|
+
["sign"]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Sign the raw body
|
|
28
|
+
const signed = await crypto.subtle.sign(
|
|
29
|
+
"HMAC",
|
|
30
|
+
key,
|
|
31
|
+
encoder.encode(rawBody)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Convert to hex string
|
|
35
|
+
const expected = Array.from(new Uint8Array(signed))
|
|
36
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
37
|
+
.join("");
|
|
38
|
+
|
|
39
|
+
// Constant-time comparison to prevent timing attacks
|
|
40
|
+
if (signature.length !== expected.length) return false;
|
|
41
|
+
let mismatch = 0;
|
|
42
|
+
for (let i = 0; i < signature.length; i++) {
|
|
43
|
+
mismatch |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
44
|
+
}
|
|
45
|
+
return mismatch === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Usage in Edge Function
|
|
49
|
+
serve(async (req) => {
|
|
50
|
+
const secret = Deno.env.get("WEBHOOK_SECRET")!;
|
|
51
|
+
const body = await req.text();
|
|
52
|
+
const sig = req.headers.get("x-webhook-signature") ?? "";
|
|
53
|
+
|
|
54
|
+
if (!(await verifyWebhookSignature(body, sig, secret))) {
|
|
55
|
+
return new Response("Unauthorized", { status: 401 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = JSON.parse(body);
|
|
59
|
+
// ... process verified payload
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Node.js (External Webhook Receivers)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import crypto from "node:crypto";
|
|
67
|
+
|
|
68
|
+
function verifySupabaseSignature(
|
|
69
|
+
rawBody: Buffer | string,
|
|
70
|
+
signature: string,
|
|
71
|
+
timestamp: string,
|
|
72
|
+
secret: string
|
|
73
|
+
): boolean {
|
|
74
|
+
// Reject old timestamps (replay attack protection — 5 min window)
|
|
75
|
+
const age = Date.now() - parseInt(timestamp, 10) * 1000;
|
|
76
|
+
if (age > 300_000) {
|
|
77
|
+
console.error("Webhook timestamp too old:", age, "ms");
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compute expected HMAC using timestamp.body format
|
|
82
|
+
const signedPayload = `${timestamp}.${rawBody.toString()}`;
|
|
83
|
+
const expected = crypto
|
|
84
|
+
.createHmac("sha256", secret)
|
|
85
|
+
.update(signedPayload)
|
|
86
|
+
.digest("hex");
|
|
87
|
+
|
|
88
|
+
// Timing-safe comparison
|
|
89
|
+
return crypto.timingSafeEqual(
|
|
90
|
+
Buffer.from(signature),
|
|
91
|
+
Buffer.from(expected)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Usage in Express
|
|
96
|
+
app.post("/webhook/supabase", (req, res) => {
|
|
97
|
+
const signature = req.headers["x-webhook-signature"] as string;
|
|
98
|
+
const timestamp = req.headers["x-webhook-timestamp"] as string;
|
|
99
|
+
|
|
100
|
+
if (!verifySupabaseSignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
|
|
101
|
+
return res.status(401).json({ error: "Invalid signature" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payload = JSON.parse(req.body.toString());
|
|
105
|
+
// ... process verified payload
|
|
106
|
+
res.json({ ok: true });
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Generating the Webhook Secret
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Generate a secure random secret
|
|
114
|
+
openssl rand -hex 32
|
|
115
|
+
# Example: a1b2c3d4e5f6...
|
|
116
|
+
|
|
117
|
+
# Set as Edge Function secret
|
|
118
|
+
supabase secrets set WEBHOOK_SECRET=a1b2c3d4e5f6...
|
|
119
|
+
|
|
120
|
+
# Set in the trigger function (use Supabase Vault for production)
|
|
121
|
+
-- Dashboard > SQL Editor
|
|
122
|
+
SELECT vault.create_secret('a1b2c3d4e5f6...', 'webhook_secret');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Signing Outgoing Webhooks from Triggers
|
|
126
|
+
|
|
127
|
+
If you need the trigger to sign the payload so the receiver can verify:
|
|
128
|
+
|
|
129
|
+
```sql
|
|
130
|
+
CREATE OR REPLACE FUNCTION public.signed_webhook()
|
|
131
|
+
RETURNS trigger AS $$
|
|
132
|
+
DECLARE
|
|
133
|
+
payload jsonb;
|
|
134
|
+
secret text;
|
|
135
|
+
signature text;
|
|
136
|
+
BEGIN
|
|
137
|
+
payload := jsonb_build_object('type', TG_OP, 'record', row_to_json(NEW)::jsonb);
|
|
138
|
+
|
|
139
|
+
-- Retrieve secret from Vault
|
|
140
|
+
SELECT decrypted_secret INTO secret
|
|
141
|
+
FROM vault.decrypted_secrets
|
|
142
|
+
WHERE name = 'webhook_secret';
|
|
143
|
+
|
|
144
|
+
-- Compute HMAC-SHA256 signature
|
|
145
|
+
signature := encode(
|
|
146
|
+
hmac(payload::text, secret, 'sha256'),
|
|
147
|
+
'hex'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
PERFORM net.http_post(
|
|
151
|
+
url := 'https://your-receiver.com/webhook',
|
|
152
|
+
headers := jsonb_build_object(
|
|
153
|
+
'Content-Type', 'application/json',
|
|
154
|
+
'x-webhook-signature', signature
|
|
155
|
+
),
|
|
156
|
+
body := payload
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
RETURN NEW;
|
|
160
|
+
END;
|
|
161
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|