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

Fcm Laravel Package

laravel-notification-channels/fcm

A Laravel notification channel for Firebase Cloud Messaging (FCM). Send push notifications to Android, iOS, and web devices through Laravel's notification system, with support for topics, data payloads, and the FCM HTTP v1 API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the config:

composer require laravel-notification-channels/fcm
php artisan vendor:publish --tag=fcm-config

v6 requires PHP 8.4+ and Laravel 12+. If you're still on Laravel 11 or PHP 8.3 you cannot take this release — pin ^5.0 and accept the older API surface (see the version-locking note under Gotchas before you let Composer drift).

The package talks to the FCM HTTP v1 API, so you need a service account rather than the old server key. In the Firebase console: Project settings → Service accounts → Generate new private key, drop the JSON somewhere outside public/ (e.g. storage/app/firebase/service-account.json), then:

FCM_CREDENTIALS=/var/www/app/storage/app/firebase/service-account.json

The project id normally comes from that file; the config exposes an override if you run multiple Firebase projects. Run php artisan config:clear after touching .env — a cached config with a stale path is the single most common "it worked locally" failure.

Give your notifiable a route. The channel calls routeNotificationFor('fcm'), which resolves to routeNotificationForFcm():

// app/Models/User.php
public function routeNotificationForFcm(): string|array|null
{
    return $this->fcm_token; // or $this->devices()->pluck('token')->all()
}

Your first notification:

use NotificationChannels\Fcm\FcmChannel;
use NotificationChannels\Fcm\FcmMessage;
use NotificationChannels\Fcm\Resources\Notification as FcmNotification;

class OrderShipped extends Notification
{
    public function __construct(public Order $order) {}

    public function via($notifiable): array
    {
        return [FcmChannel::class];
    }

    public function toFcm($notifiable): FcmMessage
    {
        return FcmMessage::create()
            ->setNotification(
                FcmNotification::create()
                    ->setTitle('Order shipped')
                    ->setBody("Order #{$this->order->id} is on its way")
            )
            ->setData([
                'order_id' => (string) $this->order->id,
                'type'     => 'order_shipped',
            ]);
    }
}

First places to read: config/fcm.php, then src/FcmChannel.php (what actually gets sent), src/FcmTopicChannel.php (the new topic-send path), src/FcmMessage.php (the top-level payload), and src/Resources/ — every platform-specific knob (AndroidConfig, ApnsConfig, WebpushConfig, FcmOptions) is a typed value object there. The README's per-platform snippets are the fastest reference; ignore blog posts older than v2, they use the decommissioned legacy API.

Implementation Patterns

Fan-out to multiple devices. Return an array from routeNotificationForFcm(); the channel loops and issues one v1 request per token. Store tokens in a devices table keyed by user + platform so you can prune per-device rather than guessing.

Topics via the new FcmTopicChannel. v6 ships a dedicated channel for topic sends, which is the cleanest way to do "everyone subscribed to promotions" — no server-side token bookkeeping, no fan-out. Register it in via() alongside (or instead of) FcmChannel and give the model its topic route; check src/FcmTopicChannel.php for the exact route method name in your installed version before wiring your model.

public function via($notifiable): array
{
    return [FcmTopicChannel::class];
}

You can still target a topic from a plain FcmMessage with setTopic('promotions'). Reach for the raw message when you need condition targeting rather than a single topic:

return FcmMessage::create()
    ->setCondition("'promotions' in topics || 'vip' in topics")
    ->setNotification(FcmNotification::create()->setTitle('Flash sale')->setBody('30% off today'));

setToken(), setTopic(), and setCondition() are mutually exclusive — pick one targeting mode per message.

Data-only (silent) messages. Omit setNotification() entirely and let the app decide what to render. Android needs to be woken up:

FcmMessage::create()
    ->setData(['type' => 'sync', 'payload' => json_encode($this->changes)])
    ->setAndroid(AndroidConfig::create()->setPriority('high'))
    ->setApns(ApnsConfig::create()
        ->setHeaders(['apns-priority' => '5', 'apns-push-type' => 'background'])
        ->setPayload(ApnsPayload::create()->setAps(Aps::create()->setContentAvailable(true))));

Platform helpers. v6 adds android() and ios() shortcuts on the message class for the common case of attaching platform-specific notification data:

FcmMessage::create()
    ->setNotification(FcmNotification::create()->setTitle('Order shipped')->setBody('...'))
    ->android(channelId: 'orders', clickAction: 'OPEN_ORDER')
    ->ios(sound: 'default', badge: $unread);

Treat these as the 80% convenience path. Drop down to the full setAndroid(AndroidConfig::create()->...) / setApns(ApnsConfig::create()->...) objects the moment you need headers, APNs priority, content-available, or FcmOptions — the helpers don't replace them, and mixing both on one message is a good way to silently lose a setting.

Platform polish (full config).

