Repository navigation
Version Packages - #48
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
November 12, 2026 19:39
43720ca to
baf4caf
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
November 12, 2026 19:55
baf4caf to
54cac38
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
5f714ceThanks @NickolasBenakis! - Add metrics:stats({ tenantId?, endpointId? })returns the queue right now:pending,due,delivering,oldestDueAtandlagMs, how long the oldest due delivery has waited. It reads the existing partial indexes: about 20ms with 450,000 pending and in-flight deliveries.opentelemetryoption takes your@opentelemetry/apimodule (1.7+) and exportshookgres.delivery.attempts,hookgres.delivery.attempt.duration,hookgres.endpoints.disabled,hookgres.worker.errors, and thehookgres.queue.pending,.due,.deliveringand.laggauges. 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
5c39e25Thanks @NickolasBenakis! - Fix problems found by testing Hookgres at scale and with real integrations:maxInFlightPerEndpointoption (default: half ofconcurrency) 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.send()rejects a payload containing aMap, aSet(JSON turns both into{}) or aBigIntwith aHookgresValidationError.WebhookEventnow types each event'sdataas it arrives after JSON, using the newJsonifiedtype: aDateschema output is astringon the receiver.send({ client })returns a correct result when the client maps columns to camelCase (for example Knex withknexSnakeCaseMappers).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 1970timestamp, when the database used a non-ISODateStyle. Both now use epoch milliseconds internally. The production guide now covers clocks (NTP),DateStyle, pool size with severalnotifyinstances, and rolling upgrades.#57
d1ef2a0Thanks @NickolasBenakis! - Add OpenTelemetry traces. With theopentelemetryoption,send()records asend webhookproducer span under your request's span, and each HTTP attempt records adeliver webhookclient 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 andtraceparentheaders 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 addtraceparentand baggage headers to them, even withtraces: false(as long asapiincludescontext). SethttpInstrumentation: trueto let them trace webhook requests anyway. Migration 5 adds a nullablemessages.trace_contextcolumn; it's only read and written when traces are on. Settraces: falsefor metrics only.#47
8cf3b22Thanks @NickolasBenakis! - Add typed events. Pass one Standard Schema per event type, such as a Zod (3.24+ or 4), Valibot or ArkType schema, asnew Hookgres({ pool, schemas }), andsend()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 aHookgresValidationErrorwhose message names the failing paths and whoseissuesholds the schema's issues; an event type with no schema, or a schema that throws, also throws aHookgresValidationError. Nothing is queued in any of these cases. Withschemas,createEndpoint()andupdateEndpoint()also rejecteventTypesthat match no declared event type. For compile-time checks only, usenew Hookgres<Events>({ pool })with an event map. Hookgres still has no runtime dependencies besidespg.New types:
EventMap,AnyEvents,EventSchemas,EventTypeFilter,InferEvents,WebhookEvent(a union of envelopes to narrow ontypeafterverifyWebhook) andStandardSchemaV1.SendInput,CreateEndpointInputandUpdateEndpointInputtake an optional event map; without one they're unchanged. New docs page: Typed events.Patch Changes
#52
8db1e86Thanks @NickolasBenakis! - Fix endpoint auto-disable: when an endpoint is disabled after failing fordisableEndpointAfterMs, its queued deliveries are now markedfailedright away, as documented, instead of stayingpendinguntil 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 reportonAttemptoutcome"failed"instead of"retrying". Manually disabled endpoints are unchanged.attemptCountno longer counts deliveries failed without an HTTP request (endpoint disabled, payload redacted, or out of attempts after a worker crash).#51
97cdfd9Thanks @NickolasBenakis! - The package'sAGENTS.mdnow listsfromMikroORMamong the tested ORM wrappers in Using your ORM's transaction. The recipe wrapsfromKysely(em.getKysely()), sosend()joins anem.transactional()transaction in MikroORM 7.#58
78c88b1Thanks @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
df7244eThanks @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 afterlockTimeoutMs. The failure is now recorded with a singleUPDATEof the endpoint, and newdisabled_atanddisabled_xidcolumns (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 (
jsonbtext):{"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
lockTimeoutMswith an attempt spent and nothing sent. They are now put back.#53
3931a9aThanks @NickolasBenakis! - Fix several problems found by running Hookgres under production-like load and failures:blockPrivateAddresses, an endpoint that answered101 Switching Protocolshung the attempt forever:requestTimeoutMsnever fired, the worker stopped delivering for every tenant andstop()never returned. Such replies now fail the attempt.FOR UPDATElock on the endpoint, which waits for every open transaction that calledsend()for it. Under steady load the worker stalled (in one test 1.2% of webhooks went out), anddrain()overran itstimeoutMs. It now takesFOR 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.lockTimeoutMs, so a dead database connection (a network partition) fails toonErrorinstead of stalling the worker andstop(). The timed-out statement is also cancelled on the server, so it can't commit later. Pools created fromconnectionStringturn on TCP keepalive, probing idle connections after 10s.concurrencythat 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 (fromprocess.env) still work.listDeliveries,rotateEndpointSecretandcleanupreject NaN too, andverifyWebhookthrows aTypeErrorfor atoleranceSecondsthat is NaN or negative, since NaN turned the replay check off (Infinitystill turns it off on purpose).createEndpoint()andupdateEndpoint()rejecteventTypesthat 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 aHookgresValidationErrorfor a payload that isn't JSON (it was stored asnull), a string with half a surrogate pair (it was a raw Postgres error) and anidempotencyKeyover 2000 bytes (longer keys could fail with a raw index error).onAttempt'snextAttemptAt, areDateobjects even if your pool parses timestamps as strings.