Skip to main content

Querying the tables yourself

If you write your own health check or report against the package's tables — WebhookDelivery, WebhookCall, WebhookServerDeliverybind timestamps through the shipped scopes, never a plain ->where('created_at', …).

Every timestamp column is timestamptz (PostgreSQL) or a UTC-naive DATETIME(6) (MySQL); a naive literal is resolved against the database session time zone, which is unrelated to app.timezone and routinely not UTC — so a hand-bound comparison is off by that offset and quietly returns the wrong rows, with no error to notice. A health check written that way can never fire.

The scopes bind the instant, per dialect, so you can't get it wrong:

use Pushery\Webhooks\Models\WebhookDelivery;

// "Are deliveries piling up undelivered because no worker is running?"
$stuck = WebhookDelivery::query()
->pendingSince(now()->subMinutes(15))
->count();

WebhookDelivery::query()->createdBefore(now()->subDay()); // strictly before
WebhookDelivery::query()->createdAfter($since); // at or after
WebhookDelivery::query()->createdBetween($from, $to); // half-open [from, to)
WebhookDelivery::query()->whereTimestamp('delivered_at', '>=', $since); // any column

For a raw statement the scopes don't cover, (new WebhookDelivery)->boundTimestamp($moment) returns the same offset-correct literal to bind by hand.

A raw format constant is deliberately not exposed: the correct literal differs by engine, so a copied constant would be right on one and wrong on the other — only a dialect-aware binding is safe.

Writing rows yourself

Reading is not the only direction that can go quietly wrong. If you insert into these tables directly — a demo seeder, a fixture, a backfill of your own — write through the models rather than a raw INSERT. Three columns of webhook_deliveries refuse a hand-written row, and one of them refuses it on only one of the two engines:

What the insert didPostgreSQL 18MySQL 8.4
put a non-UUID in event_id (a ULID, say)22P02 — rejectedaccepted, stored verbatim
supplied a value for payload_type428C9 — rejected3105 — rejected
omitted id23502 — rejected1364 — rejected

The last two rows are loud on both engines, so they cost you a red run and nothing else. payload_type is a STORED generated column mirroring the payload's own type field — it is computed, never written. id has no database-side default anywhere: the model supplies a UUIDv7 through Laravel's HasUuids, and a raw insert has to supply one too.

The first row is the one worth remembering, because it is silent on exactly one engine. event_id is a real uuid on PostgreSQL and a char(36) on MySQL, so a 26-character ULID overflows nothing and is stored as-is. That value is not private bookkeeping: it is sent as the Standard Webhooks webhook-id header, and a redelivery re-signs with the same id precisely so the receiver can recognize it. A row written with a non-UUID id therefore carries that id out to the endpoint — where the two engines now disagree about what your system even emits.

So the rule is the same one the read side arrives at from the other direction: let the package mint the value. Dispatching through Webhooks::dispatch() generates the event id as a UUIDv7 itself, and the model fills id and leaves payload_type alone. A row you write by hand has to be exactly as strict about event_id — on the engine that would have told you, and on the one that would not.