Database Triggers
Run server code in response to database changes using addDbTrigger:
// Create a trigger for when a request is updated
addDbTrigger("update", ["requests", "$requestId"], function(context) {
// The context object contains all relevant information about the change
const beforeNotes = context.dataBefore?.notes;
const afterNotes = context.dataAfter?.notes;
// Replace "pizza" with pizza emoji
const newNotes = afterNotes.replaceAll("pizza", "🍕");
// Avoid infinite loop by checking if we already replaced
if (newNotes === afterNotes) {
return;
}
// Update the data with our modified version
update(context.path, { notes: newNotes });
});
Key components of database triggers:
addDbTrigger(eventType, pathArray, callbackFunction)- Path arrays use wildcards like
$userIdto match any value at that position
Event Types
"set"- Triggered when data is created or completely replaced"update"- Triggered when data is partially updated"remove"- Triggered when data is deleted"value"- Triggered for all changes (set, update, remove)
increment() fires as "update". There is no separate increment event — register on "update" (or "value") to see it.
Triggers run after the write is committed to memory, and their dataBefore snapshot is captured immediately before it — after the write and validate rules have passed, so a denied write costs no snapshot. For a { durable: true } write, they run after the disk flush completes, alongside the subscriber notifications — so a durable write's trigger never observes state that failed to persist (a failed flush rolls the write back and runs no triggers).
A throwing trigger is contained. Each database trigger runs in its own try/catch: a throw (or a rejected promise from an async trigger) is logged with the trigger's path and the remaining triggers for that write still run. The write itself stands — it was applied and acknowledged before triggers run — so the client sees one Success reply and the failure is visible only in the server log. If a trigger's work must succeed, handle and retry it inside the trigger. Connection triggers and file triggers are guarded the same way.
Path Patterns
Use an array path with wildcards to match specific data paths:
["users", "$userId"]- Matches any user path like ["users", "john"] or ["users", "alice"]["posts", "$postId", "comments", "$commentId"]- Matches any comment on any post
Context Object
Your callback function receives a context object containing:
context.path— The path matched by the trigger pattern, truncated to the trigger pattern's depth. If you register a trigger on["users", "$userId"]and a write happens at["users", "john", "email"],context.pathis["users", "john"]— not the deeper write path. Wildcard segments are filled in with the actual key from the write.context.dataAfter— The post-write state at the trigger's path (i.e. atcontext.path, not the originating write path). May beundefinedfor remove operations or when the path no longer exists.context.admin— The auth context of the caller who made the write, ornullwhen the write came from server-side code running as root. See "root" and the three states in a trigger.context.dataBefore— A deep copy of the value atcontext.pathas it was immediately before the write, orundefinedif nothing existed there. It is captured before the write is applied and cloned, so nothing that happens afterwards can alter it. Because it is the whole prior subtree at the trigger's path, fields the write removed are still present in it, andcontext.dataBefore === undefinedis a reliable test for "this path was just created".
Detecting "is this new?" and "what was removed?": both fall out of the snapshot. context.dataBefore === undefined means nothing existed at context.path before this write. Comparing the keys of dataBefore and dataAfter shows which fields a set replaced away: replacing {item:"y", price:5} with set(..., {item:"x"}) gives dataBefore = {item:"y", price:5} and dataAfter = {item:"x"}. The snapshot is taken at the trigger's depth — a trigger on ["orders", "$id"] sees the whole order before and after, even when the write touched one field of it.
A write above the trigger's path does not fire it. Matching is by prefix of the write path: a trigger on ["users", "$uid", "email"] fires for writes at or beneath users.<uid>.email, but not for set(["users", "john"], {...}) even though that replaces the email. Register at the shallowest path whose changes you care about — bubble-up applies to subscriptions, not to triggers.
Important: When modifying data within a trigger that affects the same path you're watching, always implement safeguards to prevent infinite loops, as shown in the example.
Complete Example: Order Processing
// React to orders being created. dataBefore is undefined when nothing existed
// at the order's path before this write — that is the "is this new?" test.
// Registering on "set" (not "update") also keeps this from refiring when the
// trigger's own update() call below runs.
addDbTrigger("set", ["orders", "$orderId"], function(context) {
if (context.dataBefore === undefined && context.dataAfter) {
const orderId = context.path[1]; // wildcard segment, filled in
update(context.path, {
status: "processing",
processingStart: Date.now()
});
}
});