kutia-software-company/larafirebase
Larafirebase is a Laravel package that makes it easy to send Firebase Cloud Messaging push notifications. It supports the FCM HTTP v1 API, topic and device-group messaging, and a fluent builder for composing notification payloads with data, sound, and custom options.
The package is a thin, fluent wrapper around Firebase Cloud Messaging (FCM): it builds the HTTP payload, signs the request, and returns the decoded FCM response. There is no database layer, no channel, no queue integration — it sends one request and hands you back the result. Treat it as a transport, not a notification system.
Install and configure
composer require kutia-software-company/larafirebase
php artisan vendor:publish --provider="Kutia\Larafirebase\Providers\LarafirebaseServiceProvider"
That publishes config/larafirebase.php. Open it first — the credential keys differ between 1.3.x releases, and this single file tells you which FCM API generation you're on:
Authorization: key=... style setting, you are on the legacy fcm.googleapis.com/fcm/send API./v1/projects/{project}/messages:send) with OAuth2.Put the credential in .env (FIREBASE_SERVER_KEY or FIREBASE_CREDENTIALS, matching the config file) and add it to your secrets manager. Since 1.3.9 the package declares support for Laravel 11, 12, and 13, so you can install it on a current app without a --with-all-dependencies fight.
First send
use Kutia\Larafirebase\Facades\Larafirebase;
Larafirebase::withTitle('Order shipped')
->withBody('Your order #1042 is on the way.')
->withAdditionalData(['order_id' => '1042', 'type' => 'shipping'])
->sendMessage($user->deviceTokens()->pluck('token')->all());
Where to look first: the package source itself (vendor/kutia-software-company/larafirebase/src/). The main class is a few hundred lines of setter methods plus one Guzzle call — ten minutes of reading removes all ambiguity about method names (withTitle, withBody, withImage, withIcon, withSound, withBadge, withAdditionalData, withTopic, sendMessage, sendTopicNotification) and about what the return value actually contains. Read it before you build abstractions on top of it.
Own the token storage; the package doesn't. You need a device_tokens table (user_id, token unique, platform, last_seen_at). Upsert on login/registration, delete on logout, and de-duplicate — the same physical device will hand you a new token after reinstall or data clear, and the old one is dead weight that inflates every send.
Route::post('/device-tokens', function (Request $request) {
$request->user()->deviceTokens()->updateOrCreate(
['token' => $request->string('token')],
['platform' => $request->string('platform'), 'last_seen_at' => now()],
);
});
Wrap sends in a queued job. FCM round-trips from a few hundred milliseconds to several seconds on a bad day. Never call the facade inline in a controller that a user is waiting on.
PushToUser::dispatch($user, 'Order shipped', 'Your order #1042 is on the way.', ['order_id' => '1042'])
->onQueue('notifications');
Inside the job, catch the package exception, log the response body, and — critically — prune tokens that come back as unregistered. This is the one piece of housekeeping that keeps a push system healthy over months.
Chunk multicast sends. FCM caps recipients per request (500 for tokens, historically 1000). If you're sending to every follower of a topic, use topic messaging instead of fanning out tokens; if you genuinely need per-device sends to a large set, array_chunk($tokens, 500) and send a request per chunk.
Build your own notification channel. The package does not ship a Laravel notification channel, and this is the natural extension point. A FcmChannel with send($notifiable, Notification $notification) that calls $notification->toFcm($notifiable) and delegates to the facade lets you write $user->notify(new OrderShipped($order)) and keep the fluent API out of your domain code.
public function send($notifiable, Notification $notification): void
{
$message = $notification->toFcm($notifiable);
Larafirebase::withTitle($message->title)
->withBody($message->body)
->withAdditionalData($message->data)
->sendMessage($notifiable->routeNotificationForFcm());
}
Data payloads are for routing, not content. Send an id and a type (['type' => 'shipping', 'order_id' => '1042']) and let the client fetch the real object. It keeps you under payload limits, avoids stale data in the tray, and means a copy change doesn't require a re-send.
Topics for broadcast, tokens for personal. ->withTopic('news')->sendTopicNotification(...) is the right tool for announcements; it's one request regardless of audience size. Topic subscription is done client-side via the Firebase SDK (or server-side via the IID API) — the package only publishes to the topic, it doesn't manage memberships.
The legacy API is the biggest risk here. The release notes for 1.3.9 are purely framework-compatibility (Laravel 11/12/13 in PRs #70, #73, #87, #88) — no new messaging features. So the API generation you get depends on which 1.3.x you install. Google shut down the legacy fcm.googleapis.com/fcm/send endpoint, and a package still posting there with Authorization: key=<server key> will fail in production even though it worked in a tutorial. Check the base URL in config/larafirebase.php and, if it's the legacy endpoint, either upgrade to a version that targets HTTP v1 with a service account, or bypass the package with your own Guzzle client. The server key from the Firebase console is not accepted by v1 — v1 needs an OAuth2 bearer token minted from the service account JSON.
Data values must be strings. FCM rejects (or silently coerces) nested arrays and non-string scalars in the data payload. ['count' => 5] and ['meta' => ['a' => 'b']] are both landmines — json_encode the object and cast scalars to string yourself.
withSound / withBadge / withIcon are APNs-shaped. They land reliably on iOS and are frequently ignored on Android. If notification appearance matters cross-platform, use the android/apns platform-specific blocks in the payload rather than hoping the shared fields carry through.
Wrap every send and never let it fail a transaction. If you send inside a DB::transaction, a push failure rolls back real writes. Use DB::afterCommit() or dispatch the job after commit. Wrap the call in try/catch and log the FCM response body — the status code alone won't tell you whether one token in a batch of 50 failed.
Token cleanup signals: NotRegistered, UNREGISTERED, messaging/registration-token-not-registered, and HTTP 404 all mean "delete this token." HTTP 401/403 means your credentials are wrong — do not delete tokens on those, or a misconfigured deploy will wipe your entire push audience in one queue run.
php artisan config:cache interactions. Any env() call outside config/ returns null once config is cached. Keep all credential reads inside config/larafirebase.php and read via config() everywhere else.
Testing. The facade is fakeable: Larafirebase::shouldReceive('sendMessage')->once()->andReturn([...]) is enough for job and channel tests. Note that Http::fake() will not intercept the package's Guzzle client, so integration-style tests need GuzzleHttp\HandlerStack mocking or a real test project. Test the send-on-a-real-device path once manually: emulators without Google Play Services never obtain a token, which produces a very confusing "no tokens in the database" bug.
No retry logic, no delivery receipts. The package is fire-and-forget; FCM itself is best-effort and unordered. If you need guaranteed delivery, that's your own outbox table plus a scheduled retry — don't assume the facade call succeeded just because it didn't throw.
How can I help you explore Laravel packages today?