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 Laravel Package

laravel-notification-channels/expo

Laravel notification channel for sending push notifications through Expo. Add Expo push support to your Laravel app using familiar notification classes, with token handling and customizable message payloads.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the channel and make sure your app has an Expo push token ready:

composer require laravel-notification-channels/expo

If the package ships a config file, publish it and set your Expo access token (required only if you enabled enhanced push security in the Expo dashboard):

php artisan vendor:publish --tag=expo-config
EXPO_ACCESS_TOKEN=your-expo-access-token

Add a route resolver to your notifiable model. This is the single most important integration point — the channel resolves the recipient through Laravel's normal routeNotificationFor('expo') mechanism:

// app/Models/User.php
public function routeNotificationForExpo(): ?string
{
    return $this->expo_push_token;
}

Create a notification and return an ExpoMessage from toExpo():

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use NotificationChannels\Expo\ExpoChannel;
use NotificationChannels\Expo\ExpoMessage;

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public bool $afterCommit = true;

    public function __construct(public int $orderId) {}

    public function via(object $notifiable): array
    {
        return $notifiable->routeNotificationFor('expo')
            ? [ExpoChannel::class]
            : [];
    }

    public function toExpo(object $notifiable): ExpoMessage
    {
        return ExpoMessage::create()
            ->title('Order shipped')
            ->body("Order #{$this->orderId} is on its way.")
            ->data(['route' => 'orders.show', 'order_id' => $this->orderId])
            ->sound('default')
            ->channelId('orders');
    }
}

Your first useful use case is a transactional push: order status, chat message, booking reminder, or account alert. Trigger it from a domain event or controller and let Laravel's notification system handle dispatch:

$user->notify(new OrderShipped($order->id));

Look first at the package README for the exact fluent method list, then src/ExpoChannel.php to see how routes and errors are handled, and src/ExpoMessage.php for the supported Expo payload fields. Keep Expo's own push notification docs open for payload semantics — the package is a thin wrapper around Expo's HTTP API.

Implementation Patterns

Conditional channel selection. Only include ExpoChannel::class in via() when a token exists. This avoids noisy failures for users who have not registered a device or have uninstalled the app:

public function via(object $notifiable): array
{
    return array_filter([
        $notifiable->routeNotificationFor('expo') ? ExpoChannel::class : null,
        'mail', // fallback
    ]);
}

Token registration endpoint. Treat Expo tokens as mutable device records, not a single column on users. A user may have several devices, and tokens rotate:

Route::post('/push-tokens', function (Request $request) {
    $data = $request->validate([
        'token' => ['required', 'string', 'starts_with:ExponentPushToken,ExpoPushToken'],
        'platform' => ['required', 'in:ios,android'],
    ]);

    $request->user()->expoTokens()->updateOrCreate(
        ['token' => $data['token']],
        ['platform' => $data['platform'], 'last_seen_at' => now()],
    );
});

Multi-device delivery. The channel typically sends one message to one token. If you need fan-out, loop over tokens with on-demand notifications rather than trying to return an array from routeNotificationForExpo():

foreach ($user->expoTokens()->where('active', true)->get() as $device) {
    Notification::route('expo', $device->token)->notify(new OrderShipped($order->id));
}

On-demand routing is also useful for one-off pushes to a token you just received from the client:

Notification::route('expo', $token)->notify(new WelcomePush());

Queue everything. The channel performs a synchronous HTTP request to Expo during send(). Implement ShouldQueue, and use $afterCommit = true so pushes are not sent for transactions that roll back.

Deep links via data. Put navigation intent in the payload, not in the body text. Keep keys stable and document them for the mobile team:

->data([
    'route' => 'chat.show',
    'chat_id' => $this->chat->id,
    'badge' => $unreadCount,
])

Platform options. Use channelId() for Android 8+ notification channels, sound() and badge() for iOS behavior, and ttl()/priority() for delivery urgency. The Android channel ID must already exist in the app — creating it client-side is an app responsibility.

Localization. Translate title and body server-side at send time using the recipient's locale. The app cannot re-localize a push after it has been delivered.

Testing. Use Notification::fake() to assert the notification was routed to the Expo channel, and unit-test toExpo() directly to verify payload shape:

Notification::fake();

$this->postJson('/orders/123/ship');

Notification::assertSentTo(
    $user,
    OrderShipped::class,
    fn ($notification, $channels) => in_array(ExpoChannel::class, $channels, true)
);

For HTTP-level tests, check whether the package uses Laravel's HTTP client or Guzzle directly; if it uses the Laravel client, Http::fake() works. Otherwise mock the underlying client or keep HTTP assertions in a small integration test.

Gotchas and Tips

Missing route resolver. If routeNotificationForExpo() is absent or returns null, the channel has no recipient. Always guard via() as shown above; otherwise you will get exceptions or silent no-ops depending on the package version.

Expo Go limitations. Remote push notifications in Expo Go on Android were removed in SDK 53. Testing against Expo Go will give false negatives — use a development build or a physical device. iOS simulators also cannot receive push notifications.

Ticket errors are not delivery failures. Expo's send endpoint often returns HTTP 200 with per-message errors in the response body, such as DeviceNotRegistered or InvalidCredentials. Inspect ExpoChannel::send() to see what it throws and what it swallows. Wrap sends in try/catch, log failures, and never let a push failure break a checkout or signup flow.

Receipts and token hygiene. Expo returns a ticket ID immediately; the actual delivery result arrives later via the receipts endpoint. The channel may not expose ticket IDs to your application. If it does not, plan either a small custom wrapper around Expo's API or a scheduled job that queries receipts and deactivates tokens returning DeviceNotRegistered. Without this, your token table accumulates dead devices.

Synchronous HTTP latency. The push request happens inside the notification send. A slow Expo response becomes a slow request if you forget ShouldQueue. Set a reasonable timeout in the package config if available.

Access token and config caching. If enhanced push security is enabled in the Expo dashboard, requests without the Bearer token fail. After changing .env, run php artisan config:cache in deployments, and verify the published config key name matches the package version.

Payload size limit. Expo payloads have a roughly 4 KB limit. Do not embed base64 images or large objects in data; store them server-side and send identifiers.

Android channels are mandatory. On Android 8+, a notification with a channelId that the app has not created will not display. Align server-side IDs (orders, chat, default) with the channels registered in the app.

Badge counts need server logic. iOS badge values are absolute, not incremental. Compute unread counts in Laravel, or explicitly send 0 when the user reads everything.

Batching and rate limits. Expo rate-limits around 600 notifications per second and accepts up to 100 messages per request. The notification channel model sends one token per send, so high-volume fan-out should be chunked through a queued job rather than a tight loop in a controller.

Small-package risk. This is a low-adoption wrapper (roughly 69 stars, no dependents). Pin the version in composer.json, read the source before relying on edge-case behavior, and be prepared to fork or vendor it. The Expo API is simple enough that a thin in-house client is a viable fallback if maintenance stalls.

Test on physical devices early. Push behavior differs by OS version, app state (foreground/background/killed), notification permission status, and whether the app is a development build. A payload that works in one state can silently fail in another, so build a small debug screen in the app that prints the received payload and token.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource