Firebase Cloud Function Triggers for Firestore: a beginner-friendly guide
If you are building with Firebase, there is a moment when frontend code alone starts to feel too fragile. You want something that reacts automatically when data changes: send a welcome message when a user profile is created, update counters when a post changes, or clean up related data when a document is deleted.
That is exactly where Firestore triggers come in. A Cloud Function can listen to changes in Cloud Firestore and run backend code for you, without you having to manually call that code from the client. Firebase’s Firestore triggers support create, update, delete, and “any write” events, and they work by attaching a function to a document path.
What is a trigger?
Think of a trigger like a smart doorbell for your database.
When something happens in Firestore, Firebase notices it and runs your function.
Tiny illustration
User app writes data
|
v
Cloud Firestore document changes
|
v
Firestore trigger fires
|
v
Your Cloud Function runs
|
v
Sends email / updates another doc / logs activity / etc.In the Firestore trigger flow described by Firebase, the function waits for changes on a document, runs when the event happens, and receives event data containing the document snapshot. For write or update-related events, you can inspect the data from before and after the change.

The 4 trigger types you should know first
For beginners, these are the important ones:
- onDocumentCreated → runs when a document is written for the first time
- onDocumentUpdated → runs when an existing document changes
- onDocumentDeleted → runs when a document is deleted
- onDocumentWritten → runs for create, update, or delete events
Mental model
Create a new doc -> onDocumentCreated
Edit an existing doc-> onDocumentUpdated
Delete a doc -> onDocumentDeleted
Any of the above -> onDocumentWrittenA useful detail for beginners: Firestore triggers react to document changes, not field-level subscriptions. Also, a write that does not actually change the data is a no-op and does not fire an update or write trigger.
A simple first example
Here is the smallest useful example in Node.js using the v2 Firestore trigger API style shown in the Firebase docs:
const { onDocumentCreated }=require("firebase-functions/v2/firestore");
exports.welcome_new_user=onDocumentCreated("users/{userId}", (event) => {
constsnapshot=event.data;
if (!snapshot) {
console.log("No data found for this event.");
return;
}
constuser_data=snapshot.data();
console.log("New user created:",event.params.userId,user_data);
});This pattern matches Firebase’s documented structure: import the trigger, provide a document path, and read the document from event.data. Wildcard values like {userId} become available under event.params.
What is happening here?
- "users/{userId}" means “listen to every document inside the users collection”
- event.params.userId gives you the actual document ID
- event.data is the snapshot of the created document
- snapshot.data() returns the document fields as a JavaScript object
Understanding document paths
A Firestore trigger always listens to a document path.
Valid examples
users/marie
users/{userId}
users/{userId}/messages/{messageId}Invalid example
users/{userId}/{messageCollectionId}Why is that invalid? Because a trigger must always point to a document, not a collection. Firebase explicitly notes that your trigger path must end at a document, even when using wildcards.
Illustration
users/{userId} ✅ document
users/{userId}/messages/{messageId} ✅ document
users/{userId}/messages ❌ collectionWildcards: the beginner superpower
Wildcards let one function react to many documents.
For example:
const { onDocumentWritten }=require("firebase-functions/v2/firestore");
exports.watch_all_users=onDocumentWritten("users/{userId}", (event) => {
console.log("Changed user:",event.params.userId);
});If /users/marie changes, then event.params.userId becomes "marie". Firebase also notes that if a subcollection document changes, the parent wildcard does not automatically match that deeper path unless your trigger path explicitly includes it.
Illustration
Trigger path: users/{userId}
Matches:
- users/alice
- users/bob
Does NOT match:
- users/alice/messages/msg1If you want deeper matching, you must write the deeper path yourself:
const { onDocumentWritten }=require("firebase-functions/v2/firestore");
exports.watch_user_messages=onDocumentWritten(
"users/{userId}/{messageCollectionId}/{messageId}",
(event) => {
console.log("User:",event.params.userId);
console.log("Collection:",event.params.messageCollectionId);
console.log("Message ID:",event.params.messageId);
}
);That multi-wildcard pattern is the same kind of structure Firebase documents for nested paths.
Example 1: trigger when a new document is created
This is the most beginner-friendly trigger.
Imagine a user signs up and your app creates a profile document in users/{userId}. You want to automatically stamp the account with metadata.
const { onDocumentCreated }=require("firebase-functions/v2/firestore");
const { initializeApp }=require("firebase-admin/app");
const { getFirestore, FieldValue }=require("firebase-admin/firestore");
initializeApp();
constdb=getFirestore();
exports.add_profile_metadata=onDocumentCreated("users/{userId}",async (event) => {
constsnapshot=event.data;
if (!snapshot) {
return;
}
awaitdb.collection("user_logs").add({
user_id:event.params.userId,
action:"profile_created",
created_at:FieldValue.serverTimestamp(),
});
});Why this is useful
This keeps backend responsibilities on the server side. Your client app creates the user profile, and the backend reacts automatically. This fits Firebase’s model where the function responds to the Firestore event and can then use the Admin SDK to write elsewhere.
Example 2: trigger when a document is updated
This one is perfect for comparing old and new values.
Firebase exposes event.data.before and event.data.after so you can inspect what changed in an update event.
const { onDocumentUpdated }=require("firebase-functions/v2/firestore");
exports.detect_name_change=onDocumentUpdated("users/{userId}", (event) => {
constnew_value=event.data.after.data();
constold_value=event.data.before.data();
if (new_value.name!==old_value.name) {
console.log(`User${event.params.userId} changed name from${old_value.name} to${new_value.name}`);
}
});Illustration
Before update: { name: "Sara", age: 20 }
After update: { name: "Sarah", age: 20 }
Result:
- Trigger fires
- You compare before vs after
- You react only to the field you care aboutThis is one of the most practical patterns in real projects: listen broadly, then add your own logic to decide whether a specific field matters. That is important because Firebase does not let you attach triggers directly to individual fields.
Example 3: trigger when a document is deleted
Deletion triggers are great for cleanup logic.
const { onDocumentDeleted }=require("firebase-functions/v2/firestore");
exports.log_deleted_user=onDocumentDeleted("users/{userId}", (event) => {
constdeleted_snapshot=event.data;
if (!deleted_snapshot) {
return;
}
constdeleted_data=deleted_snapshot.data();
console.log("Deleted user:",event.params.userId,deleted_data);
});This matches Firebase’s delete trigger model, where the deleted document snapshot is available in event.data.
Example 4: listen to any write with onDocumentWritten
Sometimes you do not care whether the change was a create, update, or delete. You just want one function that catches everything.
const { onDocumentWritten }=require("firebase-functions/v2/firestore");
exports.audit_user_changes=onDocumentWritten("users/{userId}", (event) => {
constbefore_snapshot=event.data.before;
constafter_snapshot=event.data.after;
if (!before_snapshot.exists&&after_snapshot.exists) {
console.log("Document created");
return;
}
if (before_snapshot.exists&&!after_snapshot.exists) {
console.log("Document deleted");
return;
}
if (before_snapshot.exists&&after_snapshot.exists) {
console.log("Document updated");
}
});Firebase describes onDocumentWritten as the catch-all trigger for create, update, and delete events.
The most important concept: before and after
For updates and writes, Firebase gives you two snapshots:
- before = the document before the change
- after = the document after the change
Illustration
event.data.before -> old version
event.data.after -> new versionThat makes tasks like these easy:
- detect field changes
- count edits
- prevent unnecessary processing
- sync old and new values elsewhere
Writing back to Firestore from a trigger
Yes, your function can update Firestore. Firebase notes that the document reference and Admin SDK let you write data such as with set() or update().
Here is a classic example: count how many times a user changed their name.
const { onDocumentUpdated }=require("firebase-functions/v2/firestore");
const { initializeApp }=require("firebase-admin/app");
const { getFirestore, FieldValue }=require("firebase-admin/firestore");
initializeApp();
constdb=getFirestore();
exports.count_name_changes=onDocumentUpdated("users/{userId}",async (event) => {
constnew_data=event.data.after.data();
constold_data=event.data.before.data();
if (new_data.name===old_data.name) {
return;
}
constuser_ref=db.collection("users").doc(event.params.userId);
awaituser_ref.update({
name_change_count:FieldValue.increment(1),
});
});Why the if check matters
Because writing to the same document that triggered the function can create an infinite loop if you are not careful. Firebase explicitly warns about this and recommends making sure the function exits when no relevant change is needed.
Illustration of the danger
Document updates
-> trigger runs
-> function updates same document
-> trigger runs again
-> function updates same document again
-> loop...Safe pattern
Check if the important field really changed
|
+-- no -> exit
|
+-- yes -> write updateA beginner-friendly real use case
Let’s say you are building a blogging app.
When a post is published, you want to automatically create a summary record in another collection.
Firestore data
posts/{postId}Example post:
{
"title":"My first Firebase article",
"status":"published",
"author":"Lina"
}Trigger
const { onDocumentUpdated }=require("firebase-functions/v2/firestore");
const { initializeApp }=require("firebase-admin/app");
const { getFirestore, FieldValue }=require("firebase-admin/firestore");
initializeApp();
constdb=getFirestore();
exports.create_publish_log=onDocumentUpdated("posts/{postId}",async (event) => {
constbefore_data=event.data.before.data();
constafter_data=event.data.after.data();
constwas_published=before_data.status==="published";
constis_published=after_data.status==="published";
if (was_published||!is_published) {
return;
}
awaitdb.collection("post_publication_logs").add({
post_id:event.params.postId,
title:after_data.title,
author:after_data.author,
published_at:FieldValue.serverTimestamp(),
});
});Why this example is good for beginners
It teaches three essential habits at once:
- listen to the right document path
- compare before and after
- exit early to avoid unnecessary writes
Those habits align with Firebase’s documented event model and safety notes.
Authentication context triggers
Firebase also provides versions of these triggers that include authentication context, such as:
- onDocumentCreatedWithAuthContext
- onDocumentUpdatedWithAuthContext
- onDocumentDeletedWithAuthContext
- onDocumentWrittenWithAuthContext
These are useful when you want extra information about the principal that triggered the event.
Example shape:
const { onDocumentWrittenWithAuthContext }=require("firebase-functions/v2/firestore");
exports.track_writer=onDocumentWrittenWithAuthContext("users/{userId}", (event) => {
const { authType, authId }=event;
console.log("Auth type:",authType);
console.log("Auth ID:",authId);
});Firebase documents authType and authId as part of the event when using the auth-context trigger variants.
Limitations and gotchas every beginner should know
This part saves a lot of debugging pain.
1) Triggers are not field listeners
You cannot attach a trigger to only one field like users/{userId}.name. Firestore triggers work at the document level. You listen to the document, then inspect the fields yourself.
2) No-op writes do not fire update/write events
If the data stays the same, the trigger does not fire.
3) Event ordering is not guaranteed
If documents change very quickly, triggers may be delivered in an unexpected order.
4) Events are delivered at least once
A single event may cause multiple function invocations, so your code should be idempotent whenever possible. In practice, this means your function should be safe even if it runs more than once.
5) Same-project rule
Firebase notes that Cloud Functions and the Firestore trigger must be in the same project for this setup.
Beginner checklist before you write a trigger
Use this as your mini recipe:
1. Decide which collection/document pattern you want to watch
2. Pick the right trigger type
3. Read event.data, or event.data.before / event.data.after
4. Use event.params for wildcard IDs
5. Exit early if nothing important changed
6. Be careful when writing back to the same document
7. Assume duplicate delivery can happenEvery item there is grounded in the behavior Firebase documents for Firestore event triggers.
Final takeaway
Firestore triggers are one of the cleanest ways to add backend automation to a Firebase app.
For a beginner, the best way to think about them is this:
“When a Firestore document changes, my backend can react automatically.”
Start with these three ideas:
- a trigger listens to a document path
- the event gives you document snapshots
- your code decides what to do next
Once those click, a lot of useful backend workflows become simple: logs, counters, cleanup, notifications, sync jobs, and publishing flows. Firebase’s docs make clear that the core building blocks are create, update, delete, and write triggers, with wildcard paths and snapshot-based event data.
If you want, I can turn this into a polished Markdown blog post with a title, intro hook, callout boxes, and a cleaner publish-ready layout.
