Skip to content

Cloud Functions

Firenook runs your Cloud Functions code in its own runtime, one Node worker per codebase, and delivers HTTP requests and events from the other emulators to it. Read this page to see which triggers it delivers, how it loads and reloads your code, and how to call any function by hand.

Functions start when firebase.json has a functions section (or extensions, which run on the same runtime):

firebase.json
{
"functions": {
"source": "functions",
"codebase": "default",
"ignore": ["node_modules", ".git", "firebase-debug.log", "*.local"]
},
"emulators": {
"functions": { "port": 5001 }
}
}

functions can also be an array of codebases; each one gets its own worker. Firenook finds your functions through the firebase-functions SDK already in the codebase’s node_modules, as the official CLI does, so install your dependencies first (cd functions && npm install). Nothing else is needed: no firebase-tools and no Java.

Only JavaScript and TypeScript compiled to JavaScript run; Python and Dart functions do not. For TypeScript, --build compiles your code before it loads and again on every save (TypeScript functions).

Client apps reach callable functions with connectFunctionsEmulator(functions, '127.0.0.1', 5001), and HTTP functions at http://127.0.0.1:5001/<project>/<region>/<name>.

Your functions run on the Node that runs Firenook, as the official emulator runs them on its own Node. Firenook is tested on Node 22, 24 and 26:

  • On Node 22, 24 or 26 the emulators start.

  • On an older Node, emulators:start and emulators:exec stop at once and say how to switch:

    Firenook: the emulators need Node 22, 24 or 26, since your Functions run on it; this is Node 20.20.2. Switch to Node 24 (with nvm: nvm install 24) and run it again
  • On an odd or a newer major (23, 25, 27 and up), the emulators start with a warning, since nobody has tested that Node.

A codebase says which Node it deploys on with engines.node in its package.json, or runtime (such as nodejs22) on its entry in firebase.json, which comes first. When the Node running the emulators is another one, the start says so, as the official emulator does, and goes on:

firenook functions: functions/package.json asks for Node 22 in engines.node, but these functions run on Node 24.20.0, the Node running the emulators; start the emulators with Node 22 to run them on the Node they ask for (with nvm: nvm use 22)

In a terminal it is one of the start-up warnings, which w lists. A reload that leaves the codebase asking for the same Node does not repeat it.

Trigger Delivered
HTTP (onRequest), callable (onCall) and streaming callable Yes
Firestore (onDocumentCreated, …, and first-generation document.*) Yes, when Firestore runs
Storage (onObjectFinalized, onObjectDeleted, onObjectMetadataUpdated) Yes, when Storage runs
Auth blocking (beforeUserCreated, beforeUserSignedIn) and user.create, user.delete Yes, when Auth runs
Pub/Sub (onMessagePublished) Yes, when Pub/Sub runs
Scheduled (onSchedule) Yes, on their schedule (Scheduled functions)
Task queues (onTaskDispatched) Yes (Cloud Tasks)
Eventarc custom events (onCustomEventPublished) Yes (Eventarc)
Realtime Database and Firebase Alerts No: Firenook emulates neither

A function whose service the suite does not run, or does not emulate, is loaded and listed but receives no event. The first load says why, with the official emulator’s line, as a warning on the terminal screen and in the plain log. Started with --only functions,firestore:

firenook functions[us-central1-onUpload]: function ignored because the storage emulator does not exist or is not running.
firenook functions[us-central1-nightlyReport]: function ignored because the pubsub emulator does not exist or is not running.

Firenook warns this way for Firestore, Pub/Sub (and so schedules), Storage, Auth and the Realtime Database; the official emulator warns for Firestore, Pub/Sub and the Realtime Database only. The console and functions:invoke still call such a function directly.

With the starter helloWorld and a callable addMessage, both answer on the Functions port:

Terminal window
$ curl http://127.0.0.1:5001/demo-shop/us-central1/helloWorld
Hello from Firebase!
$ curl -X POST -H 'Content-Type: application/json' \
--data '{"data":{"text":"hi"}}' \
http://127.0.0.1:5001/demo-shop/us-central1/addMessage
{"result":{"echo":"hi","uid":null}}

An event function runs when its service changes. This one writes an audit document for every new order:

