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.
Architecture fit — this is the headline finding: the package is a Symfony bundle, not a Laravel package. solvecrew/expo-notifications-bundle is built around Symfony's bundle contract: a *Bundle class, a DependencyInjection\Extension that parses a Configuration tree, service definitions in YAML/XML, and likely autowired constructor injection for its Expo client and receipt-poller. None of that machinery exists in Laravel. Laravel's package ecosystem relies on ServiceProvider / Facade / register()+boot(), and the framework does not read Symfony bundle Extension classes, so installing this via Composer will register classes in the autoloader but will not boot the services, config, or any receipt-checking subscriber. In practice a Laravel team would be adopting a set of plain PHP classes and hand-wiring them.
What it actually does (and why the underlying problem is worth solving). The scope is the server side of Expo push: register/store ExponentPushTokens, batch a send to https://exp.host/--/api/v2/push/send (Expo's ~100-message chunk limit), parse the per-message ticket response, then later call /push/getReceipts to distinguish DeviceNotRegistered tokens from transient failures, and prune dead tokens. That receipt-and-prune loop is the genuinely valuable, commonly-underbuilt part of Expo integration, and the package's abstraction over it is the main argument in its favour.
Integration feasibility for a Laravel/PHP shop: low as a dependency, moderate as a library. Two paths exist: (a) install it and treat it as an unopinionated library — feasible only if its classes avoid tight coupling to Symfony ContainerInterface, Config components, and EventDispatcher, which a bundle of this kind typically does not; or (b) lift the logic. The send path is thin (an HTTP POST with Authorization: Bearer <accessToken> and a JSON array body), and the receipt path is a second POST plus a diff against stored tokens. A senior engineer reimplements both in one to three days with Illuminate\Support\Facades\Http, a notifications jobs table or queue, and a scheduled php artisan command for receipts. That is comparable effort to untangling the bundle's bootstrapping, with full control over retry semantics.
Technical risk.
Key questions to resolve before committing.
ContainerInterface or EventDispatcherInterface)?data.status === "error" / DeviceNotRegistered branch of the receipt response, not just the ticket response?429 with Retry-After?Verdict. Technically credible for its stated ecosystem, but the ecosystem is the wrong one. The right move for a Laravel team is to read this package as a reference implementation, not to install it — harvest the receipt-polling design, then build the ~200 lines natively against Laravel's HTTP client and queue, or adopt an actively maintained Laravel-native Expo notification channel if one meets the receipt-handling bar.
Stack fit. Poor as a Composer dependency in a Laravel application; good as a pattern source. The bundle's assumptions — Symfony DI, bundle config trees, likely a Symfony HttpClientInterface and EventDispatcher — collide with Laravel's container, config repository, queue, and scheduler. There is no illuminate/* bridge in the package and no evidence of Laravel support in its scope statement.
Recommended path: port, don't install. Treat the repository as an architecture reference and reimplement against Laravel primitives. Concretely:
expo_push_tokens table (user_id, token, device_id, platform, last_seen_at, revoked_at) with a unique index on token. This replaces whatever persistence the bundle assumes and gives you Eloquent access for pruning./--/api/v2/push/send, using Http::withToken(config('services.expo.token'))->retry(3, 200)->post(...). Wrap in a NotificationChannel so it composes with Laravel's Notifiable trait, which the bundle cannot offer.ExpoCheckReceipts artisan command scheduled every 15–30 minutes, keyed off a push_receipts table populated at send time from each ticket's id. This is the piece worth copying carefully: map DeviceNotRegistered → revoke token; MessageTooBig/MessageRateExceeded → log and don't retry blindly; InvalidCredentials → alert, because it means your access token is wrong and every send is silently failing.config/services.php entry (expo.token, expo.batch_size, expo.receipt_batch_size) rather than a Symfony Configuration tree.Migration path if you must evaluate the dependency first. Sequence a two-day spike: (1) composer require solvecrew/expo-notifications-bundle in a throwaway Laravel app and attempt to instantiate the send/receipt service manually with new, no container; (2) if it constructs cleanly, write a thin Laravel ServiceProvider that binds the client singleton and exposes config from services.php; (3) run it against Expo's sandbox push tool to confirm the payload shape and receipt round-trip. If step 1 fails — the likely outcome — stop and port. Do not attempt to register the bundle with Symfony\Component\HttpKernel, and do not pull in symfony/framework-bundle to satisfy it; that is a full parallel framework inside your Laravel app and an unambiguous anti-pattern.
Compatibility checks. PHP version constraint versus your runtime; whether it requires symfony/http-client (which will coexist with Guzzle but means two HTTP stacks and two sets of TLS/timeout semantics in one app); whether it requires doctrine/orm or symfony/serializer transitively; and whether the Expo API version it targets is still live. All four are quick composer why / source-read checks and all four can independently disqualify it.
Sequencing for a greenfield rollout. Deploy token registration and storage first (a POST /push-tokens endpoint, idempotent on token, called from the React Native client on login and on token refresh). Ship sends second, behind a queue, with a feature flag and a single internal test device. Add receipt polling third — explicitly later, not in the same release — because that is where the token-lifecycle bugs surface and you want send-path stability first. Only after receipts are trustworthy should you enable fan-out to real user segments.
Maintenance burden. Low if ported (~200 lines you own, covered by tests you control); elevated and asymmetric if adopted, because you would be maintaining a Symfony bundle inside a Laravel app — patching its bootstrapping to survive framework upgrades, and absorbing any upstream breakage yourself given 0 dependents and a single maintainer. Budget a standing dependency-review line item, and expect to vendor the source within a year.
Support. No commercial support, no visible Slack/Discussions, 20 stars means effectively no community. Escalation path for a production incident is "read the source and fix it yourself," which is acceptable only if you have already decided the code is small enough to own — in which case porting is strictly cheaper.
Scaling. The Expo push API is the bottleneck, not the package: ~100 messages per request, per-second rate limits, and receipt lookups batched by ticket ID. A naive synchronous send inside a web request will fall over at modest user counts (a 10k-user broadcast at 100/request is 100 sequential HTTPS calls — minutes of latency). Design for a queued, chunked job with concurrency limited by Expo's rate limits, and store receipts in the database so polling is a cursor scan rather than an in-memory list. The package's own scaling story is opaque; assume you own it.
Failure modes to instrument explicitly.
InvalidCredentials, sends appear successful at the HTTP layer. Alert on any non-ok ticket status.503/429 with Retry-After must back off; a naive retry(5) on a queued fan-out multiplies load against an already-degraded provider.device_id or you fan out duplicates to one user.Ramp-up. For a Laravel engineer with no Expo experience: roughly 1–2 days to read this package plus Expo's push docs and produce a working native implementation, plus 2–4 days to harden receipts, pruning, and observability. For the install-the-bundle path, add an indeterminate amount of time untangling Symfony bootstrapping — the classic outcome is a day spent proving it cannot be wired cleanly, followed by the port anyway. That asymmetry, not the star count, is the decisive argument.
Recommendation. Do not add this as a production dependency. Use it as a design reference to build a Laravel-native Expo channel, or evaluate a maintained Laravel-specific alternative that already implements receipt polling. Reassess only if the maintainer publishes a framework-agnostic core with a documented Laravel adapter.
How can I help you explore Laravel packages today?