@rebasepro/cli 0.6.0 → 0.7.0

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 (51) hide show
  1. package/README.md +0 -1
  2. package/dist/commands/api-keys.d.ts +1 -0
  3. package/dist/commands/generate_sdk.d.ts +1 -1
  4. package/dist/commands/init.d.ts +13 -0
  5. package/dist/index.es.js +463 -105
  6. package/dist/index.es.js.map +1 -1
  7. package/package.json +22 -18
  8. package/templates/template/backend/package.json +0 -1
  9. package/templates/template/backend/src/index.ts +1 -1
  10. package/templates/template/config/collections/posts.ts +1 -3
  11. package/templates/template/config/collections/tags.ts +1 -3
  12. package/templates/template/config/collections/users.ts +1 -4
  13. package/templates/template/package.json +0 -1
  14. package/templates/template/scripts/example.ts +4 -1
  15. package/dist/index.cjs +0 -1948
  16. package/dist/index.cjs.map +0 -1
  17. package/skills/rebase-api/SKILL.md +0 -662
  18. package/skills/rebase-api/references/.gitkeep +0 -3
  19. package/skills/rebase-auth/SKILL.md +0 -1143
  20. package/skills/rebase-auth/references/.gitkeep +0 -3
  21. package/skills/rebase-backend-postgres/SKILL.md +0 -633
  22. package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
  23. package/skills/rebase-basics/SKILL.md +0 -749
  24. package/skills/rebase-basics/references/.gitkeep +0 -3
  25. package/skills/rebase-collections/SKILL.md +0 -1328
  26. package/skills/rebase-collections/references/.gitkeep +0 -3
  27. package/skills/rebase-cron-jobs/SKILL.md +0 -699
  28. package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
  29. package/skills/rebase-custom-functions/SKILL.md +0 -233
  30. package/skills/rebase-deployment/SKILL.md +0 -583
  31. package/skills/rebase-deployment/references/.gitkeep +0 -3
  32. package/skills/rebase-design-language/SKILL.md +0 -664
  33. package/skills/rebase-email/SKILL.md +0 -701
  34. package/skills/rebase-email/references/.gitkeep +0 -1
  35. package/skills/rebase-entity-history/SKILL.md +0 -485
  36. package/skills/rebase-entity-history/references/.gitkeep +0 -1
  37. package/skills/rebase-local-env-setup/SKILL.md +0 -189
  38. package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
  39. package/skills/rebase-realtime/SKILL.md +0 -755
  40. package/skills/rebase-realtime/references/.gitkeep +0 -3
  41. package/skills/rebase-sdk/SKILL.md +0 -594
  42. package/skills/rebase-sdk/references/.gitkeep +0 -0
  43. package/skills/rebase-storage/SKILL.md +0 -765
  44. package/skills/rebase-storage/references/.gitkeep +0 -3
  45. package/skills/rebase-studio/SKILL.md +0 -746
  46. package/skills/rebase-studio/references/.gitkeep +0 -3
  47. package/skills/rebase-ui-components/SKILL.md +0 -1411
  48. package/skills/rebase-ui-components/references/.gitkeep +0 -3
  49. package/skills/rebase-webhooks/SKILL.md +0 -623
  50. package/skills/rebase-webhooks/references/.gitkeep +0 -1
  51. package/templates/template/backend/drizzle.config.ts +0 -51
