NukeBase

Database Triggers

Run server code in response to database changes using addDbTrigger:

Database trigger example
// 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 $userId to 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)

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.path is ["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. at context.path, not the originating write path). May be undefined for remove operations or when the path no longer exists.
  • context.dataBefore — A synthesized partial snapshot, not the full prior subtree. It is built from the leaves of the new value (so it captures prior values at the locations the write actually touched), then any unchanged sibling fields are filled in from the post-write state. Do not use !context.dataBefore to detect a freshly-created resource — for a brand-new set, dataBefore is populated with the new values rather than being null.

Detecting "is this new?": Because context.dataBefore mirrors the leaves of the new value and falls back to the post-write state for unchanged keys, it is rarely null in practice — even for first-time creation. If you need a one-time-init pattern, register the trigger on the "set" action and check for the absence of a marker field on context.dataAfter (a field your trigger itself sets), rather than testing dataBefore.

Deletions are not visible in dataBefore: Because dataBefore is keyed by the leaves of the new value, fields that existed before the write but are absent from the new value will not appear in dataBefore at all. For example, replacing {item:"y", price:5} with set(..., {item:"x"}) yields dataBefore = {item:"y"} — the removed price field is not surfaced.

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

Processing new orders
// React to orders being set (created OR fully replaced).
// Because context.dataBefore is unreliable for "is this new?" (see warnings
// above), use a marker field on dataAfter to ensure one-time initialization.
// Bonus: registering on "set" (not "update") prevents this from refiring
// when the trigger's own update() call below runs.
addDbTrigger("set", ["orders", "$orderId"], function(context) {
  if (context.dataAfter && !context.dataAfter.processingStart) {
    const orderId = context.path[1];  // wildcard segment, filled in
    update(context.path, {
      status: "processing",
      processingStart: Date.now()
    });
  }
});