->setAndroid(AndroidConfig::create()->setNotification(
    AndroidNotification::create()
        ->setChannelId('orders')
        ->setClickAction('OPEN_ORDER')
        ->setImage($this->order->imageUrl)
))
->setApns(ApnsConfig::create()
    ->setHeaders(['apns-priority' => '10'])
    ->setPayload(ApnsPayload::create()->setAps(Aps::create()->setSound('default')->setBadge($unread))))
->setFcmOptions(FcmOptions::create()->setAnalyticsLabel('order_shipped'));

On-demand notifications (no user model, e.g. after a guest checkout or from a webhook) skip the notifiable entirely:

Notification::route('fcm', $deviceToken)->notify(new OrderShipped($order));

Shared base notification. Most apps end up with 90% identical payloads. Extract an abstract FcmNotification base with a protected function basePayload(): FcmMessage that sets channel id, click action, analytics label, and a common data.type, then let subclasses override title/body/data. Keeps deep-link routing consistent across the app.

Testing. Notification::fake() covers "was it sent"; add a payload assertion so schema regressions don't reach production:

Notification::fake();

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

Notification::assertSentTo($user, OrderShipped::class, function ($notification, $channels) use ($user) {
    $payload = $notification->toFcm($user)->toArray();

    return in_array(FcmChannel::class, $channels)
        && $payload['data']['type'] === 'order_shipped';
});

Queue it. Implement ShouldQueue on noisy notifications and give the queue worker retries/backoff; a fan-out to 5k tokens inside a web request will time out.

Gotchas and Tips

  • v6 is a platform bump, not just a feature drop. PHP 8.4+ and Laravel 12+ are hard requirements. On a Laravel 11 app your composer update will resolve back to v5 — which is fine, but it means your codebase may be running v5 APIs while the README documents v6 (FcmTopicChannel, android()/ios() helpers won't exist). Check composer show laravel-notification-channels/fcm before copy-pasting from the docs.
  • Pin the major version in composer.json. Each of v1→v2 (value objects, legacy API removal) and v5→v6 (Laravel 12 floor) changes what you can call, so an unpinned upgrade is a rewrite or a silent downgrade, never a patch. Use "^6.0" or "^5.0" deliberately.
  • The legacy server key is gone. Upgrading from v1 means FCM_SERVER_KEY no longer exists; every send now needs the service-account JSON. A 401 with a valid-looking key almost always means you're still on the old config.
  • Name collision: NotificationChannels\Fcm\Resources\Notification vs Illuminate\Notifications\Notification. Always alias it (as FcmNotification) or you'll get fatal errors in your first file.
  • data values must be strings. FCM rejects (or silently drops) ints, bools, and nested arrays in the data payload. Cast: (string) $id, $flag ? '1' : '0', and json_encode() any structure. This bites hardest with Eloquent models — never pass $order->toArray().
  • notification vs data changes background behavior. With a notification block, the OS renders the tray entry and onMessageReceived may not fire while backgrounded; without one, the app must wake itself. Decide per notification type, not per app.
  • Topic sends have no per-device feedback. FcmTopicChannel / setTopic() returns a message id, not per-token results — you cannot detect unregistered tokens this way, and a user who never subscribed on-device gets nothing with zero errors. If you need delivery observability, fan out to tokens instead, or subscribe clients deliberately and verify server-side via the Topic Management API.
  • Empty route = silent no-op. If routeNotificationForFcm() returns null/[], the channel returns early without throwing. Users with a missing fcm_token just never get notified. Log or alert on that path in routeNotificationForFcm().
  • Token rot is your job. Tokens rotate on reinstall/restore. Catch NotificationChannels\Fcm\Exceptions\CouldNotSendNotification, inspect the response for UNREGISTERED / INVALID_ARGUMENT and delete the token; SENDER_ID_MISMATCH means the token belongs to a different Firebase project (staging vs prod credentials swapped).
  • One bad token can kill a queued job. When sending to an array of tokens, wrap each send or send per-token jobs, otherwise the exception aborts the remaining fan-out and the retry re-notifies everyone who already succeeded.
  • iOS silent pushes are easy to get wrong: apns-push-type: background + content-available: 1 + priority 5 (not 10). Priority 10 background pushes get throttled or dropped by APNs. The ios() helper won't set these for you.
  • Android 8+ needs a channel id (setChannelId('orders')) with a matching channel created client-side; without it the notification vanishes with no error server-side.
  • Debug the payload, not the response. dd($notification->toFcm($user)) is faster than guessing why nothing renders. The response body only tells you about API-level rejection, not "the OS decided not to display it".
  • Extension points: subclass FcmChannel (bind it in a service provider or reference it from via()) to add logging, metrics, or automatic invalid-token cleanup in one place; tune HTTP timeouts/client options through the config, and use setFcmOptions() for analytics labels rather than shoehorning tags into data.
  • Housekeeping: v6 moved the codebase to Pint. If you maintain a fork or vendor patch, expect a large whitespace-only diff on rebase; filter it out before reviewing real changes.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route