@@ -1,3 +0,0 @@
1
- # References
2
-
3
- Reference documentation for the rebase-ui-components skill. Add detailed reference files here.
@@ -1,623 +0,0 @@
1
- ---
2
- name: rebase-webhooks
3
- description: Guide for sending outbound HTTP webhooks on entity changes in a Rebase backend. Use this skill when the user needs to notify external services on INSERT, UPDATE, or DELETE, verify HMAC signatures, understand retry/backoff behavior, or build a webhook receiver.
4
- ---
5
-
6
- # Rebase Webhooks
7
-
8
- > **IMPORTANT FOR AGENTS**: The `WebhookDispatcher` is a **standalone service class** exported from `@rebasepro/server-core`. It is **not** auto-wired into the backend init pipeline — you must instantiate it yourself and call `onEntityChange()` from your application code (e.g. entity callbacks, custom functions, or cron jobs). Do NOT look for a `webhooks` key in `RebaseBackendConfig`.
9
-
10
- > **IMPORTANT FOR AGENTS**: Webhooks are **outbound** HTTP POST requests sent by your Rebase backend to external URLs. They are NOT inbound endpoints. To receive webhooks FROM external services, use Rebase custom functions instead (see `rebase-custom-functions` skill).
11
-
12
- ## Overview
13
-
14
- Rebase provides a `WebhookDispatcher` class for sending HTTP webhook notifications when entities change. It handles:
15
-
16
- - **Table + event matching** — Only dispatches to webhooks whose `table` and `events` match
17
- - **HMAC-SHA256 signing** — Optional payload signing for receiver verification
18
- - **Automatic retries** — Up to 3 attempts with exponential backoff (1s → 5s → 15s)
19
- - **Custom headers** — Attach authorization tokens or other headers to outbound requests
20
- - **10-second timeout** — Requests are aborted if the receiver doesn't respond in time
21
- - **Multiple webhooks** — Multiple webhooks can match the same entity change
22
-
23
- ## Setup
24
-
25
- ### Import and Instantiate
26
-
27
- ```typescript
28
- import { WebhookDispatcher } from "@rebasepro/server-core/services/webhook-service";
29
- import type { WebhookConfig } from "@rebasepro/server-core/services/webhook-service";
30
-
31
- const dispatcher = new WebhookDispatcher();
32
- ```
33
-
34
- ### Register Webhooks
35
-
36
- Call `setWebhooks()` with an array of `WebhookConfig` objects. Only **enabled** webhooks are kept — disabled ones are filtered out automatically.
37
-
38
- ```typescript
39
- dispatcher.setWebhooks([
40
- {
41
- id: "wh_orders_new",
42
- url: "https://my-service.com/webhooks/orders",
43
- secret: process.env.WEBHOOK_SECRET,
44
- events: ["INSERT"],
45
- table: "orders",
46
- enabled: true,
47
- },
48
- {
49
- id: "wh_users_all",
50
- url: "https://analytics.example.com/hooks/users",
51
- secret: "my-hmac-secret",
52
- headers: {
53
- "Authorization": "Bearer sk_live_abc123",
54
- "X-Source": "rebase",
55
- },
56
- events: ["INSERT", "UPDATE", "DELETE"],
57
- table: "users",
58
- enabled: true,
59
- },
60
- {
61
- id: "wh_disabled_example",
62
- url: "https://example.com/hook",
63
- events: ["INSERT"],
64
- table: "posts",
65
- enabled: false, // ← filtered out, never dispatched
66
- },
67
- ]);
68
- ```
69
-
70
- > **WARNING FOR AGENTS**: `setWebhooks()` **replaces** the entire webhook list each time it's called. It does NOT append. Disabled webhooks (`enabled: false`) are silently dropped.
71
-
72
- ### Dispatch on Entity Changes
73
-
74
- Call `onEntityChange()` whenever an entity is created, updated, or deleted. The dispatcher checks all registered webhooks and fires matching ones.
75
-
76
- ```typescript
77
- const results = await dispatcher.onEntityChange(
78
- "orders", // table name
79
- "INSERT", // event type
80
- "order_abc123", // entity ID
81
- { id: "order_abc123", total: 99 } // the entity record
82
- );
83
- ```
84
-
85
- For **UPDATE** events, pass the previous entity as the 5th argument:
86
-
87
- ```typescript
88
- const results = await dispatcher.onEntityChange(
89
- "users",
90
- "UPDATE",
91
- "user_42",
92
- { id: "user_42", name: "Updated Name" }, // current record
93
- { id: "user_42", name: "Original Name" } // previous record
94
- );
95
- ```
96
-
97
- For **DELETE** events, the entity may be `null`:
98
-
99
- ```typescript
100
- const results = await dispatcher.onEntityChange(
101
- "orders",
102
- "DELETE",
103
- "order_abc123",
104
- null // entity was deleted
105
- );
106
- ```
107
-
108
- ## WebhookConfig Interface
109
-
110
- ```typescript
111
- interface WebhookConfig {
112
- id: string;
113
- url: string;
114
- secret?: string;
115
- headers?: Record<string, string>;
116
- events: string[];
117
- table: string;
118
- enabled: boolean;
119
- }
120
- ```
121
-
122
- | Property | Type | Required | Default | Description |
123
- |----------|------|----------|---------|-------------|
124
- | `id` | `string` | ✅ | — | Unique identifier for this webhook. Sent in the `X-Webhook-Id` header. |
125
- | `url` | `string` | ✅ | — | The endpoint URL that receives the POST request. |
126
- | `secret` | `string` | ❌ | `undefined` | HMAC-SHA256 signing secret. When set, `X-Webhook-Signature` header is included. |
127
- | `headers` | `Record<string, string>` | ❌ | `undefined` | Custom headers merged into every outbound request (e.g. `Authorization`). |
128
- | `events` | `string[]` | ✅ | — | Event types to match. Values: `"INSERT"`, `"UPDATE"`, `"DELETE"`. |
129
- | `table` | `string` | ✅ | — | The database table name to match against. |
130
- | `enabled` | `boolean` | ✅ | — | Whether this webhook is active. Disabled webhooks are filtered out by `setWebhooks()`. |
131
-
132
- ## WebhookDeliveryResult Interface
133
-
134
- Every call to `onEntityChange()` returns an array of `WebhookDeliveryResult` — one per matching webhook.
135
-
136
- ```typescript
137
- interface WebhookDeliveryResult {
138
- webhookId: string;
139
- event: string;
140
- payload: Record<string, unknown>;
141
- statusCode: number;
142
- responseBody: string;
143
- success: boolean;
144
- attemptNumber: number;
145
- }
146
- ```
147
-
148
- | Property | Type | Description |
149
- |----------|------|-------------|
150
- | `webhookId` | `string` | The `id` of the webhook that was dispatched. |
151
- | `event` | `string` | The event type (`INSERT`, `UPDATE`, `DELETE`). |
152
- | `payload` | `Record<string, unknown>` | The full JSON payload that was sent. |
153
- | `statusCode` | `number` | HTTP status code from the receiver. `0` if the request failed entirely (network error, timeout). |
154
- | `responseBody` | `string` | Response body from the receiver, **truncated to 1000 characters**. Error message if the request failed. |
155
- | `success` | `boolean` | `true` if the final attempt returned a 2xx status code. |
156
- | `attemptNumber` | `number` | The attempt number of the final delivery (1–3). |
157
-
158
- ## Webhook Payload Format
159
-
160
- Every webhook sends a `POST` request with a JSON body containing:
161
-
162
- ```json
163
- {
164
- "type": "INSERT",
165
- "table": "orders",
166
- "record": {
167
- "id": "order_abc123",
168
- "total": 99,
169
- "status": "pending"
170
- },
171
- "old_record": null,
172
- "schema": "public",
173
- "timestamp": "2025-01-15T12:00:00.000Z"
174
- }
175
- ```
176
-
177
- | Field | Type | Description |
178
- |-------|------|-------------|
179
- | `type` | `string` | The event type: `"INSERT"`, `"UPDATE"`, or `"DELETE"`. |
180
- | `table` | `string` | The database table name where the change occurred. |
181
- | `record` | `object \| null` | The current state of the entity. `null` for DELETE events if no entity data available. |
182
- | `old_record` | `object \| undefined` | The previous state of the entity. **Only present for `UPDATE` events.** `undefined` for INSERT and DELETE. |
183
- | `schema` | `string` | Always `"public"`. |
184
- | `timestamp` | `string` | ISO 8601 timestamp of when the webhook was dispatched. |
185
-
186
- ### Payload by Event Type
187
-
188
- | Event | `record` | `old_record` |
189
- |-------|----------|--------------|
190
- | `INSERT` | The newly created entity | `undefined` |
191
- | `UPDATE` | The updated entity (new state) | The entity before the update (old state) |
192
- | `DELETE` | The deleted entity (or `null`) | `undefined` |
193
-
194
- ## HTTP Headers
195
-
196
- Every webhook request includes these headers:
197
-
198
- | Header | Value | Always Present |
199
- |--------|-------|----------------|
200
- | `Content-Type` | `application/json` | ✅ |
201
- | `X-Webhook-Id` | The webhook's `id` (e.g. `wh_orders_new`) | ✅ |
202
- | `X-Webhook-Event` | The event type (`INSERT`, `UPDATE`, `DELETE`) | ✅ |
203
- | `X-Webhook-Delivery` | A unique UUID for this specific delivery attempt | ✅ |
204
- | `X-Webhook-Attempt` | The attempt number (`"1"`, `"2"`, or `"3"`) | ✅ |
205
- | `X-Webhook-Signature` | HMAC-SHA256 signature (e.g. `sha256=abc123...`) | Only when `secret` is set |
206
- | *(custom headers)* | Values from `headers` config | Only when `headers` is set |
207
-
208
- > **IMPORTANT FOR AGENTS**: Custom headers from the `headers` config are **merged after** the standard headers. This means custom headers can **override** built-in headers like `Content-Type` if the same key is used.
209
-
210
- ## HMAC Signature Verification
211
-
212
- When a webhook has a `secret` configured, every request includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature of the raw JSON body.
213
-
214
- ### How Signing Works
215
-
216
- 1. The payload is serialized to JSON: `JSON.stringify(payload)`
217
- 2. An HMAC is computed: `createHmac("sha256", secret).update(body).digest("hex")`
218
- 3. The signature is sent as: `sha256=<hex_digest>`
219
-
220
- ### Verifying Signatures (Receiver Side)
221
-
222
- > **IMPORTANT FOR AGENTS**: Always use **timing-safe comparison** (`timingSafeEqual`) when verifying HMAC signatures to prevent timing attacks. Never use `===` for signature comparison.
223
-
224
- #### Node.js / Express Example
225
-
226
- ```typescript
227
- import { createHmac, timingSafeEqual } from "crypto";
228
- import express from "express";
229
-
230
- const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
231
-
232
- function verifyWebhookSignature(
233
- body: string,
234
- signatureHeader: string,
235
- secret: string
236
- ): boolean {
237
- const expectedSig = createHmac("sha256", secret)
238
- .update(body)
239
- .digest("hex");
240
- const expected = `sha256=${expectedSig}`;
241
-
242
- if (signatureHeader.length !== expected.length) return false;
243
-
244
- return timingSafeEqual(
245
- Buffer.from(signatureHeader),
246
- Buffer.from(expected)
247
- );
248
- }
249
-
250
- const app = express();
251
-
252
- // IMPORTANT: Use raw body for signature verification
253
- app.post("/webhooks/rebase", express.raw({ type: "application/json" }), (req, res) => {
254
- const signature = req.headers["x-webhook-signature"] as string;
255
- const rawBody = req.body.toString("utf-8");
256
-
257
- if (!signature || !verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
258
- return res.status(401).json({ error: "Invalid signature" });
259
- }
260
-
261
- const payload = JSON.parse(rawBody);
262
-
263
- console.log(`Received ${payload.type} on ${payload.table}`, payload.record);
264
-
265
- // Process the webhook...
266
- res.status(200).json({ received: true });
267
- });
268
- ```
269
-
270
- #### Rebase Custom Function Receiver
271
-
272
- ```typescript
273
- // backend/functions/webhook-receiver.ts
274
- import type { RebaseFunctionDefinition } from "@rebasepro/types";
275
- import { createHmac, timingSafeEqual } from "crypto";
276
-
277
- const WEBHOOK_SECRET = process.env.EXTERNAL_WEBHOOK_SECRET!;
278
-
279
- const fn: RebaseFunctionDefinition = {
280
- method: "POST",
281
- path: "/incoming-webhook",
282
- public: true, // External services need to reach this
283
-
284
- async handler(ctx) {
285
- const signature = ctx.req.header("x-webhook-signature");
286
- const rawBody = await ctx.req.text();
287
-
288
- if (!signature) {
289
- return ctx.json({ error: "Missing signature" }, 401);
290
- }
291
-
292
- const expectedSig = createHmac("sha256", WEBHOOK_SECRET)
293
- .update(rawBody)
294
- .digest("hex");
295
- const expected = `sha256=${expectedSig}`;
296
-
297
- if (
298
- signature.length !== expected.length ||
299
- !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
300
- ) {
301
- return ctx.json({ error: "Invalid signature" }, 401);
302
- }
303
-
304
- const payload = JSON.parse(rawBody);
305
- // Process payload...
306
-
307
- return ctx.json({ received: true });
308
- },
309
- };
310
-
311
- export default fn;
312
- ```
313
-
314
- ## Retry Logic
315
-
316
- The `WebhookDispatcher` automatically retries failed deliveries with **exponential backoff**.
317
-
318
- ### Retry Behavior
319
-
320
- | Attempt | Delay Before | Total Elapsed |
321
- |---------|-------------|---------------|
322
- | 1 | — (immediate) | 0s |
323
- | 2 | 1,000 ms (1s) | ~1s |
324
- | 3 | 5,000 ms (5s) | ~6s |
325
-
326
- - **Max retries:** 3 attempts total
327
- - **Backoff delays:** `[1000, 5000, 15000]` ms (the 15s delay is defined but never used since the 3rd attempt is the last)
328
- - A delivery is **successful** if the HTTP response status code is `>= 200 && < 300`
329
- - A delivery **fails** if the status code is outside 2xx range OR if a network/timeout error occurs
330
- - If any attempt succeeds, retries stop immediately and the successful result is returned
331
- - If all 3 attempts fail, the result from the **last attempt** is returned
332
-
333
- ### What Triggers a Retry
334
-
335
- | Scenario | Retries? |
336
- |----------|----------|
337
- | HTTP 2xx response | ❌ No — success, stop immediately |
338
- | HTTP 4xx response (e.g. 400, 404) | ✅ Yes — retries up to max |
339
- | HTTP 5xx response (e.g. 500, 502) | ✅ Yes — retries up to max |
340
- | Network error (DNS failure, connection refused) | ✅ Yes — `statusCode: 0` |
341
- | Request timeout (>10 seconds) | ✅ Yes — `statusCode: 0`, `AbortError` |
342
-
343
- > **WARNING FOR AGENTS**: The dispatcher retries on **all** non-2xx status codes, including 4xx client errors. There is no distinction between retryable and non-retryable HTTP errors. If the receiver returns 400 Bad Request, the dispatcher will still retry twice more.
344
-
345
- ### Timeout
346
-
347
- Each individual delivery attempt has a **10-second timeout** enforced via `AbortController`:
348
-
349
- ```typescript
350
- const controller = new AbortController();
351
- const timeout = setTimeout(() => controller.abort(), 10000); // 10s
352
- ```
353
-
354
- If the receiver does not respond within 10 seconds, the request is aborted and treated as a failure (triggering a retry if attempts remain).
355
-
356
- ## Integration Patterns
357
-
358
- ### With Entity Callbacks
359
-
360
- The most common pattern is to wire the dispatcher into Rebase entity callbacks so webhooks fire automatically on CRUD operations:
361
-
362
- ```typescript
363
- // backend/collections/orders.ts
364
- import type { EntityCollection, EntityCallbacks } from "@rebasepro/types";
365
- import { WebhookDispatcher } from "@rebasepro/server-core/services/webhook-service";
366
-
367
- const dispatcher = new WebhookDispatcher();
368
- dispatcher.setWebhooks([
369
- {
370
- id: "wh_orders",
371
- url: process.env.ORDERS_WEBHOOK_URL!,
372
- secret: process.env.ORDERS_WEBHOOK_SECRET,
373
- events: ["INSERT", "UPDATE", "DELETE"],
374
- table: "orders",
375
- enabled: true,
376
- },
377
- ]);
378
-
379
- const callbacks: EntityCallbacks = {
380
- afterCreate: async ({ entity, collection }) => {
381
- await dispatcher.onEntityChange(
382
- collection.path,
383
- "INSERT",
384
- entity.id,
385
- entity
386
- );
387
- },
388
- afterUpdate: async ({ entity, previousEntity, collection }) => {
389
- await dispatcher.onEntityChange(
390
- collection.path,
391
- "UPDATE",
392
- entity.id,
393
- entity,
394
- previousEntity
395
- );
396
- },
397
- afterDelete: async ({ entityId, collection }) => {
398
- await dispatcher.onEntityChange(
399
- collection.path,
400
- "DELETE",
401
- entityId,
402
- null
403
- );
404
- },
405
- };
406
-
407
- const ordersCollection: EntityCollection = {
408
- name: "Orders",
409
- path: "orders",
410
- callbacks,
411
- // ... properties
412
- };
413
-
414
- export default ordersCollection;
415
- ```
416
-
417
- ### With Custom Functions (Manual Dispatch)
418
-
419
- Trigger webhooks from a custom function endpoint:
420
-
421
- ```typescript
422
- // backend/functions/process-payment.ts
423
- import type { RebaseFunctionDefinition } from "@rebasepro/types";
424
- import { WebhookDispatcher } from "@rebasepro/server-core/services/webhook-service";
425
-
426
- const dispatcher = new WebhookDispatcher();
427
- dispatcher.setWebhooks([
428
- {
429
- id: "wh_payments",
430
- url: "https://accounting.example.com/hooks/payments",
431
- secret: process.env.PAYMENT_WEBHOOK_SECRET,
432
- events: ["INSERT"],
433
- table: "payments",
434
- enabled: true,
435
- },
436
- ]);
437
-
438
- const fn: RebaseFunctionDefinition = {
439
- method: "POST",
440
- path: "/process-payment",
441
-
442
- async handler(ctx) {
443
- const { orderId, amount } = await ctx.req.json();
444
-
445
- // Create the payment record
446
- const payment = await ctx.client.collection("payments").create({
447
- orderId,
448
- amount,
449
- status: "completed",
450
- });
451
-
452
- // Manually dispatch the webhook
453
- const results = await dispatcher.onEntityChange(
454
- "payments",
455
- "INSERT",
456
- payment.id,
457
- payment
458
- );
459
-
460
- const allSuccess = results.every(r => r.success);
461
-
462
- return ctx.json({
463
- payment,
464
- webhooksDelivered: results.length,
465
- webhooksSucceeded: allSuccess,
466
- });
467
- },
468
- };
469
-
470
- export default fn;
471
- ```
472
-
473
- ### Shared Dispatcher Instance
474
-
475
- For applications with many collections, create a single shared dispatcher:
476
-
477
- ```typescript
478
- // backend/lib/webhooks.ts
479
- import { WebhookDispatcher } from "@rebasepro/server-core/services/webhook-service";
480
- import type { WebhookConfig } from "@rebasepro/server-core/services/webhook-service";
481
-
482
- const dispatcher = new WebhookDispatcher();
483
-
484
- // Load webhook configs from environment or database
485
- const webhooks: WebhookConfig[] = [
486
- {
487
- id: "wh_orders",
488
- url: process.env.ORDERS_WEBHOOK_URL!,
489
- secret: process.env.WEBHOOK_SECRET,
490
- events: ["INSERT", "UPDATE", "DELETE"],
491
- table: "orders",
492
- enabled: !!process.env.ORDERS_WEBHOOK_URL,
493
- },
494
- {
495
- id: "wh_users",
496
- url: process.env.USERS_WEBHOOK_URL!,
497
- secret: process.env.WEBHOOK_SECRET,
498
- events: ["INSERT", "UPDATE"],
499
- table: "users",
500
- enabled: !!process.env.USERS_WEBHOOK_URL,
501
- },
502
- {
503
- id: "wh_analytics",
504
- url: "https://analytics.example.com/ingest",
505
- events: ["INSERT", "UPDATE", "DELETE"],
506
- table: "orders",
507
- headers: { "Authorization": `Bearer ${process.env.ANALYTICS_API_KEY}` },
508
- enabled: true,
509
- },
510
- ];
511
-
512
- dispatcher.setWebhooks(webhooks);
513
-
514
- export { dispatcher };
515
- ```
516
-
517
- Then import it from any callback or function:
518
-
519
- ```typescript
520
- import { dispatcher } from "../lib/webhooks";
521
-
522
- // In an entity callback:
523
- afterCreate: async ({ entity, collection }) => {
524
- await dispatcher.onEntityChange(collection.path, "INSERT", entity.id, entity);
525
- },
526
- ```
527
-
528
- ## WebhookDispatcher API Reference
529
-
530
- ### `setWebhooks(webhooks: WebhookConfig[]): void`
531
-
532
- Registers the list of webhooks to watch. Filters out any with `enabled: false`. **Replaces** the entire list — not additive.
533
-
534
- | Parameter | Type | Description |
535
- |-----------|------|-------------|
536
- | `webhooks` | `WebhookConfig[]` | Array of webhook configurations. |
537
-
538
- ### `onEntityChange(table, event, entityId, entity, previousEntity?): Promise<WebhookDeliveryResult[]>`
539
-
540
- Checks all registered webhooks for matching `table` + `event`, and dispatches to each match.
541
-
542
- | Parameter | Type | Description |
543
- |-----------|------|-------------|
544
- | `table` | `string` | The database table name (e.g. `"orders"`). |
545
- | `event` | `"INSERT" \| "UPDATE" \| "DELETE"` | The type of entity change. |
546
- | `entityId` | `string` | The unique ID of the changed entity. |
547
- | `entity` | `Record<string, unknown> \| null` | The current entity data. May be `null` for deletes. |
548
- | `previousEntity` | `Record<string, unknown> \| null` | *(Optional)* The previous entity state. Only relevant for `UPDATE` events — included as `old_record` in the payload. |
549
-
550
- **Returns:** `Promise<WebhookDeliveryResult[]>` — One result per matching webhook. Empty array if no webhooks match.
551
-
552
- ## Event Types
553
-
554
- The dispatcher supports three event types, passed as the `event` parameter to `onEntityChange()`:
555
-
556
- | Event | When to Use | `record` Contains | `old_record` Contains |
557
- |-------|-------------|--------------------|-----------------------|
558
- | `INSERT` | A new entity was created | The new entity | `undefined` |
559
- | `UPDATE` | An existing entity was modified | The updated entity | The entity before update |
560
- | `DELETE` | An entity was removed | The deleted entity (or `null`) | `undefined` |
561
-
562
- ## Error Handling
563
-
564
- ### Checking Delivery Results
565
-
566
- ```typescript
567
- const results = await dispatcher.onEntityChange("orders", "INSERT", id, entity);
568
-
569
- for (const result of results) {
570
- if (!result.success) {
571
- console.error(
572
- `Webhook ${result.webhookId} failed after ${result.attemptNumber} attempts. ` +
573
- `Status: ${result.statusCode}, Body: ${result.responseBody}`
574
- );
575
- }
576
- }
577
- ```
578
-
579
- ### Failure Scenarios
580
-
581
- | Scenario | `statusCode` | `responseBody` | `success` |
582
- |----------|-------------|----------------|-----------|
583
- | Receiver returns 200 | `200` | Response text (truncated to 1000 chars) | `true` |
584
- | Receiver returns 500 | `500` | `"Internal Server Error"` | `false` |
585
- | DNS resolution failure | `0` | `"getaddrinfo ENOTFOUND ..."` | `false` |
586
- | Connection refused | `0` | `"connect ECONNREFUSED ..."` | `false` |
587
- | 10s timeout exceeded | `0` | `"The operation was aborted"` | `false` |
588
- | Network error | `0` | Error message (truncated to 1000 chars) | `false` |
589
-
590
- ### Non-Blocking Pattern
591
-
592
- If you don't want webhook delivery to block your API response, use fire-and-forget:
593
-
594
- ```typescript
595
- afterCreate: async ({ entity, collection }) => {
596
- // Fire-and-forget — don't await
597
- dispatcher.onEntityChange(collection.path, "INSERT", entity.id, entity)
598
- .catch(err => console.error("Webhook dispatch error:", err));
599
- },
600
- ```
601
-
602
- ## Edge Cases and Gotchas
603
-
604
- | Scenario | Behavior |
605
- |----------|----------|
606
- | **No webhooks match the table/event** | Returns empty array `[]` immediately. No HTTP requests made. |
607
- | **Webhook URL is unreachable** | Retries 3 times with backoff (1s, 5s delays). Final result has `statusCode: 0`. |
608
- | **Receiver returns 4xx (e.g. 400, 404)** | Treated as failure and retried (no distinction between 4xx and 5xx). |
609
- | **Receiver takes >10 seconds** | Request aborted via `AbortController`. Treated as failure, retried. |
610
- | **Multiple webhooks match same event** | All matching webhooks are dispatched **sequentially** (not in parallel). |
611
- | **Disabled webhook in `setWebhooks()`** | Silently filtered out. Never dispatched. |
612
- | **`setWebhooks()` called multiple times** | Replaces the entire list each time. Previous webhooks are discarded. |
613
- | **`secret` is not set** | No `X-Webhook-Signature` header is sent. Payload is not signed. |
614
- | **Custom header conflicts with built-in** | Custom headers override built-in headers (they are spread after). |
615
- | **Response body very large** | Truncated to **1000 characters** in the `WebhookDeliveryResult`. |
616
- | **`entity` is `null` for DELETE** | Sent as `"record": null` in the payload. |
617
- | **`previousEntity` not passed for UPDATE** | `old_record` is `undefined` (omitted from JSON). |
618
- | **`onEntityChange` is async** | The entire retry sequence is awaited. For INSERT with 3 failed attempts: ~6s of waiting. |
619
-
620
- ## References
621
-
622
- - **Documentation:** [rebase.pro/docs/backend/webhooks](https://rebase.pro/docs/backend/webhooks)
623
- - **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)
@@ -1,51 +0,0 @@
1
- import * as dotenv from "dotenv";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- import { defineConfig } from "drizzle-kit";
5
- import { tables } from "./src/schema.generated";
6
- import { getTableName } from "drizzle-orm";
7
- import { getTableConfig, PgTable } from "drizzle-orm/pg-core";
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- // Load .env from the project root (single file for the entire project)
13
- dotenv.config({ path: path.resolve(__dirname, "../.env") });
14
-
15
- if (!process.env.DATABASE_URL) {
16
- throw new Error("DATABASE_URL is not set. Make sure .env file exists in the project root and contains DATABASE_URL");
17
- }
18
-
19
- // Extract table names from the generated schema, excluding system tables in the 'rebase' schema.
20
- // This ensures drizzle-kit ONLY manages tables defined in the schema, and ignores the system tables
21
- // managed by Rebase's own bootstrapper.
22
- const tableNames = Object.values(tables)
23
- .filter(table => getTableConfig(table as PgTable).schema !== "rebase")
24
- .map(table => getTableName(table as PgTable));
25
-
26
- // Dynamically extract all schemas defined in the generated tables (excluding 'rebase') to ensure Drizzle Kit manages them.
27
- const schemas = Array.from(new Set(
28
- Object.values(tables)
29
- .map(table => getTableConfig(table as PgTable).schema || "public")
30
- .filter(schema => schema !== "rebase")
31
- ));
32
-
33
- export default defineConfig({
34
- schema: "./src/schema.generated.ts",
35
- out: "./drizzle",
36
- dialect: "postgresql",
37
- dbCredentials: {
38
- url: process.env.DATABASE_URL
39
- },
40
- // Only manage tables defined in the generated schema.
41
- // Unmapped tables in the database are completely ignored.
42
- tablesFilter: tableNames,
43
- // Restrict drizzle-kit to the schemas defined in our collections
44
- schemaFilter: schemas.length > 0 ? schemas : ["public"],
45
- // Prevent drizzle-kit from managing roles not defined in the schema
46
- entities: {
47
- roles: false
48
- },
49
- // If PostGIS or other extensions create helper tables, ignore them
50
- extensionsFilters: ["postgis"]
51
- });