kreait/laravel-firebase
A Laravel integration for the Firebase PHP Admin SDK that lets you authenticate with service account JSON files or arrays and access Firebase services, including support for multiple projects.
1. Install and let the package wire itself up. The package ships a ServiceProvider with package discovery enabled, so there is no manual registration step on Laravel 11/12.
composer require kreait/laravel-firebase
php artisan vendor:publish --provider="Kreait\Laravel\Firebase\ServiceProvider" --tag=config
Version selection matters more than usual here: 6.x requires Laravel 11/12 and PHP ≥ 8.2, 5.x covers Laravel 9–11. Pin accordingly before you start writing code against it.
2. Get a Service Account first, code second. In the Firebase console: Project Settings → Service Accounts → Generate new private key. Store the JSON outside public/ and outside the repo (e.g. storage/app/firebase/firebase-auth.json, gitignored). Then the minimum .env:
FIREBASE_CREDENTIALS=storage/app/firebase/firebase-auth.json
FIREBASE_DATABASE_URL=https://<your-project>.firebaseio.com
FIREBASE_CREDENTIALS is resolved through base_path() for relative paths (fixed in 1.4.0 for Windows), so storage/app/... works without a leading slash. GOOGLE_APPLICATION_CREDENTIALS is also honored, as is SDK auto-discovery — more on why that's a footgun in the last section.
3. Where to look first. The package README is deliberately thin, and that is the point: it is an integration layer, not an abstraction. The API surface you will actually spend your day in is the underlying SDK documentation at firebase-php.readthedocs.io. The package's own config/firebase.php is the second place — read it end to end once, it documents multi-project structure, tenant IDs, HTTP client options, and log channels inline.
4. First use case — pick the one that proves the credential chain. Verify an ID token from a mobile/web client, because it exercises credential loading, HTTP, and error handling in one shot:
use Kreait\Firebase\Contract\Auth;
Route::post('/session', function (Auth $auth, Request $request) {
$verified = $auth->verifyIdToken($request->bearerToken());
$uid = $verified->claims()->get('sub');
// issue your own Sanctum/session token here
});
If that returns a token instead of an exception, your credentials, project, and network path are all correct. Everything else (Firestore, Messaging, Storage) is downstream of that.
The rule you must internalize immediately: do not instantiate Kreait\Firebase\Factory. The ServiceProvider already did it, configured it, and bound the result. Use constructor injection, the Firebase facade, or app().
The package binds the SDK's component interfaces in the container, so type-hinting the interface keeps your code decoupled from both the package and the SDK version:
use Kreait\Firebase\Contract\Auth;
use Kreait\Firebase\Contract\Firestore;
use Kreait\Firebase\Contract\Messaging;
final class PushNotifier
{
public function __construct(private Messaging $messaging) {}
public function notify(string $deviceToken, string $title, string $body): void
{
$message = \Kreait\Firebase\Messaging\CloudMessage::withTarget('token', $deviceToken)
->withNotification(['title' => $title, 'body' => $body]);
$this->messaging->send($message);
}
}
This is the single highest-leverage pattern: every controller, job, and console command that touches Firebase gets a thin injected service, never a facade call buried in business logic. The Kreait\Laravel\Firebase\Facades\Firebase facade is fine for tinker and one-off artisan commands; it is not a good fit for testable application code.
Projects are defined as named keys under firebase.projects in the config. The facade defaults to the default project; be explicit when you have more than one:
use Kreait\Laravel\Firebase\Facades\Firebase;
$auth = Firebase::auth(); // default project
$appAuth = Firebase::project('app')->auth();
$otherAuth = Firebase::project('another-app')->auth();
For a multi-tenant SaaS where each customer is a Firebase project, do not sprinkle project('...') calls through your code. Resolve the correct project in a resolver/middleware and bind the resulting component (or a wrapper around it) into the container per request, scoped:
$this->app->scoped(Auth::class, fn () => Firebase::project($tenant->firebase_project)->auth());
scoped rather than singleton matters under Octane and queue workers, where a stale tenant binding leaking across requests is a real data-isolation bug.
If you use Firebase's multi-tenancy instead of multiple projects, configure it declaratively rather than passing tenant IDs around:
FIREBASE_AUTH_TENANT_ID=tenant-abc
or per project under firebase.projects.*.auth.tenant_id. Remember this makes all Auth calls tenant-scoped for that binding — you cannot mix tenant and non-tenant Auth operations on the same resolved instance, so if you need both, keep a second, non-tenant project entry.
The most common production pattern is a middleware that verifies the Firebase bearer token and logs the user in locally, rather than trusting the token downstream:
final class FirebaseAuthMiddleware
{
public function __construct(private Auth $auth) {}
public function handle($request, Closure $next)
{
try {
$token = $this->auth->verifyIdToken($request->bearerToken());
} catch (\Throwable $e) {
return response()->json(['error' => 'unauthenticated'], 401);
}
$request->attributes->set('firebase_uid', $token->claims()->get('sub'));
return $next($request);
}
}
Keep the mapping from firebase_uid → local users row in your own layer. Do not make Firebase Authentication your user table; Firebase is an identity provider, and you will want local relations, soft deletes, and roles that Firestore will not give you.
There is no model, no relations, no query builder in the Eloquent sense. The pragmatic pattern is a repository per collection that owns the array-shape and exposes typed DTOs:
final class OrderRepository
{
private \Google\Cloud\Firestore\CollectionReference $orders;
public function __construct(Firestore $firestore)
{
$this->orders = $firestore->database()->collection('orders');
}
public function find(string $id): ?array
{
return $this->orders->document($id)->snapshot()->exists()
? $this->orders->document($id)->snapshot()->data()
: null;
}
}
Treat Firestore as a secondary store (a sync target for mobile clients, a queue, a cache), and keep the relational source of truth in SQL. Trying to make Firestore the primary store in a Laravel app fights every tool in the framework.
Firestore writes, Storage uploads, and Messaging sends are all outbound HTTP with a default 30 s timeout (since SDK 6.7 — it used to be infinite). Anything user-facing that writes to Firebase belongs in a queued job with $tries and backoff set, so a Firebase outage degrades to a delayed job instead of a hung request. For Firebase Messaging specifically, batch when possible — sendAll() accepts up to 500 messages per call, which is markedly faster than looping send().
Several useful knobs live only in config/firebase.php and have no env shorthand:
http_client_options — timeouts, proxies, verify for custom CAs (common in corporate networks).logging.http_debug_log_channel — point it at a log channel to dump full request/response bodies when debugging. This replaces the old debug/FIREBASE_ENABLE_DEBUG flag removed in 4.0.0.firestore.default_database — override when you are not on (default).The ServiceProvider is not deferred (the 5.9.0 deferral was reverted in 5.9.1 because of auto-discovery ordering issues). That means the SDK factory is constructed during boot even for requests that never touch Firebase. If boot cost or credential discovery on CLI commands is a problem, bind your own wrapper lazily and only resolve the SDK component inside the method that needs it.
Auto-discovery will find the wrong credentials one day. The package falls back to inspecting environment variables and Google's well-known paths. On a workstation or CI runner with GOOGLE_APPLICATION_CREDENTIALS already set for something else, Firebase silently authenticates against the wrong project. Always set FIREBASE_CREDENTIALS explicitly and treat the auto-discovery path as a convenience for local dev only. There is no longer a flag to disable discovery (removed in 5.0.0) — setting the path explicitly is the mitigation.
private_key newlines are the classic failure. If you transpose the Service Account into a config array and put private_key in an env var, \n sequences must survive .env parsing, config caching, and the config file. Symptoms are openssl_sign(): supplied key param cannot be coerced or a bare auth failure. Options, in order of preference: point at the JSON file; base64-encode the whole JSON into one env var and decode at config time; only then hand-escape the key.
php artisan config:cache bakes credentials into bootstrap/cache/config.php. If credentials live in the config array, they are now a plaintext file on disk with different permissions than your .env, and key rotation requires re-caching. Prefer FIREBASE_CREDENTIALS pointing at a mounted file, and remember to re-run config:cache as part of any credential rotation deploy.
The package is not deferred, so credential errors surface at boot. A malformed or missing JSON will blow up during container boot rather than at first use — which is good for catching misconfiguration, but it also means a queue worker or artisan command that has nothing to do with Firebase can fail if the mount is missing. Container images therefore need the credentials file mounted even for commands like migrate.
SDK exceptions are not Laravel exceptions. They are under Kreait\Firebase\Exception\* and do not extend HttpException, so they render as 500s by default. Register a mapping in bootstrap/app.php (or App\Exceptions\Handler on older stacks) for the ones you actually expect — invalid ID token, not found, invalid argument, quota — and let the rest bubble to your error tracker. Catching \Throwable around verifyIdToken and returning 401 is the pragmatic default for auth middleware.
Deprecated facades are gone, not aliased. FirebaseAuth, FirebaseDatabase, FirebaseFirestore, FirebaseMessaging, FirebaseStorage, FirebaseRemoteConfig, FirebaseDynamicLinks were removed in 4.0.0. If you are following a blog post from 2021, it will class not found on you. Everything routes through the single Kreait\Laravel\Firebase\Facades\Firebase.
The namespace and the repo URL disagree — intentional. The project moved to the beste GitHub org in early 2026, but the Composer package is still kreait/laravel-firebase and the namespace is still Kreait\Laravel\Firebase. Do not "fix" the vendor namespace during an upgrade; do update any forked remote URLs.
Testing: mock the contract, not the SDK. The SDK's concrete classes are hard to fake. Bind a mock of Kreait\Firebase\Contract\Auth / Messaging / etc. in the container in your test case and assert on the message/claims you would have sent. This is another argument for constructor-injecting the contract into your own service rather than calling the facade statically — a static facade call is untestable without container gymnastics.
Timeout defaults changed under you. If you upgraded across SDK 6.7, calls that used to hang forever now fail at 30 s. Long-running Storage uploads or large batch messaging jobs may need http_client_options.timeout raised — but raise it per-call or per-service, not globally, or you will reintroduce the hung-worker problem.
Verify AppCheck server-side if you expose Firebase-backed endpoints to clients. The package exposes the AppCheck component (5.2.0), which lets you validate App Check tokens on your own Laravel routes. It is cheap to add and is the difference between an endpoint that only your real app can call and one that anyone with the project ID can hammer.
Watch dependency drift between the package and the SDK. The package is a thin bridge; the SDK moves faster. Constrain both, and read the SDK's changelog — not just this package's — before upgrading, because the breaking API changes will arrive through the SDK.
How can I help you explore Laravel packages today?