functions/index.js
const { onDocumentCreated } = require('firebase-functions/firestore');
const logger = require('firebase-functions/logger');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
initializeApp();
exports.onOrderCreated = onDocumentCreated('orders/{orderId}', async (event) => {
const order = event.data.data();
logger.info('order created', { orderId: event.params.orderId, total: order.total });
await getFirestore().doc(`audit/${event.params.orderId}`).set({ seen: true, total: order.total });
});

Writing orders/o42 runs it, and the log shows the call:

firenook functions: Beginning execution of "us-central1-onOrderCreated"
firenook functions: {"orderId":"o42","total":42,"severity":"INFO","message":"order created"}
firenook functions: Finished "us-central1-onOrderCreated" in 2.9ms

Inside a worker, the Admin SDK finds every emulator by itself: Firenook sets FIRESTORE_EMULATOR_HOST, FIREBASE_AUTH_EMULATOR_HOST, FIREBASE_STORAGE_EMULATOR_HOST, PUBSUB_EMULATOR_HOST, CLOUD_TASKS_EMULATOR_HOST and CLOUD_EVENTARC_EMULATOR_HOST. The worker holds demo credentials, never yours: a call that asks for an access token first, such as an Eventarc publish or a task enqueue, gets the emulators’ token without leaving the machine. Environment variables and secrets covers your own .env and .secret.local files, and params with their defaults.

Firenook watches each codebase and reloads it when you save. The other emulators keep running, and so does your data:

firenook functions: Loaded functions definitions from source: helloWorld, addMessage, onOrderCreated, onUpload, nightlyReport, sendReceipt, onShipped.
firenook functions[us-central1-helloWorld]: http function initialized (http://127.0.0.1:5001/demo-shop/us-central1/helloWorld).
firenook functions[us-central1-onShipped]: eventarc function initialized.
firenook functions routing refreshed: 7 registered functions

Trimmed: every function gets its initialized line. Functions that do not load leave every emulator running, the version loaded before keeps answering, and the next save loads them again; if a save seems to change nothing, read the terminal screen or the console. In a terminal, r reloads by hand.

In 0.2.0, a function you remove, rename or move to another region (a region param changed in .env, for one) keeps its old route until the emulators restart. A call to the old route answers 500, and the log says:

firenook functions[us-central1-params]: the source no longer exports this function (record kept from an earlier load)

Restart the emulators after you change a function’s name or region. Debugging and reload on save covers --inspect-functions and attaching a debugger.

firenook functions:invoke calls a function on the running suite. Without --event-data it sends an HTTP request, or a callable body with --data:

Terminal window
$ npx firenook functions:invoke addMessage --data '{"text":"hi"}'
200 OK http://127.0.0.1:5001/demo-shop/us-central1/addMessage
{"result":{"echo":"hi","uid":null}}

With --event-data it builds the event the official functions:shell would and delivers it, for any background trigger:

Terminal window
$ npx firenook functions:invoke onOrderCreated --resource orders/o99 \
--event-data '{"status":"paid","total":99}'
Firenook: firestore event google.cloud.firestore.document.v1.created for onOrderCreated (us-central1, gcfv2) via trigger key us-central1-onOrderCreated-0
200 OK http://127.0.0.1:5001/functions/projects/demo-shop/triggers/us-central1-onOrderCreated-0
{"status":"acknowledged"}

A Pub/Sub event is published through the Pub/Sub emulator, so every subscriber sees it, and a task goes through the Cloud Tasks emulator, so retries and rate limits apply. --auth fills the event’s auth, --params fills path wildcards, --event-type sets an Eventarc type, and --json prints the whole record. CLI commands lists every flag. functions:shell itself does not exist: Firenook never starts a second Functions runtime.

Firenook keeps each invocation with the request or event that caused it, its answer, its timing and the log lines it printed: the last 2,000 calls, at most 64 MiB. The console’s Functions page shows them, and your coding agent reads them through MCP, with the error and stack trace of a call that failed.

  • No network sandbox. A demo- project never reaches a real Firebase project, and workers get demo credentials, never yours, but your code can still call any external API it is written to call.
  • No Secret Manager. Put secret values in .secret.local.
  • No Python or Dart runtimes.
  • Headers named x-firenook-* are the engine’s own and are removed before your function sees a request.