diff --git a/.changeset/disable-fails-deliveries.md b/.changeset/disable-fails-deliveries.md deleted file mode 100644 index 6192a6f..0000000 --- a/.changeset/disable-fails-deliveries.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"hookgres": patch ---- - -Fix endpoint auto-disable: when an endpoint is disabled after failing for `disableEndpointAfterMs`, its queued deliveries are now marked `failed` right away, as documented, instead of staying `pending` until their next retry time (up to 10 hours later, or for good with no worker running) and being resent if the endpoint was re-enabled first. The attempt that disables the endpoint, and any attempt recorded after it was disabled automatically, now report `onAttempt` outcome `"failed"` instead of `"retrying"`. Manually disabled endpoints are unchanged. `attemptCount` no longer counts deliveries failed without an HTTP request (endpoint disabled, payload redacted, or out of attempts after a worker crash). diff --git a/.changeset/mikroorm-recipe.md b/.changeset/mikroorm-recipe.md deleted file mode 100644 index 1d00ea6..0000000 --- a/.changeset/mikroorm-recipe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"hookgres": patch ---- - -The package's `AGENTS.md` now lists `fromMikroORM` among the tested ORM wrappers in [Using your ORM's transaction](https://hookgres.dev/docs/guides/orms#mikroorm). The recipe wraps `fromKysely(em.getKysely())`, so `send()` joins an `em.transactional()` transaction in MikroORM 7. diff --git a/.changeset/npm-description.md b/.changeset/npm-description.md deleted file mode 100644 index 178d378..0000000 --- a/.changeset/npm-description.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"hookgres": patch ---- - -The npm description now says what Hookgres is in the words people search for: a transactional outbox for webhooks on Postgres, with webhook delivery signed by Standard Webhooks. diff --git a/.changeset/observability.md b/.changeset/observability.md deleted file mode 100644 index 2e6779a..0000000 --- a/.changeset/observability.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"hookgres": minor ---- - -Add metrics: - -- `stats({ tenantId?, endpointId? })` returns the queue right now: `pending`, `due`, `delivering`, `oldestDueAt` and `lagMs`, how long the oldest due delivery has waited. It reads the existing partial indexes: about 20ms with 450,000 pending and in-flight deliveries. -- The `opentelemetry` option takes your `@opentelemetry/api` module (1.7+) and exports `hookgres.delivery.attempts`, `hookgres.delivery.attempt.duration`, `hookgres.endpoints.disabled`, `hookgres.worker.errors`, and the `hookgres.queue.pending`, `.due`, `.delivering` and `.lag` gauges. Attributes are low-cardinality (outcome, status code, `error.type`), with no tenant or endpoint. Hookgres doesn't depend on OpenTelemetry; without the option nothing changes. See the new Metrics and traces guide. diff --git a/.changeset/production-hardening-2.md b/.changeset/production-hardening-2.md deleted file mode 100644 index 8eafe16..0000000 --- a/.changeset/production-hardening-2.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"hookgres": minor ---- - -Fix problems found by testing Hookgres at scale and with real integrations: - -- A slow or hanging endpoint no longer holds up every other tenant. Workers now refill slots as deliveries finish instead of waiting for a whole batch. A new `maxInFlightPerEndpoint` option (default: half of `concurrency`) limits how many slots one endpoint can take. In a test, one tenant's 100 queued deliveries to a hung endpoint delayed another tenant's webhook by 10s; now it's delivered in about 50ms. -- Claiming due deliveries no longer slows down with the size of the backlog. Each claim read and sorted every due row, which took 235–324ms per batch at 1M due. It now uses the indexes in order: about 10ms. -- A delivery whose keep-alive connection the receiver closed just as it was reused is retried once on a fresh connection, instead of counting as a failed attempt. -- `send()` rejects a payload containing a `Map`, a `Set` (JSON turns both into `{}`) or a `BigInt` with a `HookgresValidationError`. `WebhookEvent` now types each event's `data` as it arrives after JSON, using the new `Jsonified` type: a `Date` schema output is a `string` on the receiver. -- `send({ client })` returns a correct result when the client maps columns to camelCase (for example Knex with `knexSnakeCaseMappers`). -- Docs: the Express receiver recipe registers the webhook route before `express.json()`, which otherwise parses the body first so every signature check fails. The Fastify recipe scopes its raw-body parser to the webhook route. -- `cleanup({ olderThan: ms })` deleted nothing, and webhook envelopes carried a 1970 `timestamp`, when the database used a non-ISO `DateStyle`. Both now use epoch milliseconds internally. The production guide now covers clocks (NTP), `DateStyle`, pool size with several `notify` instances, and rolling upgrades. diff --git a/.changeset/production-hardening-3.md b/.changeset/production-hardening-3.md deleted file mode 100644 index 33b8eca..0000000 --- a/.changeset/production-hardening-3.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"hookgres": patch ---- - -Fix problems found by fuzzing and infrastructure failure testing: - -- Workers recording failed attempts for the same endpoint could deadlock with each other while `send()` calls for it were running, introduced by the lock change in this release cycle. The attempt's result was lost and the delivery resent after `lockTimeoutMs`. The failure is now recorded with a single `UPDATE` of the endpoint, and new `disabled_at` and `disabled_xid` columns (migration 4, catalog only) tell the worker that disabled an endpoint that it was the one. - -- One oversized message could stop every worker. The claim query returned the payload once per claimed delivery, so a large message fanned out to several endpoints could exhaust a worker's memory while it read the result. That aborted the claim, so the same deliveries went to every worker again, forever, and nothing else was delivered. Workers now claim without payloads, then load each message's payload once, as JSON text, and build its request body once for all its deliveries. In a test, peak memory for a 50 MB payload sent to 8 endpoints dropped from about 1.5 GB to about 900 MB. A worker that still runs out of memory has spent the attempt, so the delivery eventually fails instead of blocking the queue. The FAQ documents the memory to budget. -- The look-past scan that keeps other tenants served behind a full endpoint's backlog is now timed with a monotonic clock, so a wall-clock step back no longer pauses it, and it backs off after an error as well as after finding nothing. -- Webhook bodies now carry the payload as Postgres stores it (`jsonb` text): `{"a": 1, "b": [1, 2]}` with spaces, and large integers exactly as sent rather than rounded to float64. Receivers that parse JSON are unaffected; ones that compare or hash raw bodies across the upgrade will see different bytes. Signatures cover the exact bytes sent. -- A worker query that timed out cancelled its statement through the same pool. With every connection busy, for example after a network partition hit all of them at once, the cancels waited for each other and the worker hung for good. The cancel now runs on its own connection outside the pool, bounded at 5 seconds including the connect, and the race timer for a pool checkout is cleared when the checkout fails fast. -- If preparing a claimed batch failed (a database error putting extra rows back, or loading payloads), its deliveries stayed locked until `lockTimeoutMs` with an attempt spent and nothing sent. They are now put back. diff --git a/.changeset/production-hardening.md b/.changeset/production-hardening.md deleted file mode 100644 index f6ae602..0000000 --- a/.changeset/production-hardening.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"hookgres": patch ---- - -Fix several problems found by running Hookgres under production-like load and failures: - -- With `blockPrivateAddresses`, an endpoint that answered `101 Switching Protocols` hung the attempt forever: `requestTimeoutMs` never fired, the worker stopped delivering for every tenant and `stop()` never returned. Such replies now fail the attempt. -- Recording a failed attempt took a `FOR UPDATE` lock on the endpoint, which waits for every open transaction that called `send()` for it. Under steady load the worker stalled (in one test 1.2% of webhooks went out), and `drain()` overran its `timeoutMs`. It now takes `FOR NO KEY UPDATE`, which doesn't conflict with them. -- `send()` failed with a foreign key error, rolling back the caller's transaction, if one of the tenant's endpoints was deleted at the same moment. It now skips the deleted endpoint. -- `deleteEndpoint()` could deadlock with a worker recording failures for that endpoint. It now deletes the deliveries first and retries the rare remaining deadlock. -- Every worker query, including its wait for a pool connection, now gives up after `lockTimeoutMs`, so a dead database connection (a network partition) fails to `onError` instead of stalling the worker and `stop()`. The timed-out statement is also cancelled on the server, so it can't commit later. Pools created from `connectionString` turn on TCP keepalive, probing idle connections after 10s. -- The constructor rejects durations that aren't finite numbers in range, including NaN, and a `concurrency` that isn't a positive integer. These used to pass and leave the worker unable to claim anything, busy-looping, or unable to record failed attempts. Numeric strings (from `process.env`) still work. `listDeliveries`, `rotateEndpointSecret` and `cleanup` reject NaN too, and `verifyWebhook` throws a `TypeError` for a `toleranceSeconds` that is NaN or negative, since NaN turned the replay check off (`Infinity` still turns it off on purpose). -- `createEndpoint()` and `updateEndpoint()` reject `eventTypes` that isn't an array (a string was split into one-character patterns) and URLs with credentials. Credentials are removed from delivery errors for endpoints stored earlier. -- `send()` throws a `HookgresValidationError` for a payload that isn't JSON (it was stored as `null`), a string with half a surrogate pair (it was a raw Postgres error) and an `idempotencyKey` over 2000 bytes (longer keys could fail with a raw index error). -- Endpoint, delivery and attempt dates, and `onAttempt`'s `nextAttemptAt`, are `Date` objects even if your pool parses timestamps as strings. diff --git a/.changeset/tracing.md b/.changeset/tracing.md deleted file mode 100644 index e55bf18..0000000 --- a/.changeset/tracing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"hookgres": minor ---- - -Add OpenTelemetry traces. With the `opentelemetry` option, `send()` records a `send webhook` producer span under your request's span, and each HTTP attempt records a `deliver webhook` client span linked back to it, so you can get from a request to every attempt at delivering its webhooks. Endpoint URLs (which often carry secrets), baggage and `traceparent` headers to customers are left out on purpose. Webhook requests are sent with tracing suppressed, so HTTP instrumentations (undici, `node:http`) don't record their URLs or add `traceparent` and baggage headers to them, even with `traces: false` (as long as `api` includes `context`). Set `httpInstrumentation: true` to let them trace webhook requests anyway. Migration 5 adds a nullable `messages.trace_context` column; it's only read and written when traces are on. Set `traces: false` for metrics only. diff --git a/.changeset/typed-events.md b/.changeset/typed-events.md deleted file mode 100644 index b6eaa8f..0000000 --- a/.changeset/typed-events.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"hookgres": minor ---- - -Add typed events. Pass one [Standard Schema](https://standardschema.dev) per event type, such as a Zod (3.24+ or 4), Valibot or ArkType schema, as `new Hookgres({ pool, schemas })`, and `send()` is typed from them: a misspelled event type or a payload that doesn't match fails to compile. At runtime, `send()` validates each payload before writing and stores the schema's output, so transforms are applied and, with Zod or Valibot objects, undeclared keys are dropped. An invalid payload throws a `HookgresValidationError` whose message names the failing paths and whose `issues` holds the schema's issues; an event type with no schema, or a schema that throws, also throws a `HookgresValidationError`. Nothing is queued in any of these cases. With `schemas`, `createEndpoint()` and `updateEndpoint()` also reject `eventTypes` that match no declared event type. For compile-time checks only, use `new Hookgres({ pool })` with an event map. Hookgres still has no runtime dependencies besides `pg`. - -New types: `EventMap`, `AnyEvents`, `EventSchemas`, `EventTypeFilter`, `InferEvents`, `WebhookEvent` (a union of envelopes to narrow on `type` after `verifyWebhook`) and `StandardSchemaV1`. `SendInput`, `CreateEndpointInput` and `UpdateEndpointInput` take an optional event map; without one they're unchanged. New docs page: Typed events. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3deb7c4..b69089c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## 0.6.0 + +### Minor Changes + +- [#56](https://github.com/NickolasBenakis/hookgres/pull/56) [`5f714ce`](https://github.com/NickolasBenakis/hookgres/commit/5f714ce08f8d1dab0dec035c7dbe9471fd880ce3) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Add metrics: + - `stats({ tenantId?, endpointId? })` returns the queue right now: `pending`, `due`, `delivering`, `oldestDueAt` and `lagMs`, how long the oldest due delivery has waited. It reads the existing partial indexes: about 20ms with 450,000 pending and in-flight deliveries. + - The `opentelemetry` option takes your `@opentelemetry/api` module (1.7+) and exports `hookgres.delivery.attempts`, `hookgres.delivery.attempt.duration`, `hookgres.endpoints.disabled`, `hookgres.worker.errors`, and the `hookgres.queue.pending`, `.due`, `.delivering` and `.lag` gauges. Attributes are low-cardinality (outcome, status code, `error.type`), with no tenant or endpoint. Hookgres doesn't depend on OpenTelemetry; without the option nothing changes. See the new Metrics and traces guide. + +- [#54](https://github.com/NickolasBenakis/hookgres/pull/54) [`5c39e25`](https://github.com/NickolasBenakis/hookgres/commit/5c39e25896c59788fe1a124f95cbc0d19c65c924) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Fix problems found by testing Hookgres at scale and with real integrations: + - A slow or hanging endpoint no longer holds up every other tenant. Workers now refill slots as deliveries finish instead of waiting for a whole batch. A new `maxInFlightPerEndpoint` option (default: half of `concurrency`) limits how many slots one endpoint can take. In a test, one tenant's 100 queued deliveries to a hung endpoint delayed another tenant's webhook by 10s; now it's delivered in about 50ms. + - Claiming due deliveries no longer slows down with the size of the backlog. Each claim read and sorted every due row, which took 235–324ms per batch at 1M due. It now uses the indexes in order: about 10ms. + - A delivery whose keep-alive connection the receiver closed just as it was reused is retried once on a fresh connection, instead of counting as a failed attempt. + - `send()` rejects a payload containing a `Map`, a `Set` (JSON turns both into `{}`) or a `BigInt` with a `HookgresValidationError`. `WebhookEvent` now types each event's `data` as it arrives after JSON, using the new `Jsonified` type: a `Date` schema output is a `string` on the receiver. + - `send({ client })` returns a correct result when the client maps columns to camelCase (for example Knex with `knexSnakeCaseMappers`). + - Docs: the Express receiver recipe registers the webhook route before `express.json()`, which otherwise parses the body first so every signature check fails. The Fastify recipe scopes its raw-body parser to the webhook route. + - `cleanup({ olderThan: ms })` deleted nothing, and webhook envelopes carried a 1970 `timestamp`, when the database used a non-ISO `DateStyle`. Both now use epoch milliseconds internally. The production guide now covers clocks (NTP), `DateStyle`, pool size with several `notify` instances, and rolling upgrades. + +- [#57](https://github.com/NickolasBenakis/hookgres/pull/57) [`d1ef2a0`](https://github.com/NickolasBenakis/hookgres/commit/d1ef2a0a2408ce803b30f6b70dbbc7a32e2bf790) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Add OpenTelemetry traces. With the `opentelemetry` option, `send()` records a `send webhook` producer span under your request's span, and each HTTP attempt records a `deliver webhook` client span linked back to it, so you can get from a request to every attempt at delivering its webhooks. Endpoint URLs (which often carry secrets), baggage and `traceparent` headers to customers are left out on purpose. Webhook requests are sent with tracing suppressed, so HTTP instrumentations (undici, `node:http`) don't record their URLs or add `traceparent` and baggage headers to them, even with `traces: false` (as long as `api` includes `context`). Set `httpInstrumentation: true` to let them trace webhook requests anyway. Migration 5 adds a nullable `messages.trace_context` column; it's only read and written when traces are on. Set `traces: false` for metrics only. + +- [#47](https://github.com/NickolasBenakis/hookgres/pull/47) [`8cf3b22`](https://github.com/NickolasBenakis/hookgres/commit/8cf3b22d2914c8e51772831b22c3e07b6cca2884) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Add typed events. Pass one [Standard Schema](https://standardschema.dev) per event type, such as a Zod (3.24+ or 4), Valibot or ArkType schema, as `new Hookgres({ pool, schemas })`, and `send()` is typed from them: a misspelled event type or a payload that doesn't match fails to compile. At runtime, `send()` validates each payload before writing and stores the schema's output, so transforms are applied and, with Zod or Valibot objects, undeclared keys are dropped. An invalid payload throws a `HookgresValidationError` whose message names the failing paths and whose `issues` holds the schema's issues; an event type with no schema, or a schema that throws, also throws a `HookgresValidationError`. Nothing is queued in any of these cases. With `schemas`, `createEndpoint()` and `updateEndpoint()` also reject `eventTypes` that match no declared event type. For compile-time checks only, use `new Hookgres({ pool })` with an event map. Hookgres still has no runtime dependencies besides `pg`. + + New types: `EventMap`, `AnyEvents`, `EventSchemas`, `EventTypeFilter`, `InferEvents`, `WebhookEvent` (a union of envelopes to narrow on `type` after `verifyWebhook`) and `StandardSchemaV1`. `SendInput`, `CreateEndpointInput` and `UpdateEndpointInput` take an optional event map; without one they're unchanged. New docs page: Typed events. + +### Patch Changes + +- [#52](https://github.com/NickolasBenakis/hookgres/pull/52) [`8db1e86`](https://github.com/NickolasBenakis/hookgres/commit/8db1e869f17842a3c877cbd9723fc393452f26af) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Fix endpoint auto-disable: when an endpoint is disabled after failing for `disableEndpointAfterMs`, its queued deliveries are now marked `failed` right away, as documented, instead of staying `pending` until their next retry time (up to 10 hours later, or for good with no worker running) and being resent if the endpoint was re-enabled first. The attempt that disables the endpoint, and any attempt recorded after it was disabled automatically, now report `onAttempt` outcome `"failed"` instead of `"retrying"`. Manually disabled endpoints are unchanged. `attemptCount` no longer counts deliveries failed without an HTTP request (endpoint disabled, payload redacted, or out of attempts after a worker crash). + +- [#51](https://github.com/NickolasBenakis/hookgres/pull/51) [`97cdfd9`](https://github.com/NickolasBenakis/hookgres/commit/97cdfd9b200f02566776d3b53d183f95d3274f45) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - The package's `AGENTS.md` now lists `fromMikroORM` among the tested ORM wrappers in [Using your ORM's transaction](https://hookgres.dev/docs/guides/orms#mikroorm). The recipe wraps `fromKysely(em.getKysely())`, so `send()` joins an `em.transactional()` transaction in MikroORM 7. + +- [#58](https://github.com/NickolasBenakis/hookgres/pull/58) [`78c88b1`](https://github.com/NickolasBenakis/hookgres/commit/78c88b12268be0d7b469ab9674a0a78b3b28d43e) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - The npm description now says what Hookgres is in the words people search for: a transactional outbox for webhooks on Postgres, with webhook delivery signed by Standard Webhooks. + +- [#55](https://github.com/NickolasBenakis/hookgres/pull/55) [`df7244e`](https://github.com/NickolasBenakis/hookgres/commit/df7244ebb009bae1f037f7f79e065b5726692f60) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Fix problems found by fuzzing and infrastructure failure testing: + - Workers recording failed attempts for the same endpoint could deadlock with each other while `send()` calls for it were running, introduced by the lock change in this release cycle. The attempt's result was lost and the delivery resent after `lockTimeoutMs`. The failure is now recorded with a single `UPDATE` of the endpoint, and new `disabled_at` and `disabled_xid` columns (migration 4, catalog only) tell the worker that disabled an endpoint that it was the one. + + - One oversized message could stop every worker. The claim query returned the payload once per claimed delivery, so a large message fanned out to several endpoints could exhaust a worker's memory while it read the result. That aborted the claim, so the same deliveries went to every worker again, forever, and nothing else was delivered. Workers now claim without payloads, then load each message's payload once, as JSON text, and build its request body once for all its deliveries. In a test, peak memory for a 50 MB payload sent to 8 endpoints dropped from about 1.5 GB to about 900 MB. A worker that still runs out of memory has spent the attempt, so the delivery eventually fails instead of blocking the queue. The FAQ documents the memory to budget. + - The look-past scan that keeps other tenants served behind a full endpoint's backlog is now timed with a monotonic clock, so a wall-clock step back no longer pauses it, and it backs off after an error as well as after finding nothing. + - Webhook bodies now carry the payload as Postgres stores it (`jsonb` text): `{"a": 1, "b": [1, 2]}` with spaces, and large integers exactly as sent rather than rounded to float64. Receivers that parse JSON are unaffected; ones that compare or hash raw bodies across the upgrade will see different bytes. Signatures cover the exact bytes sent. + - A worker query that timed out cancelled its statement through the same pool. With every connection busy, for example after a network partition hit all of them at once, the cancels waited for each other and the worker hung for good. The cancel now runs on its own connection outside the pool, bounded at 5 seconds including the connect, and the race timer for a pool checkout is cleared when the checkout fails fast. + - If preparing a claimed batch failed (a database error putting extra rows back, or loading payloads), its deliveries stayed locked until `lockTimeoutMs` with an attempt spent and nothing sent. They are now put back. + +- [#53](https://github.com/NickolasBenakis/hookgres/pull/53) [`3931a9a`](https://github.com/NickolasBenakis/hookgres/commit/3931a9ae577ef897e9cc84ccb37e5073b6a95d16) Thanks [@NickolasBenakis](https://github.com/NickolasBenakis)! - Fix several problems found by running Hookgres under production-like load and failures: + - With `blockPrivateAddresses`, an endpoint that answered `101 Switching Protocols` hung the attempt forever: `requestTimeoutMs` never fired, the worker stopped delivering for every tenant and `stop()` never returned. Such replies now fail the attempt. + - Recording a failed attempt took a `FOR UPDATE` lock on the endpoint, which waits for every open transaction that called `send()` for it. Under steady load the worker stalled (in one test 1.2% of webhooks went out), and `drain()` overran its `timeoutMs`. It now takes `FOR NO KEY UPDATE`, which doesn't conflict with them. + - `send()` failed with a foreign key error, rolling back the caller's transaction, if one of the tenant's endpoints was deleted at the same moment. It now skips the deleted endpoint. + - `deleteEndpoint()` could deadlock with a worker recording failures for that endpoint. It now deletes the deliveries first and retries the rare remaining deadlock. + - Every worker query, including its wait for a pool connection, now gives up after `lockTimeoutMs`, so a dead database connection (a network partition) fails to `onError` instead of stalling the worker and `stop()`. The timed-out statement is also cancelled on the server, so it can't commit later. Pools created from `connectionString` turn on TCP keepalive, probing idle connections after 10s. + - The constructor rejects durations that aren't finite numbers in range, including NaN, and a `concurrency` that isn't a positive integer. These used to pass and leave the worker unable to claim anything, busy-looping, or unable to record failed attempts. Numeric strings (from `process.env`) still work. `listDeliveries`, `rotateEndpointSecret` and `cleanup` reject NaN too, and `verifyWebhook` throws a `TypeError` for a `toleranceSeconds` that is NaN or negative, since NaN turned the replay check off (`Infinity` still turns it off on purpose). + - `createEndpoint()` and `updateEndpoint()` reject `eventTypes` that isn't an array (a string was split into one-character patterns) and URLs with credentials. Credentials are removed from delivery errors for endpoints stored earlier. + - `send()` throws a `HookgresValidationError` for a payload that isn't JSON (it was stored as `null`), a string with half a surrogate pair (it was a raw Postgres error) and an `idempotencyKey` over 2000 bytes (longer keys could fail with a raw index error). + - Endpoint, delivery and attempt dates, and `onAttempt`'s `nextAttemptAt`, are `Date` objects even if your pool parses timestamps as strings. + ## 0.5.1 ### Patch Changes diff --git a/package.json b/package.json index 0637fdc..a20a3bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hookgres", - "version": "0.5.1", + "version": "0.6.0", "description": "Transactional outbox for webhooks on Postgres: webhook delivery that commits with your data. Signed with Standard Webhooks, retried, logged. TypeScript, no extra infrastructure.", "keywords": [ "webhooks",