firebase-admin
firebase-admin is Google's privileged server SDK for Firebase. It lets trusted Node.js code manage users, verify authentication and App Check tokens, read and write Firestore or Realtime Database data, send Firebase Cloud Messaging notifications, and access Cloud Storage without pretending to be an end user. It belongs only on servers, workers, and serverless functions you control because its credentials can bypass the client-side security boundary.
Use firebase-admin when Firebase is already the backend and trusted Node code needs official privileged access. Do not install it in client code, on Node 20, or as a casual database wrapper when portability and a narrow dependency matter more than Firebase integration.
Use it if
- Your Node.js backend must verify Firebase ID tokens or manage Firebase Authentication users and custom claims
- A trusted worker or API needs administrative access to Firestore, Realtime Database, Cloud Storage, Remote Config, or Firebase Cloud Messaging
- You run on Google Cloud and want Application Default Credentials instead of distributing service-account JSON keys
- You need one officially maintained server SDK spanning several Firebase products under the same project identity
- Any of this code will run in a browser, mobile app, Electron renderer, or other user-controlled process: Admin credentials grant privileged access and a leaked key can expose the whole Firebase project; use the firebase client SDK there
- You need a self-hosted backend or an easy exit from Google services: this API is tightly coupled to Firebase Auth, databases, messaging, IAM, and project configuration, so Supabase, Appwrite, or Parse is a better starting point
- Your production runtime is Node 20 or older: v14 requires Node 22 or newer, and staying on v13 means choosing an older major instead of receiving the current API line
- You only need one small Google API client: installing this broad SDK plus its optional Firestore and Storage packages is unnecessary surface area when a focused Google Cloud client fits
- You are upgrading old namespace-style code or legacy FCM code and expect a drop-in major update: v14 removed legacy namespace support, Instance ID, and legacy messaging types, while v14.1 began deprecating registration-token targets in favor of Firebase Installation IDs
Setup reality
The npm install has no peer dependencies or native compilation, but that is the easy part. Version 14 needs Node 22+, and every real request needs Google credentials plus the right IAM roles and Firebase project settings. Application Default Credentials are cleanest on Google Cloud; elsewhere you must secure a service-account key, preserve private-key newlines in environment variables, and never ship it to a client. Realtime Database needs its URL, Storage needs the correct bucket, and local testing needs each service's emulator environment variable rather than one universal switch.
Patterns
Initialize with Application Default Credentialsinitialize-with-adc
import { applicationDefault, initializeApp } from 'firebase-admin/app';
export const app = initializeApp({
credential: applicationDefault(),
});ADC is automatic on supported Google Cloud runtimes. Elsewhere, set GOOGLE_APPLICATION_CREDENTIALS to a protected service-account JSON file or configure another ADC source.
Initialize from service-account environment variablesinitialize-service-account
import { cert, initializeApp } from 'firebase-admin/app';
const app = initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
});Escaped newlines are the usual deployment failure. Keep the private key in a secret manager, never in source control or any client bundle.
Reuse the default app during hot reloadavoid-duplicate-apps
import { applicationDefault, getApp, initializeApp } from 'firebase-admin/app';
let app;
try {
app = getApp();
} catch {
app = initializeApp({ credential: applicationDefault() });
}
export { app };Calling initializeApp again with an explicit Credential is not idempotent and throws. This matters in development servers and test runners that reload modules.
Verify a Firebase ID token on an API requestverify-id-token
import { getAuth } from 'firebase-admin/auth';
const header = req.headers.authorization ?? '';
const match = header.match(/^Bearer (.+)$/);
if (!match) throw new Error('missing bearer token');
const decoded = await getAuth().verifyIdToken(match[1], true);
console.log(decoded.uid);The true argument checks revocation and disabled-user state with an extra backend request. Without it, signature and claims are checked but revocation is not.
Create, fetch, and update an Auth usermanage-auth-user
import { getAuth } from 'firebase-admin/auth';
const auth = getAuth();
const created = await auth.createUser({
email: 'ada@example.com',
displayName: 'Ada',
emailVerified: false,
});
const user = await auth.getUser(created.uid);
await auth.updateUser(user.uid, { disabled: true });Auth operations need sufficient IAM permissions. createUser rejects duplicate email addresses, so handle auth/email-already-exists as an expected application error.
Assign authorization claimsset-custom-claims
import { getAuth } from 'firebase-admin/auth';
await getAuth().setCustomUserClaims(uid, {
role: 'editor',
paid: true,
});This replaces the user's existing custom claims rather than merging them. New claims appear after the client obtains a fresh ID token; they are not a database for profile data.
Exchange a fresh ID token for a session cookiecreate-session-cookie
import { getAuth } from 'firebase-admin/auth';
const decoded = await getAuth().verifyIdToken(idToken);
if (Date.now() / 1000 - decoded.auth_time > 5 * 60) {
throw new Error('recent sign-in required');
}
const expiresIn = 5 * 24 * 60 * 60 * 1000;
const sessionCookie = await getAuth().createSessionCookie(idToken, { expiresIn });Validate a CSRF token before this exchange, then send the result as an HttpOnly, Secure cookie. Verify the session cookie on later requests with verifySessionCookie.
Write and read a Firestore documentfirestore-read-write
import { FieldValue, getFirestore } from 'firebase-admin/firestore';
const db = getFirestore();
const ref = db.collection('users').doc(uid);
await ref.set({
displayName: 'Ada',
updatedAt: FieldValue.serverTimestamp(),
}, { merge: true });
const snapshot = await ref.get();
const user = snapshot.exists ? snapshot.data() : null;Admin Firestore access uses IAM and bypasses Firebase client Security Rules. Validate request data in your server before writing it.
Update Firestore data in a transactionfirestore-transaction
import { getFirestore } from 'firebase-admin/firestore';
const db = getFirestore();
const ref = db.collection('counters').doc('orders');
const next = await db.runTransaction(async (tx) => {
const snapshot = await tx.get(ref);
const value = (snapshot.data()?.value ?? 0) + 1;
tx.set(ref, { value });
return value;
});Firestore may retry the callback after contention. Keep it deterministic and do not send email, charge cards, or perform other outside side effects inside it.
Apply an atomic Realtime Database updateupdate-realtime-database
import { getDatabase, ServerValue } from 'firebase-admin/database';
const db = getDatabase();
await db.ref().update({
[`users/${uid}/displayName`]: 'Ada',
[`audit/${uid}/updatedAt`]: ServerValue.TIMESTAMP,
});Configure databaseURL during app initialization or use getDatabaseWithUrl. Admin access is privileged by default, so Security Rules do not protect these writes.
Send an FCM notification to an installationsend-push-notification
import { getMessaging } from 'firebase-admin/messaging';
const messageId = await getMessaging().send({
fid: installationId,
notification: {
title: 'Order shipped',
body: 'Your package is on the way',
},
data: { orderId },
});In v14.1+, fid is the current target field. Registration-token targets still exist in v14 but are deprecated, so do not start new code on token.
Verify and consume an App Check tokenverify-app-check-token
import { getAppCheck } from 'firebase-admin/app-check';
const result = await getAppCheck().verifyToken(appCheckToken, {
consume: true,
});
if (result.alreadyConsumed) {
throw new Error('replayed App Check token');
}
console.log(result.appId);Replay protection makes an additional backend call and can increase attestation usage. Reserve consume: true for low-volume, security-sensitive, or expensive operations.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| firebase | npm | Browser, mobile-web, or other untrusted code that must obey Firebase Security Rules instead of holding admin privileges |
| @supabase/supabase-js | npm | A Postgres-based hosted backend with an open-source stack and a more portable data model fits better than Firebase |
| node-appwrite | npm | You want an Appwrite server SDK and the option to self-host the backend |
| parse-server | npm | You want to run a Parse-compatible backend yourself and accept operating the server as part of the tradeoff |