sinemacula/laravel-aws-sns-listener
A Laravel package for receiving and handling AWS SNS notifications, with support for routing messages to listeners, managing subscriptions, and verifying signatures so you can integrate push notifications and event-driven workflows into your app.
Install the package with Composer; the service provider is auto-discovered, so there is nothing to register manually.
composer require sinemacula/laravel-aws-sns-listener
php artisan vendor:publish --provider="SineMacula\Aws\Sns\ServiceProvider"
Publish the config first and read it before writing any code — it is where the route path, middleware stack, and expected topic ARNs live. Check the package README for the exact --tag value, since tags are not listed in the release notes.
// config/aws-sns-listener.php (shape, confirm keys against the published file)
return [
'route' => [
'uri' => 'webhooks/aws/sns',
'middleware' => ['api'], // must not include CSRF verification
],
'topics' => [
// ARNs you are willing to accept messages from
],
];
Then:
POST /webhooks/aws/sns publicly reachable over HTTPS (a tunnel such as ngrok or Expose is fine in development).PendingConfirmation, your endpoint is not returning the confirmation payload correctly (see Gotchas).namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use SineMacula\Aws\Sns\Events\NotificationReceived; // confirm exact FQCN in the repo
class LogSnsNotification implements ShouldQueue
{
public function handle(NotificationReceived $event): void
{
logger()->info('sns.received', [
'message_id' => $event->message->getMessageId(),
'topic' => $event->message->getTopicArn(),
]);
}
}
Where to look first: src/ mirrors the whole surface — the service provider (route/config wiring), the middleware (signature validation), the message factory (payload → typed entity), and the Events/ directory (what you actually listen to). The typed entities for SES, S3, and CloudWatch tell you exactly which accessors you get in a listener.
Listen to typed events, not raw JSON. The package maps SNS payloads into typed message entities and dispatches a distinct Laravel event per provider. Type-hint the event in handle() and use the entity's accessors rather than json_decode-ing the request yourself.
public function handle(SesNotificationReceived $event): void
{
$notification = $event->message->getNotification();
if ($notification->isBounce()) {
Suppression::firstOrCreate(['email' => $notification->getBouncedRecipient()]);
}
}
Keep listeners thin and queued. Implement ShouldQueue on every listener so the webhook request returns 2xx immediately and processing happens on your queue worker. This decouples SNS's retry behaviour from your processing time and gives you Laravel's retry/backoff/DLQ semantics for free.
Use one generic listener as a catch-all. Typed guards only fire for recognised notification types. Register a listener on the generic notification event to log or park anything unrecognised — otherwise a new SNS payload shape silently disappears.
public function handle(NotificationReceived $event): void
{
if (! $this->isHandled($event)) {
Log::warning('sns.unhandled_type', ['type' => $event->message->getType()]);
}
}
Fan out via Laravel's event system, not the controller. Because each notification becomes a normal Laravel event, you get multiple independent listeners, Event::fake() in tests, and event discovery. Do not add business logic in the controller or middleware.
Make handlers idempotent. SNS is at-least-once. The package's v2.0.1 "deduplicate typed notification guards" change is about internal event dispatch, not message-level deduplication — assume you must dedupe yourself. A cheap pattern:
$key = 'sns:'.$event->message->getMessageId();
if (! Cache::add($key, true, now()->addDays(7))) {
return; // already processed
}
Validate the topic allowlist in config, not code. Populate the expected-topic list in the published config so the package can reject messages from topics you did not subscribe. Multi-tenant or multi-environment setups should drive this from env vars rather than hardcoding ARNs.
Test with Event::fake() for your handlers and real HTTP posts for the integration. Unit-test listeners by faking the event and asserting side effects. Separately, keep one feature test that POSTs a captured SNS envelope to the route with signature checking disabled in the test environment, so you catch middleware/route regressions.
Local iteration loop. Capture a real payload from CloudWatch Logs once, save it as a fixture, and replay it via Http::post() in a test or a small artisan command. This is far faster than re-triggering SNS and avoids needing a public tunnel for every change.
web middleware, VerifyCsrfToken will 419 every SNS request. Use the api group or add the endpoint to $except. The package ships middleware, but it does not remove CSRF for you.SubscriptionConfirmation POST quickly (SNS times out). If confirmation hangs, check that the route is reachable, that CSRF is off, and that no auth middleware blocks unauthenticated AWS traffic..env, run php artisan config:cache (and config:clear locally) or your changes will appear to do nothing. This is the most common "the package is broken" report.failed() handling — anything your listener drops goes to failed_jobs, not back to SNS.MessageId, topic ARN, and SNS type on every message. These are the only fields that let you correlate a support ticket with an SNS delivery and dedupe store entry. Without them, debugging is guesswork.composer audit too; the release bumped dependencies for security reasons without naming the CVEs.composer.json rather than assuming. Pin the version and let Dependabot-style patch upgrades flow through your normal audit cycle.AGENTS.md explicitly frames this as an integration layer, not an AWS SDK wrapper — do not expect it to provision topics, manage subscriptions, or implement business logic for you.How can I help you explore Laravel packages today?