Architecture
LaraWebhook is organized around four layers:
Domain: business rules, entities, value objects, and domain events.Application: use cases, ports, commands, results, and read models.Infrastructure: Laravel adapters, Eloquent persistence, controllers, middleware, jobs, and service bindings.Shared: cross-cutting primitives such as the facade, the event bus port, and package-level wiring.
Recommended Flows
Middleware flow
Use validate-webhook:{service} for the default inbound flow.
ValidateWebhookchecks the signature withSignatureValidator.ReceiveWebhookresolves the event type, idempotency key, and external id.ReceiveWebhookchecks duplicates throughWebhookDuplicateDetector.ReceiveWebhookvalidates the payload throughValidateWebhook.ReceiveWebhookrecords the audit log throughRecordWebhookLog.- The Laravel adapter dispatches collected domain events through the application event bus.
This is the recommended path for inbound webhooks.
Manual facade flow
Use the facade for controlled manual orchestration:
use Proxynth\Larawebhook\Shared\Infrastructure\Laravel\Facades\Larawebhook;
use Proxynth\Larawebhook\Ingestion\Domain\ValueObjects\Signature;
$signature = Signature::fromString($request->header('Stripe-Signature'));
$log = Larawebhook::validateAndLog($request->getContent(), $signature, 'stripe', 'payment_intent.succeeded');Signature::fromString() wraps the raw header in a typed value object. For providers that include timestamp metadata, the timestamp stays attached to the signature object.
Application Roles
ValidateWebhook
ValidateWebhook is the application boundary for signature verification. It accepts a parsed payload, a service, a signature, the event name, the external id, and the secret. It returns a simple validation result and does not write persistence itself.
RecordWebhookLog
RecordWebhookLog is the application boundary for audit logging. It delegates persistence to the audit writer port and returns the stored WebhookLog.
ReceiveWebhook
ReceiveWebhook orchestrates the inbound webhook flow. It resolves the event type, computes the idempotency key, rejects duplicates, validates the signature, records the audit log, and returns domain events alongside the result.
Identity and Deduplication
external_idstores the provider delivery or event identifier when the provider exposes one.idempotency_keystores the deduplication key resolved by the application.- Providers without a stable external id use a payload-hash fallback for idempotency.
Deduplication is backed by the processed_webhook_events projection.
webhook_logs is an audit trail and read model source. It may contain several rows for the same webhook across receive, retry or replay attempts.
processed_webhook_events owns the uniqueness constraint on service + idempotency_key.
webhook_logs does not own any deduplication constraint. It is an audit trail and may contain multiple rows for the same webhook across receive, retry, replay or manual audit writes.
Concurrency
processed_webhook_events owns a database unique constraint on service + idempotency_key.
The application first checks duplicates through WebhookDuplicateDetector, then records successful processing through ProcessedWebhookRecorder.
If two identical webhooks are received concurrently, both requests may pass the initial duplicate check. The database unique constraint remains the final guard.
Duplicate insert collisions are converted into an already_processed result instead of surfacing as an infrastructure error.
Historical migration
idempotency_key was introduced after external_id. Older installations may contain webhook logs where external_id was used as the deduplication key.
During migration, LaraWebhook backfills idempotency_key from external_id for one row per service + external_id pair. Historical duplicate audit rows keep a null idempotency_key to avoid migration failures when the unique index on service + idempotency_key is created.
After migration:
external_idremains the provider identifier;idempotency_keyis the deduplication key;- duplicate detection uses
idempotency_key; - historical duplicate logs remain available as audit data.
Read Side
Dashboard and API query handlers use read models and read repositories:
WebhookLogSummaryWebhookLogDetailsWebhookFailureDetails
These types stay in Application so the read side remains framework-agnostic.
Retry Behavior
LaraWebhook supports both retry modes:
- Sync retry: the request flow retries inline before returning.
- Async retry: the middleware returns
202 Acceptedand dispatchesRetryWebhookJob.
The retry flow keeps the application decision in the use case and leaves queue dispatch to the Laravel adapter.
Payload Storage
Payload storage is configurable:
none: store no payloadredacted: store a sanitized payloadfull: store the raw payload for debugging and replay
Choose the least permissive mode that still supports your operational needs.
Slack Signatures
Slack signatures use a timestamped HMAC scheme. The incoming request includes:
X-Slack-SignatureX-Slack-Request-Timestamp
The validator uses both values so timestamp tolerance can be enforced and replay protection stays in place.
Facade Guidance
Recommended facade methods:
validate()validateAndLog()validateWithRetries()- read helpers such as
logs(),logsForService(),failedLogs(),successfulLogs()
Legacy helpers:
logSuccess()- deprecatedlogFailure()- deprecated
These legacy helpers remain available for compatibility, but they go through the application logging flow and should not be the default choice for new code.