Skip to content

Version Packages - #48

Merged
NickolasBenakis merged 1 commit into
mainfrom
changeset-release/main
Nov 12, 2026
Merged

NickolasBenakis merged 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

hookgres@0.6.0

Minor Changes

  • #56 5f714ce Thanks @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 5c39e25 Thanks @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 d1ef2a0 Thanks @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 8cf3b22 Thanks @NickolasBenakis! - Add typed events. Pass one Standard Schema 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<Events>({ 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 8db1e86 Thanks @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 97cdfd9 Thanks @NickolasBenakis! - The package's AGENTS.md now lists fromMikroORM among the tested ORM wrappers in Using your ORM's transaction. The recipe wraps fromKysely(em.getKysely()), so send() joins an em.transactional() transaction in MikroORM 7.

  • #58 78c88b1 Thanks @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 df7244e Thanks @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 3931a9a Thanks @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.

@vercel

vercel Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hookgres Ready Ready Preview Nov 12, 2026 7:55pm UTC

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 6 times, most recently from 43720ca to baf4caf Compare November 12, 2026 19:39
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from baf4caf to 54cac38 Compare November 12, 2026 19:55
@NickolasBenakis
NickolasBenakis merged commit 1666fef into main Nov 12, 2026
14 checks passed
@NickolasBenakis
NickolasBenakis deleted the changeset-release/main branch November 12, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant