Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Awspushbundle Laravel Package

mcfedr/awspushbundle

Symfony bundle for sending mobile push notifications through Amazon SNS. Configure AWS credentials and platform applications, then target iOS, Android, and other devices with a simple service for device tokens, topics, and payloads. Ideal for integrating AWS-backed push into Symfony apps.

View on GitHub
Deep Wiki
Context7

Getting Started

mcfedr/awspushbundle is a Symfony bundle, not a Laravel package — there is no service provider, no config/*.php, and no facade. It wraps aws/aws-sdk-php's SNS client with two things that are genuinely worth having:

  • Mcfedr\AwsPushBundle\Service\Topics — register/unregister device endpoints, create topics, send() a message to a topic, broadcast().
  • Mcfedr\AwsPushBundle\Message\Message — a fluent value object (message body, title, badge, sound, custom data, TTL) that builds the platform-specific SNS payload for you.

Everything else (the DI extension, console commands, YAML config) is Symfony-only glue that Laravel cannot consume. Treat it as a library, not a bundle.

Step 1 — pull the platform application ARNs out of AWS first

This is the step people skip and then spend a day debugging. In the SNS console:

  1. Create a platform application per platform (APNS / APNS_SANDBOX for iOS, FCM/GCM for Android).
  2. Upload the APNs .p8 key or FCM server key to that platform application.
  3. Copy the platform application ARN (arn:aws:sns:eu-west-1:123456789:app/APNS/MyApp) — not a topic ARN.

The bundle's config is essentially just a platform => ARN map plus AWS credentials. In Laravel, mirror it in config/services.php:

// config/services.php
'aws_push' => [
    'region'    => env('AWS_DEFAULT_REGION', 'eu-west-1'),
    'platforms' => [
        'ios'     => env('AWS_SNS_APNS_ARN'),
        'android' => env('AWS_SNS_FCM_ARN'),
    ],
    'debug'     => env('AWS_PUSH_DEBUG', false),
],

Step 2 — wire the two classes by hand

composer require mcfedr/awspushbundle:^6.21 aws/aws-sdk-php

Then, before writing the provider, open vendor/mcfedr/awspushbundle/Service/Topics.php and read the constructor. Its signature has changed across majors (SNS client, platform map, logger, debug flag, optional device provider) and you are bypassing the bundle's DI, so the constructor is your contract.

// app/Providers/AwsPushServiceProvider.php
use Aws\Sns\SnsClient;
use Mcfedr\AwsPushBundle\Service\Topics;

public function register(): void
{
    $this->app->singleton(SnsClient::class, fn () => new SnsClient([
        'version' => 'latest',
        'region'  => config('services.aws_push.region'),
        // No key/secret: let the SDK resolve the default provider chain
        // (env vars, ~/.aws, ECS task role, EC2 instance profile).
    ]));

    $this->app->singleton(Topics::class, fn ($app) => new Topics(
        $app->make(SnsClient::class),
        config('services.aws_push.platforms'),
        $app->make('log'),
        config('services.aws_push.debug'),
    ));
}

First use case — register on login, notify on event

// app/Http/Controllers/DeviceController.php
public function store(RegisterDeviceRequest $request, Topics $topics)
{
    $topics->registerDevice(
        $request->string('token'),
        $request->string('platform')   // 'ios' | 'android' — must match config key
    );

    return response()->noContent();
}
// anywhere
use Mcfedr\AwsPushBundle\Message\Message;

$message = Message::create('Your order shipped')
    ->setTitle('Order #1042')
    ->setBadge(1)
    ->setSound('default')
    ->setCustom(['order_id' => 1042, 'route' => '/orders/1042']);

$topics->send($message, 'order-updates');

If registering a device works and send() returns an SNS message ID, you have the whole happy path and can stop reading the bundle's source.


Implementation Patterns

Pattern 1 — Device lifecycle as a first-class concern

SNS endpoints are stateful and rot. The bundle registers/unregisters, but ownership of the token is yours. Keep a device_endpoints table:

Schema::create('device_endpoints', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $t->string('token', 512);
    $t->string('platform', 16);
    $t->string('endpoint_arn')->nullable()->unique();
    $t->timestamp('last_seen_at')->nullable();
    $t->timestamps();
    $t->unique(['token', 'platform']);
});

Wire it to three events and nothing else:

Event Action
Login / app open registerDevice() + upsert row (token may have rotated)
Logout unregisterDevice() and delete the row
Nightly job Delete rows with last_seen_at older than 90 days, then unregisterDevice()

Tokens rotate silently on both APNs and FCM. Register on every app launch, not just first install, or you will accumulate dead endpoints and wonder why delivery rates drop.

Pattern 2 — Topics for audience, endpoints for people

SNS topics are free and instant, but they fan out to everyone subscribed. Use them as your segment primitive:

  • all-users, beta-testers, ios-2.4, digest-subscribers → topics.
  • Per-user notifications (order shipped, DM received) → publish directly to that user's endpoint ARN. The Topics service doesn't wrap this well; use SnsClient::publish(['TargetArn' => $arn, 'Message' => ...]) with the same Message object's payload.

A per-user SNS topic is an anti-pattern at scale — you pay topic sprawl and SNS topic quotas, and you lose the ability to clean up endpoints in bulk.

Pattern 3 — Never send from a request cycle

publish is a network round trip per topic. Wrap it:

// app/Jobs/SendPushNotification.php
class SendPushNotification implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(
        public readonly array $payload,     // serializable Message state
        public readonly string $topic,
    ) {}

    public function handle(Topics $topics): void
    {
        $message = Message::create($this->payload['body'])
            ->setTitle($this->payload['title'] ?? null)
            ->setCustom($this->payload['custom'] ?? []);

        if (isset($this->payload['badge'])) {
            $message->setBadge($this->payload['badge']);
        }

        $topics->send($message, $this->topic);
    }
}

Note the deliberate serialization into a plain array. Message is not guaranteed to be cleanly serializable for SerializesModels, and payloads captured at dispatch time are what you actually want to debug later.

Pattern 4 — Localisation via topics, not per-device payloads

If you need three languages, do not loop devices and vary the payload. Create news-en, news-fr, news-de, register the device into the topic matching its locale at login time, and dispatch three queued jobs. Cheaper, and the message body stays trivially cacheable.

Pattern 5 — A thin Push facade over the library

Once the wiring is done, hide it. A single App\Support\Push class with toTopic(), toUser(), toEndpointArn() means you can swap SNS for FCM v1 / APNs directly later without touching call sites — which matters given this package's bus factor (40 stars, zero dependents).

Pattern 6 — Testing without hitting SNS

Two layers, tested separately:

// Unit-test payload shape without any HTTP
$message = Message::create('hi')->setCustom(['a' => 1]);
$this->assertSame('hi', $message->getMessage());

// Integration: swap the SnsClient for one with a MockHandler
$mock = new MockHandler([
    new Result(['MessageId' => 'abc-123']),
]);
$client = new SnsClient([
    'version' => 'latest',
    'region'  => 'eu-west-1',
    'handler' => HandlerStack::create($mock),
]);
$this->app->instance(SnsClient::class, $client);

Assert on the request body your code sent — that is where MessageStructure mistakes surface.


Gotchas and Tips

It is not a Laravel package. composer require will drag in symfony/config, symfony/dependency-injection, symfony/http-kernel, symfony/console. If you already use something that pins Symfony components (e.g. a mailer or a queue bridge), run composer why symfony/dependency-injection immediately and check for conflicts. If you only want the ergonomics, the pragmatic move is to copy Message.php and the ~50 relevant lines of Topics.php into app/Support/ and drop the dependency entirely. It's MIT-licensed and the surface is small.

Pin the major, and don't guess it. The current line is 6.x (6.21.0 as of this release) — a ^7.0 constraint will simply not resolve. Use composer require mcfedr/awspushbundle:^6.21 with an exact-minor floor so the constructor signature you reverse-engineered in Step 2 can't drift under you, and keep the Push facade so a swap is a one-file change. 40 stars, zero dependents, and a low release cadence is a thin bus factor for anything in your critical notification path.

Platform application ARN ≠ topic ARN. The single most common failure. arn:...:app/APNS/... for registration, arn:...:MyTopic for send(). A mismatch produces InvalidParameter or, worse, a successful registration that never delivers.

iOS and Android need different payload envelopes. SNS only splits them if the publish request sets MessageStructure: 'json' with APNS/GCM keys. If you bypass Message and hand-roll publish(), you'll send one JSON blob to both platforms — APNs will silently drop the Android-only data keys, and FCM will reject the aps block. Use the Message object, or replicate its MessageStructure handling exactly.

Badges are absolute, not incremental. APNs sets the badge to whatever number you send. The bundle does not track counts, so you must compute the value (a cached unread count) or send 0 to clear it. setBadge(1) on every push means every user sits at "1" forever — a classic review comment.

Silent/background pushes are not just content-available. Modern APNs requires the apns-push-type: background header and apns-priority: 5, delivered as custom SNS message attributes or inside the APNS payload depending on your setup. Verify in a real TestFlight build; the simulator won't tell you.

Disabled endpoints are your problem. SNS marks an endpoint Enabled=false after repeated APNs/FCM errors and then does nothing more. Poll getEndpointAttributes on a schedule, re-enable on transient failures, delete after N consecutive failures. Nothing in this bundle does that for you — plan for a weekly cleanup job from day one.

TTL is silent. TTL in seconds on the SNS publish. Expired messages vanish with a successful publish response. If "notifications sometimes don't arrive," check TTL before you check anything else.

Credentials: don't put keys in the platform map. The bundle's YAML config accepts key/secret, which invites .env leakage. Pass no credentials to SnsClient and let the AWS SDK default provider chain resolve them — env vars locally, instance/task role in production. Then no push credential ever lands in your config cache.

Region must match the platform application. An APNS platform app in us-east-1 and an SNS client configured for eu-west-1 fail at registration time with a confusing ARN-not-found. Set region per environment and assert it on boot.

The debug flag logs full payloads. Great in local dev, a PII problem in production — device tokens and message bodies land in storage/logs. Default it off and gate it on app()->isLocal().

SNS success ≠ delivery. publish returning a MessageId only means SNS accepted it. Close the loop with CloudWatch NumberOfNotificationsDelivered / NumberOfNotificationsFailed per platform application, and log the MessageId alongside your own correlation ID so you can reconcile a user's "I never got it" report.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect