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

Expo Notifications Bundle Laravel Package

solvecrew/expo-notifications-bundle

A Laravel bundle for Expo push notifications: manage device tokens, send notifications to iOS and Android through Expo, and add configurable push messaging to your app with a simple API.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: despite the Laravel-oriented framing, solvecrew/expo-notifications-bundle is a Symfony bundle, not a Laravel package. There is no ExpoNotificationsServiceProvider, no config/expo-notifications.php, and php artisan package:discover will do nothing with it. Your job as a Laravel dev is to decide whether to (a) pull in the bundle and bridge its services into Laravel's container, or (b) lift the handful of value objects/HTTP calls you actually need and implement against Expo's API directly.

Minimal steps to evaluate:

composer require solvecrew/expo-notifications-bundle

Then read, in this order:

  1. src/ — the core classes (notification/message value objects, the sender/client service, any exception classes). These are the parts that matter and are often framework-agnostic.
  2. DependencyInjection/Configuration.php — this is the authoritative list of config keys (access token, project ID, HTTP client options). Symfony bundles document config here, not in a README.
  3. Resources/config/services.* (.yaml/.xml) — shows how services are wired and what interfaces/aliases exist for injection.
  4. composer.json — confirm the Symfony version constraints and whether symfony/http-client is required.

First use case: send a single push to one Expo token.

// Assuming you've instantiated/wired the bundle's sender class manually
$message = (new NotificationMessage())
    ->setTo('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]')
    ->setTitle('Order shipped')
    ->setBody('Your parcel is on the way.')
    ->setData(['order_id' => 123]);

$response = $sender->send($message); // POSTs to https://exp.host/--/api/v2/push/send

If you can get that one call working against a real ExponentPushToken, you understand the package. Everything else is token management, batching, and receipts.

Implementation Patterns

Bridge the Symfony service into Laravel. The cleanest integration is a thin service provider that instantiates the bundle's sender (or wraps it) and binds it as a singleton, rather than trying to load the bundle's DI extension:

$this->app->singleton(ExpoSender::class, function ($app) {
    return new ExpoSender(
        client: new \Symfony\Component\HttpClient\HttpClient(...),
        accessToken: config('services.expo.access_token'),
    );
});

Keep a config/services.php entry for the Expo access token and, if your SDK version requires it, the projectId.

Store tokens in Eloquent, not the bundle. The bundle has no opinion about persistence (and should not). Create an expo_tokens table with a unique index on the token, user_id, created_at, and last_used_at. This is what lets you do the two things Expo actually requires: dedupe and prune.

Send from queued jobs, not controllers. Batch dispatch is the correct shape:

class SendExpoPush implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public int $userId, public array $payload) {}

    public function handle(ExpoSender $sender): void
    {
        $tokens = ExpoToken::where('user_id', $this->userId)->pluck('token');

        foreach ($tokens->chunk(100) as $chunk) {   // Expo hard limit is 100/request
            $response = $sender->sendMultiple($chunk->map(
                fn ($t) => (new NotificationMessage())
                    ->setTo($t)
                    ->setTitle($this->payload['title'])
                    ->setBody($this->payload['body'])
                    ->setData($this->payload['data'] ?? [])
            )->all());

            $this->handleTickets($response);
        }
    }
}

Treat the send response as tickets, not delivery. Expo's /push/send returns {data: [{status: "ok", id: "..."}]} — the id is a receipt ID. Persist those IDs in a push_receipts table keyed by message, then run a scheduled command every ~15 minutes that calls /push/getReceipts with the outstanding IDs. Only there do you learn about DeviceNotRegistered or MessageTooBig.

// app/Console/Commands/CheckExpoReceipts.php — schedule every 15 minutes
Schedule::command('expo:check-receipts')->everyFifteenMinutes();

On DeviceNotRegistered, delete the token row immediately. On InvalidCredentials, fail loudly — your server credentials are wrong and no amount of retrying will help.

Mirror Laravel's notification ergonomics. If your team already uses Notification classes, wrap the bundle's sender in a channel (ExpoChannel::send()) so push becomes just another via() option. This keeps call sites idiomatic and hides the Symfony-shaped API.

Gotchas and Tips

Container mismatch is the #1 failure mode. Symfony's autowiring will not resolve in Laravel, and the bundle expects symfony/http-client (usually). Either provide the client yourself via the bridge provider, or swap in a Laravel/Guzzle adapter that implements whatever interface the sender's constructor type-hints. Check the constructor signature before writing the binding.

Config keys live in Configuration.php, not in docs. Symfony's TreeBuilder is often the only place the environment variable names and defaults are written down. Read it before you guess at EXPO_ACCESS_TOKEN vs EXPO_TOKEN.

Validate the token format client-side. Tokens look like ExponentPushToken[...] or ExpoPushToken[...]. Sending a malformed one gets you a per-message error inside an otherwise 200 OK response — the HTTP status is not the result.

A 200 response is not success. Expo returns HTTP 200 with a data array where individual entries can have status: "error" and an error code. Log the raw response body during initial integration; treat "all messages ok" as a post-send assertion, not an HTTP-level check.

Rate limits are per-project and low. Expo's documented limit is roughly 600 requests/second per project and a small number of messages per request. Your chunking strategy matters more than your retry strategy; back off on 429s rather than hammering.

Receipts expire. Expo purges receipt data after a day or so. If your scheduled command is down longer than that, you lose visibility into failures — so schedule it reliably and clean up old push_receipts rows.

Bounded retries only. Retry on 429/5xx with exponential backoff (Laravel's $backoff on the job works); never retry on DeviceNotRegistered or InvalidCredentials — those are terminal and will just burn queue throughput.

Debugging tips: Http::fake() does not intercept the Symfony client — mock the Symfony HttpClientInterface instead, or inject a fake client into your bridge. Log the ticket IDs you persist; that's the only way to correlate a user complaint ("push didn't arrive") back to a receipt error days later.

Extension points: the useful surfaces are the sender service (swap the HTTP client for testing/observability) and the message value object (subclass or compose to add sound, badge, channelId, priority — check what setters actually exist before assuming parity with Expo's full payload schema).

Version pinning matters. With 20 stars and zero dependents, this package has no ecosystem pressure to stay current with Symfony/Laravel releases. Pin it, run your test suite, and be prepared to fork the one or two classes you depend on if Symfony 7/8 compatibility drifts. For a production Laravel app, forking ten lines of an HTTP POST is frequently the lower-risk choice.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator