kutia-digital-agency/larafirebase
Larafirebase is a Laravel package for sending Firebase Cloud Messaging push notifications via a simple API. Target devices, topics, or conditions with notification and data payloads, making Firebase integration in Laravel quick and clean.
kutia-software-company/larafirebase is a thin Laravel wrapper around the Firebase Cloud Messaging HTTP v1 API. It gives you a fluent facade for building a message payload and dispatching it to device tokens, topics or topic conditions — without pulling in the full kreait/firebase-php SDK.
Release 1.3.9 is a compatibility/metadata release: it adds Laravel 11, 12 and 13 support (PRs #70, #73, #87, #88) and introduces three new contributors. There are no API changes, no deprecations and no config changes in this release. Treat the upgrade from 1.3.8 as low risk.
First steps:
composer require kutia-software-company/larafirebase:"^1.3.9"
The ^1.3.9 floor matters — 1.3.8 and below will fail dependency resolution on Laravel 11+ because the illuminate/* constraints were not widened there.php artisan vendor:publish output for the larafirebase tag) and inspect config/larafirebase.php. The important keys are the path to the service account JSON, the API endpoint/project, and proxy/timeout settings.storage/app/firebase/service-account.json. Add that path to .env rather than committing it.use Kutia\Larafirebase\Facades\Larafirebase;
Larafirebase::withTitle('Order shipped')
->withBody("Order #{$order->id} is on its way")
->sendMessage($user->deviceTokens->pluck('token')->all());
Verify the exact fluent method names against the package README for your pinned version — the recipient method (sendMessage, sendToTopic, sendToCondition) and the payload setters (withTitle, withBody, withData, withAdditionalData, withImage, withSound) have been stable for a long time but the surface has grown incrementally.Where to look first in the source: src/Larafirebase.php (the fluent builder), src/Services/FirebaseService.php or equivalent (the HTTP/OAuth layer), and config/larafirebase.php. There is very little code; reading it end-to-end takes minutes and answers most questions the README does not.
Wrap the facade in a single application-level service. Do not sprinkle Larafirebase:: calls across controllers and listeners. Create App\Services\PushNotifier that owns payload construction and returns a normalised result object. Benefits: one place to add logging, one place to handle token pruning, and an easy seam for faking in tests.
One queued job per logical message, not per token. Push sending is I/O-bound (see the 500-recipient / one-request-per-message note below). Dispatch a job from your domain event and let the queue absorb latency:
final class SendPushNotification implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function __construct(
public readonly array $tokens,
public readonly string $title,
public readonly string $body,
public readonly array $data = [],
) {}
}
Chunk recipient lists. For a fan-out to thousands of devices, chunk into batches of ~500 tokens per job so a single failure does not lose the whole audience and retries stay cheap.
Store device tokens relationally. A device_tokens table with a polymorphic tokenable relation (user, guest session, etc.) plus platform, last_seen_at, and invalidated_at columns. Prune on failure rather than trying to validate tokens up front — FCM tells you when a token is dead, and that is the only reliable signal.
Prefer topics for broadcast, tokens for transactional. Topic sends (sendToTopic) survive token churn and are ideal for announcements. Token sends are the right tool for "your order shipped". Combine them with a subscription-management flow that calls the package's subscribe/unsubscribe methods whenever a user enables or disables a notification category.
Integrate with Laravel's notification system via a custom channel. The most maintainable pattern is a FirebaseChannel in your own app that resolves the notification's toFirebase($notifiable) payload and calls your PushNotifier. This keeps push alongside mail/database channels and keeps the package out of your domain code.
Multi-tenant / multi-project setups. If you push to several Firebase projects, avoid publishing the config twice. Instead swap the credential path at runtime in the service constructor:
config(['larafirebase.path_to_service_account_json' => $tenant->firebase_credentials_path]);
Do this before the facade resolves, or construct a fresh instance, because config is read at send time in most implementations — verify this against the version you pin, since long-running workers will cache whatever you set last.
Testing strategy. Push sending sits behind a queue in real apps, so most tests should be Queue::fake() + assertPushed(SendPushNotification::class). For the unit test of the notifier itself, either bind a fake implementation of your PushNotifier interface, or — if the package uses Guzzle directly — inject a Guzzle MockHandler into the underlying client. Http::fake() only works if the package resolves Laravel's HTTP client; confirm before relying on it. Keep a tiny opt-in integration test that sends to a real test device token, gated behind an env flag, so credential/endpoint breakage is caught in CI rather than production.
composer update silently keeps you on 1.3.8, Laravel 12/13 installs will conflict. Check composer show kutia-software-company/larafirebase after upgrading.illuminate/* in composer.json; it does not mean the package has been exercised against an unreleased framework. If you are on the bleeding edge, run your own smoke test against a real FCM project.int, bool, nested arrays) in the data map. Cast at the boundary: array_map('strval', $data).messaging/registration-token-not-registered (or a 404 with UNREGISTERED). Catch the exception, mark the token invalid, and move on. Do not retry these — a retry budget spent on dead tokens is a retry budget not spent on transient 5xx errors. Classify errors before letting the queue retry: retry 5xx/timeouts, never retry 400/401/404.config:cache will bake in the credentials path. Because the path is usually expressed with storage_path() or env() inside the config file, a cached config deployed to a container with a different filesystem layout will point at nothing. Set the path explicitly per environment and re-run php artisan config:cache on deploy.file_exists() + is_readable() check in a boot-time health check, and keep the file mode 0600.content-available, and apns-priority live under the apns key of the message envelope. If the fluent setters do not cover your case, use the raw-payload escape hatch (fromRaw or equivalent) rather than forking the package — check the version's README for the exact method name.news), not the /topics/news subscription form. Topic names are case-sensitive and allow only [a-zA-Z0-9-_.~%]. Conditions are capped at five topics per expression.Middleware::log() in a local service provider — the package's proxy/timeout config keys are the natural place to hook a custom client if you need finer instrumentation.macro() the facade or build a small subclass that adds your setters and forwards to the raw payload. Avoid editing vendor code — this package moves often enough that a fork will drift.How can I help you explore Laravel packages today?