ankurk91/fcm-notification-channel
Laravel notification channel for Firebase Cloud Messaging (FCM). Send push notifications to device tokens, topics, or conditions through Laravel's notification system, with fluent message builders and simple service account configuration.
Before installing anything, confirm you actually can send a push: the package targets the legacy FCM HTTP API (https://fcm.googleapis.com/fcm/send) using a server key, and that API was turned off in mid-2024. If you are starting fresh today, this package will likely fail in production regardless of how well you wire it up. Treat the first task as "can I still authenticate against FCM at all?" — test a single curl against the endpoint with your server key before writing any app code.
If you have a working legacy key (or you're maintaining a legacy project that still has one), the minimal path is:
composer.json constraints yourself.
composer require ankurk91/fcm-notification-channel
config/fcm.php (server key, endpoint, timeout). Publish and inspect it before touching anything else — this is where the API URL lives, so it is the natural place to point at a proxy or a compatibility shim if you have one.
php artisan vendor:publish --tag=fcm
.env. The server key typically comes from the Firebase console under Project Settings → Cloud Messaging. Never hardcode it in the config file.fcm_token (nullable string, indexed) to your users table, or wherever you store device tokens.Notifiable model:
public function routeNotificationForFcm()
{
return $this->fcm_token;
}
via() and toFcm():
public function via($notifiable) { return [FcmChannel::class]; }
public function toFcm($notifiable)
{
return FcmMessage::create()
->to($notifiable->fcm_token)
->data(['title' => 'Hi', 'body' => 'It works']);
}
Then $user->notify(new WelcomeNotification).The first real use case is almost always transactional: "order shipped", "your ride is arriving", "new message" — a small payload with a title/body and a deep-link key in data.
If step 6 returns a 401/404 from Google, stop. You are almost certainly hitting the dead legacy endpoint and need a different transport (HTTP v1 via a service account, or a maintained channel such as laravel-notification-channels/fcm v2 / kreait/laravel-firebase).
Single device vs. topic vs. condition. The message builder exposes targeting at the message level, and this is the main lever you'll pull:
// Single device
FcmMessage::create()->to($token);
// Fan-out to a topic (subscribed client-side)
FcmMessage::create()->toTopic('news');
// Boolean expression over topics
FcmMessage::create()->toCondition("'news' in topics && 'sports' in topics");
Use toTopic() for broadcast-style features (everyone who opted into a category) and to() for per-user transactional pushes. Conditions are powerful but painful to debug — reserve them for genuinely combinatorial audience rules.
Data-only vs. notification payloads. Decide this early, because Android and iOS behave very differently:
content-available handling).Most teams end up data-only for control, or notification payloads for simplicity. Pick one and make it a lint rule in the notification classes.
Queue the notification, not the channel. This is standard Laravel, but it matters more here because FCM round-trips are slow:
class OrderShipped extends Notification implements ShouldQueue
{
public $tries = 3;
public $backoff = [10, 60, 300];
}
Retries interact badly with fan-out sends (you can double-deliver to topics). If a notification targets a topic, keep $tries = 1 or accept duplicates.
One notification, multiple channels. The real value of the notification system is that FCM sits alongside mail/database:
public function via($notifiable)
{
return $notifiable->wantsPush()
? [FcmChannel::class, 'database']
: ['database'];
}
Guard on user preference and on token presence — sending to a user with fcm_token = null wastes a request and can log noise.
Token lifecycle. Register tokens on login/app start, and clean them up on logout. The package surfaces FCM's "not registered" responses via exceptions on failure; wire this into your token store:
public function failed($notifiable, $exception) { /* remove dead token */ }
For batch sends, wrap the loop and delete tokens that come back as NotRegistered / InvalidRegistration.
Test with fakes. Standard Laravel:
Notification::fake();
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class);
This is the only sane way to test the channel, because actually running the transport requires live credentials.
Custom messages per audience. Extract builders so notification classes stay readable:
FcmMessage::create()
->to($token)
->priority('high')
->timeToLive(3600)
->data(['type' => 'order', 'id' => (string) $order->id]);
curl that your key still works. If it doesn't, the correct move is a migration to the HTTP v1 API (service account + OAuth2), which this package does not implement. A maintained alternative is the right call for new work.data values must be strings. FCM rejects integers/booleans in data. Cast aggressively: (string) $order->id, never $order->id.php artisan config:cache, changing the key in .env at runtime does nothing. Always config:clear when debugging credentials.priority('high') for time-sensitive pushes; set a TTL on anything where a stale push is worse than no push (e.g. "your table is ready"). Without it FCM may deliver hours late.routeNotificationForFcm returning an array sends to multiple tokens in one call — convenient for multi-device users, but a single bad token can affect the batch, and per-token error reporting gets fuzzy. Prefer one call per live token when you need precise invalidation.notification blocks are rendered by the OS; data blocks are not. The same payload will look different across platforms and across app foreground/background states. Test on real devices, not just via Notification::fake().FcmChannel for your own subclass in via(), and (b) the message builder itself. If you need HTTP v1, signing, or per-tenant service accounts, you're writing a parallel channel from scratch rather than extending this one.CouldNotSendNotification-style exception on non-2xx; log the response body in your notification's failed() hook. Google's error strings ("SenderId mismatch", "MismatchSenderId") are the fastest path to the real problem.How can I help you explore Laravel packages today?