Firebase Cloud Functions v2 represents a massive leap forward for the serverless Firebase ecosystem. Built entirely on top of Google Cloud Run and Eventarc, v2 introduces game-changing capabilities such as concurrent request handling (up to 1,000 requests per container instance), regional endpoints, custom secrets management, and robust event-driven triggers.
However, migrating from Cloud Functions v1 to v2 is not a simple package update. The API surface has been completely redesigned.
This article details the real-world lessons, syntax shifts, and architectural patterns of migrating Firestore Event Triggers from v1 to v2, using actual code patterns implemented in the Tetangga App codebase.
ποΈ The Architectural Evolution: v1 vs v2
Before diving into the code, it is important to understand the underlying infrastructure shift:
- In v1: Functions were deployed to Google Cloud Functions (1st Gen). Each instance could only process one request at a time. If 10 triggers fired simultaneously, Firebase had to spin up 10 separate cold containers, resulting in high latency spikes and massive CPU billing.
- In v2: Functions are built on Cloud Run and Eventarc. A single container instance can handle up to 1,000 concurrent requests. This virtually eliminates cold starts for active applications and slashes CPU resource costs by up to 90%.
π Syntax Breakdown: Moving from v1 to v2
Letβs look at the absolute changes when writing a Firestore Document Creation listener.
The Legacy v1 Code (Deprecated)
import * as functions from "firebase-functions";
export const onTopicCreatedV1 = functions.firestore
.document("topics/{topicId}")
.onCreate(async (snap, context) => {
const topic = snap.data();
const topicId = context.params.topicId; // v1 uses 'context' for wildcards
console.log(`New topic created: ${topicId}`);
});
The Modern v2 Code
import { onDocumentCreated } from "firebase-functions/v2/firestore";
export const onTopicCreatedV2 = onDocumentCreated(
{
document: "topics/{topicId}",
database: "production-db", // v2 allows specifying multi-databases!
region: "asia-southeast2", // Configure region directly inside the options object
},
async (event) => {
const snap = event.data; // v2 uses a single 'event' object
if (!snap) return;
const topic = snap.data();
const topicId = event.params.topicId; // Wildcard parameters are now inside 'event.params'
console.log(`New topic created: ${topicId}`);
}
);
π Key Real-World Lessons from the Tetangga Codebase
During the migration of the Tetangga neighborhood portal triggers (such as onTopicCreatedV2 and onTopicReplyCreatedV2), several critical lessons were learned.
1. The Single Parameter Paradigm Shift
In v1, your callbacks always took two arguments: (snapshot, context). In v2, your callbacks receive a single unified event object of type FirestoreEvent.
This leads to the following key mappings:
- The Document Snapshot: Accessed via
event.data. ForonDocumentCreatedandonDocumentDeleted, this is a singleQueryDocumentSnapshot. ForonDocumentWrittenandonDocumentUpdated,event.datacontains{ before, after }snapshots. - The Wildcards (Context): Path parameters (like
{topicId}) are accessed viaevent.params.topicIdinstead of the legacycontext.params.topicId. - Metadata: Event ID, timestamp, and target resource paths are accessed directly via properties like
event.id,event.time, andevent.subject.
2. Multi-Database Support is Now Native
If your project utilizes Firebase's multi-database Firestore feature, v1 made targeting non-default databases highly complex. In v2, you can specify the target database directly inside the options configuration block:
export const onTopicCreatedV2 = onDocumentCreated(
{ document: 'topics/{topicId}', database: 'aldianapps' },
async (event) => {
// This listener only targets the 'aldianapps' Firestore database!
}
);
3. Exposing Secrets Safely
If your triggers communicate with external APIs (like sending FCM notifications, or sending Twilio WhatsApp alerts like in Tetangga), you must not expose secret tokens in your code. v2 allows you to bind Google Cloud Secrets Manager secrets directly to your triggers:
import { onDocumentCreated } from "firebase-functions/v2/firestore";
export const onNotificationTrigger = onDocumentCreated(
{
document: "notifications/{id}",
secrets: ["TWILIO_AUTH_TOKEN", "FCM_SERVER_KEY"] // Auto-bind secrets safely
},
async (event) => {
const token = process.env.TWILIO_AUTH_TOKEN; // Accessible securely in runtime
// Send alert safely...
}
);
4. Resolving the AppRegistryNotReady Initialization Order
When importing v2 triggers alongside Twilio, sendgrid, or custom database configurations, ensure you initialize your Firebase Admin SDK first, before exporting any triggers or importing services that depend on firestore instances:
import * as admin from "firebase-admin";
import { getFirestore } from "firebase-admin/firestore";
// Initialize Firebase Admin first
const app = admin.initializeApp();
const db = getFirestore(app);
// NOW import / export trigger scripts
import { onDocumentCreated } from "firebase-functions/v2/firestore";
π Step 5: Summary of Migration Gains
By refactoring your Firestore database triggers to Firebase Functions v2, you achieve:
- Dramatic Cost Reductions: Up to 10x less container instantiation overhead via Cloud Run request concurrency.
- Improved Maintainability: Strict TypeScript types for the
eventpayload and wildcard parameters. - Native Multi-Database Listeners: Effortless scaling across Firestore shards.
- State-of-the-Art Secrets Security: Zero-leak environment configurations bound directly to Cloud Secrets Manager.