Skip to main content

Choosing your database

Reach for a topology in this order — lead with what your app already has, not with an engine demand:

TopologyWhenWhat you run
T1 — Send-onlyYou only send webhooksNo database. Platform off; the Server engine needs only a queue.
T2 — Same databaseYou persist, on the engine your app already usesPostgreSQL or MySQL 8.4+ — the package migrates its tables into your app's own connection.
T3 — Side-carYou persist, but want the webhook tables elsewherePoint webhooks.database.connection at a dedicated connection — e.g. a MySQL app with a PostgreSQL side-car. See A dedicated database connection.

The one-line recommendation: don't switch engines for this package — use the one your app already runs on. PostgreSQL is the reference engine and keeps a few storage accelerations MySQL can't express, but every guarantee the package makes holds identically on both — exact percentile numbers, race-free dedupe, the body_sha256 byte-fidelity promise, the DB-enforced GDPR-erasure cascade, DST-safe timestamps, and case-sensitive identity. MySQL users give up storage optimizations, never correctness.

What PostgreSQL buys you

Every row below is a PostgreSQL-only storage optimization. None of them changes a result — they change cost at scale. Read the recommendation as "this is when, and only when, the difference is worth an engine."

Difference (PostgreSQL only)When it hits youTipRecommendation
O(1) retention — PG drops an old month as a partition; MySQL runs an indexed, chunked DELETE.Invisible below ~1M deliveries/month; real IO pressure at tens of millions.Lower platform.retention_months, enable payload offload, run webhooks:partition-maintenance off-peak.Above ~1M deliveries/month and long retention → PostgreSQL. Otherwise MySQL is fine.
Index bounded by the open backlog — PG partial indexes; MySQL indexes all history.Same volume threshold.Nothing to configure — the composite indexes are used natively on both.Not worth an engine on its own; folds into the retention call above.
Indexed containment search into an inbound payload — PG jsonb GIN.Only if you search inside stored payloads.No shipped query uses it — nothing breaks. Search deliveries/calls with Scout instead — opt-in, and it takes three steps: see Searching the logs.Not an engine-choice factor.
The tdigest percentile tier — an optional PG extension.Only at very high dashboard volume, and only if you set dashboard.percentiles.driver = 'tdigest'.The default live driver is the same speed and returns identical numbers on both engines.Not a reason to pick PostgreSQL — and tdigest isn't on Neon (Laravel Cloud's Postgres) either, so this tier is unavailable there regardless.
max_allowed_packet — MySQL defaults to 64 MB vs PG's ~1 GB.Only with multi-MB single events.Enable offload (server.large_payload.enabled = true, and the inbound offload) so a big body never reaches the column; reclaim the offloaded objects with a disk lifecycle policy, or php artisan webhooks:prune-orphaned-payloads (retention prunes rows only — see config/webhooks.php).Enable offload on MySQL if you emit multi-MB events.

One hard warning — read this if you run MySQL

The package declares utf8mb4_0900_as_cs (case- and accent-sensitive) on every identity column, on purpose. Do not ALTER it back to a _ci collation. Under a case-insensitive collation evt_AbC and evt_abc collapse into one dedupe row, and a distinct, signature-verified webhook is answered 200 and silently discarded.

php artisan webhooks:preflight checks this and fails when it finds it. It asks MySQL which columns the package's tables carry a UNIQUE index on — the columns where a collation stops being cosmetic and starts deciding whether two rows are the same row — and refuses any that no longer distinguish case and accents, naming the table, the column, the index and the ALTER that puts it back. The set comes from the live schema rather than a list in the package, so a unique index added later is covered the day it exists.

A manual ALTER can still undo the schema between two preflights; what it can no longer do is survive one.

MariaDB is not supported

MariaDB is not supported, at any tier — it is rejected loudly at migrate time and by webhooks:preflight. Its JSON is a LONGTEXT alias, and it has neither the multi-valued index the fan-out lookup needs nor functional indexes. Use MySQL 8.4+ or PostgreSQL.

Deploying to Laravel Cloud

Laravel Cloud offers both a first-party MySQL 8.4 database and Neon-powered Serverless Postgres — either works. Two cautions:

  1. Run migrations against your database's direct, non-pooled endpoint. migrate and webhooks:partition-maintenance issue transactional DDL, which a transaction pooler (PgBouncer, Neon's pooled endpoint) breaks.
  2. Neon does not ship the tdigest extension, so dashboard.percentiles.driver = 'tdigest' is unavailable there — the default live driver needs no extension and returns the same numbers.

A dedicated database connection

By default the package migrates its tables into your app's default connection. To keep them on a different one — the headline case being a MySQL app with a PostgreSQL side-car for the webhook tables (topology T3) — set webhooks.database.connection to a connection defined in config/database.php:

WEBHOOKS_DB_CONNECTION=webhooks_pgsql

Every model, migration and analytics query then resolves that one connection, so the package never silently splits across two databases; leave it unset and everything stays on the app default. php artisan webhooks:preflight prints the connection it resolved. When you test a host app on this topology, transact both connections so each case rolls back:

protected $connectionsToTransact = ['mysql', 'webhooks_pgsql'];