# Firenook docs > Firenook is a faster drop-in replacement for the Firebase Local Emulator Suite. It reads firebase.json, .firebaserc, rules and indexes the way the Firebase CLI does, serves Firestore, Authentication, Cloud Storage, Cloud Functions, Pub/Sub, Cloud Tasks, Eventarc, Extensions on the official ports, and imports and exports the official format. It is one npm package with a native engine for macOS, Linux and Windows: no Java, nothing else downloaded. It does not emulate Realtime Database, Hosting, App Hosting, Data Connect. Open source under the Apache License 2.0; 0.2.0 is its first release for public use. # Firenook docs > Run the Firebase emulators with Firenook. Install it, switch an existing project, connect your app, keep your data, and use the console and MCP. Firenook is a drop-in replacement for the Firebase Local Emulator Suite. It runs your project from the files you already have: the same `firebase.json`, rules and Functions code, the same client and Admin SDKs, and the same ports. It serves Firestore, Authentication, Cloud Storage, Cloud Functions, Extensions, Pub/Sub, Cloud Tasks and Eventarc from one npm package, with no Java and nothing else to download. ## Find your way - [Get started](https://firenook.dev/docs/get-started/install): Install Firenook, switch from the Firebase emulators, start a new project, connect your app, and run tests and CI. - [Services](https://firenook.dev/docs/services/firestore): What each emulator does: Firestore, Authentication, Cloud Storage, Cloud Functions, Extensions, Pub/Sub, Cloud Tasks, Eventarc and Security Rules. - [Cloud Functions in depth](https://firenook.dev/docs/functions/scheduled): Scheduled functions, environment variables and secrets, debugging, and reload on save. - [Your data](https://firenook.dev/docs/data/keep-data): Keep data between runs, start from a seed, export snapshots, inspect and compact the store, and recover after a crash. - [Tools](https://firenook.dev/docs/tools/console): The web console, the terminal screen, and the MCP server for your coding agent. - [Reference](https://firenook.dev/docs/reference/cli): Every CLI command and flag, how configuration is read, the ports, and the MCP tools. - [Help](https://firenook.dev/docs/help/troubleshooting): Troubleshooting, the Java 21 error, Windows, running without a Google login, the FAQ, versions, and how to report a problem. - [Contribute](https://firenook.dev/docs/contribute/build): Build Firenook from source, run its tests, and see how the launcher, engine and console fit together. ## Status These docs describe Firenook `0.2.0`, the first release for public use. It runs on Node 22, 24 or 26, on macOS (Apple silicon and Intel), Linux x64 and arm64 with glibc 2.28 or newer, and Windows x64. Every release passes conformance replays against the official emulator and packed installs on all five platforms. Commit your lockfile and upgrade on purpose; see [versions and verification](https://firenook.dev/docs/help/versions). Realtime Database, Hosting, App Hosting and Data Connect are not emulated. A `firebase.json` that configures them still starts, with a warning. [Compatibility](https://firenook.dev/compatibility) records what each service covers and what it does not. Firenook is an independent open-source project under the Apache License 2.0, not affiliated with or endorsed by Google. Firebase and Cloud Firestore are trademarks of Google LLC. ## Next New here? [Install Firenook](https://firenook.dev/docs/get-started/install). Already running the official emulator? [Switch over](https://firenook.dev/docs/get-started/switch). # Install Firenook > Add Firenook to a project with npm, pnpm, Yarn or Bun, check that your machine can run it, and confirm the install before you start. Firenook is one npm package. Install it in your project, next to firebase-tools if you have it: keep that for deploying, since Firenook has no deploy or login command. **npm** ```sh npm install --save-dev firenook ``` **pnpm** ```sh pnpm add --save-dev firenook ``` **Yarn** ```sh yarn add --dev firenook ``` **Bun** ```sh bun add --dev firenook ``` Commit the lockfile: it keeps every machine and every CI run on the release you installed until you upgrade. Run it through your package manager (`npx firenook`, `pnpm exec firenook`, `yarn firenook` or `bunx firenook`) or from a script in `package.json`. These docs use `npx`. ## What it needs The emulators run on **Node 22, 24 or 26**. Your Functions run on the same Node, as they run on Node in production, so pick the one your Functions target. The other commands, such as `init` and `--version`, run on any Node from 20 up. An older Node is refused, with the way to switch: ```console $ npx firenook emulators:start 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 ``` An odd or newer major, such as Node 23 or 27, starts with a warning: ```text Firenook: Node 23.11.0 is not one the emulators are tested on (22, 24 and 26); they start on it, and your Functions run on it. If something behaves differently, switch to Node 24 (with nvm: nvm install 24) ``` A Functions codebase whose `engines.node` asks for another Node still loads, and the log says so, as the official emulator does: ```text firenook functions: functions/package.json asks for Node 24 in engines.node, but these functions run on Node 22.23.3, the Node running the emulators; start the emulators with Node 24 to run them on the Node they ask for (with nvm: nvm use 24) ``` Firenook runs on these platforms, each with its own engine package: | Platform | Engine package | | --- | --- | | macOS on Apple silicon | `@firenook/cli-darwin-arm64` | | macOS on Intel | `@firenook/cli-darwin-x64` | | Linux x64 with glibc | `@firenook/cli-linux-x64` | | Linux arm64 with glibc | `@firenook/cli-linux-arm64` | | Windows x64 | `@firenook/cli-win32-x64` | On Linux the engine needs glibc 2.28 or newer: RHEL 8 and its rebuilds, Debian 10, Ubuntu 20.04, the `node:24` and `node:24-slim` images, or a later release of any of them. Each release starts the engine on AlmaLinux 8, Debian 11, Ubuntu 22.04 and `node:24-slim` before it is published. Alpine and other musl-based systems have no glibc, and an older glibc is too old. Firenook says what the system lacks and stops: ```text Firenook: the Linux engine needs glibc 2.28 or newer, and this system has no glibc: its C library is musl, as on Alpine Linux. Use a glibc system or image instead, such as node:24 or node:24-slim rather than node:24-alpine ``` Windows on Arm and 32-bit systems are not supported either. [CI](https://firenook.dev/docs/get-started/ci) and [Docker](https://firenook.dev/docs/get-started/docker) say what this means for the image you pick. ## How the engine gets installed The `firenook` package is a small launcher. Its optional dependencies hold one engine per platform, and your package manager installs only the one that matches your machine. Nothing runs at install time: there is no postinstall script and no download. Your lockfile records the engine of every platform, so a lockfile written on a Mac installs the Linux engine in CI. npm, pnpm, Yarn (classic, and Yarn 4 with Plug'n'Play) and Bun all install the right engine. Keep optional dependencies enabled. Without them, for example after `npm install --omit=optional`, every command that needs the engine stops: ```console $ npx firenook binary-path Firenook: Missing @firenook/cli-darwin-arm64@0.2.0. Install with optional dependencies enabled. No postinstall script is needed. ``` > **Note** > > Recent releases of Yarn 4 install a version only once it is a day old (the > `npmMinimalAgeGate` setting). On the day of a release, Yarn reports that the > new version is quarantined; install it the next day, or lower that setting. Nothing else is downloaded, at install or at start: no Java, no firebase-tools, no emulator archives. The one exception is an Extension from the registry, which is fetched once, on first use; `firenook ext:vendor` copies it into the project so that even that works offline. In a terminal, Firenook also asks the npm registry once a day whether a newer release is out. It never asks in CI, and `FIRENOOK_NO_UPDATE_CHECK=1` turns the check off. ## Keep its working data out of Git Every run keeps its working data under `.firenook/` in your project. Add that folder to your `.gitignore`: ```text .firenook/ ``` `firenook init` adds this line for you, with `*-debug.log`. ## Check the install `--version` prints the release and the engine's source revision. `binary-path` finds the engine for your machine, checks it against the checksum it was published with, and prints where it is: ```console $ npx firenook --version 0.2.0 (engine source 7a9614bea49f8208f96d2126b29ef11ee2f2810b) $ npx firenook binary-path ~/demo-shop/node_modules/@firenook/cli-darwin-arm64/bin/firenook ``` In a project with a `firebase.json`, `doctor` runs the checks that `emulators:start` runs, starts nothing, and prints what it found as JSON: the engine, the Node, the project id, the services that would start, their ports and any warnings. Trimmed: ```console $ npx firenook doctor { "binary": "~/demo-shop/node_modules/@firenook/cli-darwin-arm64/bin/firenook", "node": { "version": "24.20.0", "major": 24, "tested": true, "path": "~/.nvm/versions/node/v24.20.0/bin/node" }, "project": { "project": "demo-shop", "demo": true, "services": [ "firestore", "auth", "storage", "functions", "pubsub" ], "host": "localhost", "errors": [], "warnings": [] }, "warnings": [], "problems": [], "version": "0.2.0", "engineRevision": "7a9614bea49f8208f96d2126b29ef11ee2f2810b" } ``` `node` says which Node runs and whether the emulators are tested on it. `problems` lists what would stop a start, such as a Node older than 22 or a missing engine package; when it lists any, `doctor` exits with code 1. `doctor` itself runs on any Node from 20 up. Without a `firebase.json` it says so and exits with code 1: ```console $ npx firenook doctor Firenook: No firebase.json at ~/demo-shop/firebase.json; run firenook init in the project directory or pass --config ``` ## Start the emulators In a project that already has a `firebase.json`: ```sh npx firenook emulators:start ``` In a terminal you get a live screen: the start-up, the ready summary and a line for every function call. Press `o` to open the console, `r` to reload your functions, and `q` to stop, which saves the data first. The console is at `http://127.0.0.1:4000/console`. ## Next Starting from nothing? [Start a new project](https://firenook.dev/docs/get-started/new-project) with `firenook init`. Already running the official emulator? [Switch over](https://firenook.dev/docs/get-started/switch) in three steps. # Switch from the Firebase emulators > Move a project from the official Firebase emulators to Firenook by changing one word. Your config, rules, Functions code, app and data stay as they are. Firenook is a drop-in replacement. Your `firebase.json`, `.firebaserc`, rules and indexes, Functions code and app stay as they are, and firebase-tools stays installed for deploying. This page takes an existing project across in three steps, after you [install Firenook](https://firenook.dev/docs/get-started/install). ## 1. Check your project `init --adopt` reads your `firebase.json` the way the official CLI does and prints a plan. Nothing changes while you pass `--dry-run`: ```console $ npx firenook init --adopt --dry-run Firenook init --adopt: ~/demo-shop/firebase.json add give Claude Code the Firenook MCP server: ~/demo-shop/.mcp.json Dry run: nothing was written. ``` This project needs nothing but the MCP server for the coding agent the folder shows. The plan has four kinds of line: | Line | Means | | --- | --- | | `error` | Firenook cannot start with this configuration, for example an unknown key under `emulators` | | `skip` | A service Firenook does not emulate: Realtime Database, Hosting, App Hosting or Data Connect | | `warn` | Another warning about the configuration | | `add` | Something Firenook can add for you | The additions are a missing `emulators` entry for a service the file configures, a missing project id in `.firebaserc`, a rules or indexes file that `firebase.json` names but that does not exist, and the MCP server for a coding agent the folder shows. A project with gaps gets a plan like this one: ```console $ npx firenook init --adopt --dry-run Firenook init --adopt: ~/demo-shop/firebase.json skip Firenook: firebase.json configures the hosting emulator, which Firenook does not implement; skipping it. skip Firenook: firebase.json configures the database emulator, which Firenook does not implement; skipping it. add add emulators.storage {port: 9199} add set .firebaserc projects.default = demo-shop (create the file) add write Storage rules (official init template): ~/demo-shop/storage.rules add give Claude Code the Firenook MCP server: ~/demo-shop/.mcp.json Dry run: nothing was written. ``` Run `npx firenook init --adopt` without `--dry-run` to apply the additions. It edits your files as text: every byte you wrote stays as it was, and each addition goes at the end of the object it joins, in that object's indentation, line endings and layout. In the project above, `firebase.json` gets one entry at the end of `emulators`, on a line of its own like the entries before it: ```diff "database": { "port": 9000 }, - "ui": { "enabled": true } + "ui": { "enabled": true }, + "storage": {"port": 9199} } ``` When the plan has an error, it writes nothing and exits with code 1: ```console $ npx firenook init --adopt Firenook init --adopt: ~/demo-shop/firebase.json error Unsupported configured emulator: bogus. Valid emulators keys are firestore, auth, storage, functions, pubsub, hub, ui, logging, eventarc, tasks, extensions, singleProjectMode, database, hosting, dataconnect, apphosting. skip Firenook: firebase.json configures the hosting emulator, which Firenook does not implement; skipping it. skip Firenook: firebase.json configures the database emulator, which Firenook does not implement; skipping it. add give Claude Code the Firenook MCP server: ~/demo-shop/.mcp.json Fix the errors above, then re-run firenook init --adopt; nothing was written. ``` In a terminal, plain `npx firenook init` in a folder with a `firebase.json` shows the same plan and applies it on a yes. `npx firenook doctor` runs the same checks plus the engine and Node. It changes nothing and prints the result as JSON, with what would stop a start under `problems`. ## 2. Swap the command `firebase emulators:start` becomes `firenook emulators:start`, and `firebase emulators:exec` becomes `firenook emulators:exec`. The common flags carry over: `--only`, `--project`, `--import`, `--export-on-exit`, `--inspect-functions`, and `--ui` for `emulators:exec`. ```diff "scripts": { - "emulators": "firebase emulators:start --import ./data --export-on-exit", - "test:integration": "firebase emulators:exec \"npm test\"" + "emulators": "firenook emulators:start --import ./data --export-on-exit", + "test:integration": "firenook emulators:exec \"npm test\"" } ``` Firenook reads and writes the official export format, so `--import` takes the data you already have, and every emulator listens on the port `firebase.json` gives it. Your app needs no change: `connectFirestoreEmulator(db, '127.0.0.1', 8080)`, `FIRESTORE_EMULATOR_HOST` and the rest work as before. ## 3. Keep your data without exporting This step is optional. Instead of an import at every start and an export at every stop, replace `--export-on-exit` with `--keep-data`. The first start imports `./data` into `.firenook/state`, every later start picks the data up where you left it, and `./data` itself is never written: ```sh npx firenook emulators:start --import ./data --keep-data ``` Keep `--import ./data` in the command. Data that began from a seed remembers it, and a start without that `--import` is refused, with the data left as it was. [Keep data between runs](https://firenook.dev/docs/data/keep-data) has the details. ## What is different - **Node 22, 24 or 26, no Java.** The emulators run on one of these, and your Functions run on the same Node. Nothing else is installed or downloaded. - **`functions:shell`** is `firenook functions:invoke`, which calls any function on the running suite, background triggers included. - **Not emulated:** Realtime Database, Hosting, App Hosting and Data Connect. A `firebase.json` that configures them still starts, with a warning. - **The console** replaces Google's Emulator UI, on the same UI port, at `http://127.0.0.1:4000/console`. The old page addresses, such as `/firestore` or `/auth`, lead to the matching console page. - **The terminal.** In a terminal, `emulators:start` draws its own screen in place of the official banner and table. Pipes and CI get plain lines, with `All emulators ready` on a line of its own. - **One host for the whole suite.** Every emulator listens on the same address. Per-service `host` values in `firebase.json` must match (loopback names aside), and `--host` sets the address for all of them. - **Working files stay.** Each start keeps its working files in `.firenook/runs/`, and Firenook never deletes them. Add `.firenook/` to your `.gitignore`, and remove old runs when you no longer need them. - **`--log-verbosity`** is accepted for compatibility, and Firenook prints its full log whatever the level, except `DEBUG`, which works like `--debug`. ## Going back Stop Firenook (it saves first), export with `npx firenook emulators:export ./export` while it runs or with `--export-on-exit`, and start the official CLI on that export. Never run both on the same ports or in the same working directory. ## Next [Connect your app](https://firenook.dev/docs/get-started/connect) if it is not connected yet, or [run your tests](https://firenook.dev/docs/get-started/tests) against Firenook. # Start a new project > Set up a new project with firenook init. The screen of choices, what it writes and installs, TypeScript by default, and its scripted forms. `firenook init` sets up a new project that runs every emulator: a `firebase.json`, a project id, rules, indexes and a Functions codebase in TypeScript. In a terminal it asks on one screen, then installs the packages, pins Firenook, builds your code once and offers to start the emulators. It needs no Google account. If your project already has a `firebase.json`, [switch it over](https://firenook.dev/docs/get-started/switch) instead. ## Create the project In an empty folder, run `init` through your package manager: **npm** ```sh npx firenook init ``` **pnpm** ```sh pnpm dlx firenook init ``` **Yarn** ```sh yarn dlx firenook init ``` **Bun** ```sh bunx firenook init ``` This runs the latest release, on any Node from 20 up, and `init` pins that release in the folder's `package.json`, so every later command runs the same one. A folder that already has a `package.json` keeps it: `init` adds Firenook and an `emulators` script, and keeps a pin or a script that is already there. ## One screen of choices In a terminal, `init` opens on one screen with every choice already made, so Enter takes them all: ```text ╭──────────────────────────────────────────────────────────────────────────╮ │ │ │ ▄█ │ │ ▄███▀ ▄ firenook 0.2.0 │ │ ▄███▀ ███▄ a new project in ~/my-app │ │ ▀██▀ ███▀ Node 24.20.0 · Claude Code found │ │ █▀▀ │ │ │ ├─ New project ────────────────────────────────────────────────────────────┤ │ │ │ Project ID › demo-my-app │ │ demo- keeps it on this machine: no request can reach a │ │ real Firebase project │ │ Emulators ◉ Firestore 8080 ◉ Auth 9099 ◉ Storage 9199 │ │ ◉ Pub/Sub 8085 ◉ Console 4000 │ │ Functions ● TypeScript ○ JavaScript ○ None │ │ Agents ◉ Claude Code ○ Cursor ○ VS Code │ │ Set up ◉ Install packages ◉ Pin Firenook │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ ↑↓ field ←→ choice space toggle enter create esc cancel ``` | Row | What it chooses | | --- | --- | | Project ID | `demo-` plus the folder's name. Type another id to use your real project's | | Emulators | Firestore, Auth, Storage, Pub/Sub and the console, each on its official port. A port something already listens on is named, with the program that holds it | | Functions | A TypeScript codebase, a JavaScript one, or none. Cloud Tasks and Eventarc come with Functions | | Agents | The coding agents the folder shows (`.claude/`, `.cursor/`, `.vscode/`, or `claude` on the `PATH`), which get Firenook's [MCP server](https://firenook.dev/docs/tools/mcp) | | Set up | Install the packages, and pin Firenook in `package.json` with an `emulators` script | `↑` and `↓` move between the rows, `←`, `→` and Space change one, and the line under the row you are on says what it does: for TypeScript, `functions/src/index.ts, compiled to lib/ as you save`. Esc or Ctrl-C leaves with `Cancelled: nothing was written.` and exit code 130, and the folder stays as it was. When the folder sits inside another package, a `package.json` in a folder above it, Pin Firenook starts off and its line names that file: add Firenook there instead. ## What Enter does Enter writes the files, gives the chosen agents the MCP server, pins Firenook, and runs the installs: `npm install` in `functions/`, and your package manager's install in the project. A TypeScript codebase is then built once. Each step gets a line with its time: ```text ├─ Set-up ──────────────────────────────────────────────────────── 15.3 s ─┤ │ │ │ Files ✓ 4 ms firebase.json, .firebaserc, rules, functions/, … │ │ Agents ✓ 0 ms .mcp.json for Claude Code │ │ Firenook ✓ 517 ms 0.2.0 in package.json │ │ Packages ✓ 14.1 s npm install in functions/, 241 packages │ │ Build ✓ 1.1 s tsc, into functions/lib │ │ │ ``` Then `init` asks `Start the emulators now?`. Yes starts them in the same terminal. No ends with what to run later: ```text ├─ Next ───────────────────────────────────────────────────────────────────┤ │ │ │ Start npm run emulators │ │ starts the emulators and compiles your TypeScript as you │ │ save │ │ Connect the console's Connect button has the lines that point │ │ your app at the emulators, for each SDK │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ ``` Ctrl-C during the installs stops them and keeps the files already written. On a Node older than 22, `init` says how to switch instead of offering to start. ## Start it One command starts the emulators from then on: ```sh npm run emulators ``` The `emulators` script is `firenook emulators:start --keep-data --build`: - `--keep-data` keeps your data between runs in `.firenook/state`, from the first start. Without it every start begins empty, as the official emulator does. See [keep data between runs](https://firenook.dev/docs/data/keep-data). - `--build` runs the codebase's own `build:watch` script (`tsc --watch`) beside the emulators. Your TypeScript is compiled before it loads and again on every save, with any errors in the log. See [TypeScript](https://firenook.dev/docs/functions/typescript). The codebase has one HTTP function. Call it once the emulators are ready: ```console $ curl http://127.0.0.1:5001/demo-my-app/us-central1/helloWorld Hello from Firebase! ``` The console is at `http://127.0.0.1:4000/console`. The codebase has a `serve` script of its own, as the official template does. From `functions/`, `npm run serve` starts only the Functions emulator, with `--build` for TypeScript; Firenook finds `firebase.json` in the folder above. > **Caution** > > `--keep-data` keeps Firestore, Auth and Storage together, so it needs all > three. If you turn Auth or Storage off, on the screen or with `--only`, > remove `--keep-data` from the `emulators` script and from the start command > `init` prints. Otherwise the start is refused: > > ```text > suite failed: --resume-state requires the firestore, auth and storage services (the native state receipt covers all three) > ``` ## What init writes | File | What it holds | | --- | --- | | `firebase.json` | Firestore with its rules and indexes, Storage with its rules, the Functions codebase in `functions/` with a `predeploy` build, every emulator on its official port, and the console on | | `.firebaserc` | The project id, as `projects.default` | | `firestore.rules` | The official starter rules: open to every client for 30 days from today, then closed | | `firestore.indexes.json` | No indexes and no field overrides | | `storage.rules` | The official starter rules: every client read and write is denied | | `functions/package.json` | `firebase-functions` 7 and `firebase-admin` 13 on Node 24, TypeScript 7, and the `build`, `build:watch` and `serve` scripts | | `functions/tsconfig.json` | The official settings: `src/` compiled into `lib/` | | `functions/src/index.ts` | The `helloWorld` HTTP function | | `functions/.gitignore` | `node_modules/`, `lib/` and `*.local` | | `.gitignore` | `.firenook/` and `*-debug.log`, appended when missing | | `package.json` | With Pin Firenook: Firenook at an exact version, and the `emulators` script | | `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` | Firenook's MCP server, for each agent you chose | These are the official `firebase init` templates. With JavaScript, `functions/index.js` holds the function instead, with no `tsconfig.json`, no build scripts and no `predeploy`, and the scripts start without `--build`. The scripts, as `init` writes them for TypeScript: ```json { "name": "my-app", "private": true, "scripts": { "emulators": "firenook emulators:start --keep-data --build" }, "devDependencies": { "firenook": "0.2.0" } } ``` ```json "scripts": { "build": "tsc", "build:watch": "tsc --watch", "serve": "firenook emulators:start --only functions --build" }, ``` The Admin SDK and server code are not bound by the rules. Your app's client SDKs are, so write real rules before the Firestore starter rules close. The emulators and their ports, from the `firebase.json` that `init` writes: ```json { "emulators": { "auth": { "port": 9099 }, "functions": { "port": 5001 }, "firestore": { "port": 8080 }, "pubsub": { "port": 8085 }, "storage": { "port": 9199 }, "eventarc": { "port": 9299 }, "tasks": { "port": 9499 }, "hub": { "port": 4400 }, "logging": { "port": 4500 }, "ui": { "enabled": true, "port": 4000 }, "singleProjectMode": true } } ``` Change a port here and Firenook listens on the new one; your app's connect calls must use the same port. The [ports reference](https://firenook.dev/docs/reference/ports) lists them all. ## Without a terminal In a script, in CI, or with its input redirected, `init` asks nothing. It writes the files, installs nothing, and prints the commands that finish the set-up: ```console $ npx firenook init Firenook init: project demo-my-app in ~/my-app wrote ~/my-app/firebase.json wrote ~/my-app/.firebaserc wrote ~/my-app/firestore.rules wrote ~/my-app/firestore.indexes.json wrote ~/my-app/storage.rules wrote ~/my-app/functions/package.json wrote ~/my-app/functions/tsconfig.json wrote ~/my-app/functions/src/index.ts wrote ~/my-app/functions/.gitignore wrote ~/my-app/.gitignore Next steps: npm install --prefix functions npx firenook emulators:start --keep-data --build npx firenook mcp add (gives Claude Code the Firenook MCP server) ``` The last line appears when the folder shows a coding agent. `--yes` takes the screen's choices without asking and does the set-up too, a line a step: ```console $ npx firenook init --yes Firenook init: project demo-my-app in ~/my-app wrote ~/my-app/firebase.json wrote ~/my-app/.firebaserc wrote ~/my-app/firestore.rules wrote ~/my-app/firestore.indexes.json wrote ~/my-app/storage.rules wrote ~/my-app/functions/package.json wrote ~/my-app/functions/tsconfig.json wrote ~/my-app/functions/src/index.ts wrote ~/my-app/functions/.gitignore wrote ~/my-app/.gitignore done Agents: .mcp.json for Claude Code (0.0 s) done Firenook: 0.2.0 in package.json (0.4 s) done Packages: npm install in functions/, 241 packages (2.3 s) done Build: tsc, into functions/lib (0.8 s) Next steps: npm run emulators ``` These flags change the choices, in a terminal or not: | Flag | Does | | --- | --- | | `--yes` | Takes the choices without asking, and installs, pins and builds | | `--no-install` | Installs nothing; the next steps list the installs | | `--no-pin` | Leaves `package.json` as it is | | `--language typescript` | The Functions codebase: `typescript` (the default), `javascript` or `none`. `none` writes no codebase, and so no Functions, Cloud Tasks or Eventarc emulator (`--no-functions` does the same) | | `--only firestore,auth,storage,functions,pubsub` | The emulators to set up. The console and the hub always come; Cloud Tasks and Eventarc come with `functions` | | `--agent claude,cursor,vscode` | The coding agents to give the MCP server, instead of the ones the folder shows | | `--project ID` | Uses this project id instead of `demo-` | | `--dry-run` | Prints each file as `write` or `keep`, and writes nothing | | `--force` | Writes `firebase.json` over an existing one, and keeps every other file that exists | | `--json` | Prints the plan and the next steps as one line of JSON, and asks nothing | ## The demo- project id `init` names the project after its folder: `demo-` plus the folder's name in lowercase, with other characters turned into hyphens. A folder called `My Shop` gets `demo-my-shop`, and a folder already called `demo-shop` keeps that name. A project id that starts with `demo-` is local only, as it is for the official emulator. Firenook contacts no cloud service for it, and you need no Google account or login. Your app must use the same id when it connects; see [connect your app](https://firenook.dev/docs/get-started/connect). Your Functions are still your code, and a function that calls an outside API reaches it. The plain output that pipes and CI get says so as it starts: ```text Demo project demo-my-app: no cloud service is contacted, but user Functions can still reach external providers; this CLI is not a network sandbox. ``` If `.firebaserc` already names a project, `init` keeps it. `--project` picks another: any `demo-` id, or a real project id (6 to 30 lowercase letters, digits and hyphens, starting with a letter). ## A folder that has a firebase.json In a terminal, `init` in a folder with a `firebase.json` shows what `init --adopt` would add, and adds it on a yes; see [switch from the Firebase emulators](https://firenook.dev/docs/get-started/switch). Without a terminal it stops and says what to do: ```console $ npx firenook init Firenook: ~/demo-shop/firebase.json exists; use firenook init --adopt to adapt it, or --force to write a new one (the other files there are kept) ``` `--force` writes `firebase.json` again and keeps the rules, the indexes, the Functions code and the rest of `.firebaserc` as they are. Delete a file first to get its template. TypeScript is the default there too, so pass `--language javascript` for a JavaScript codebase. ## Next [Connect your app](https://firenook.dev/docs/get-started/connect) to the emulators, then [run your tests](https://firenook.dev/docs/get-started/tests) against them. # Connect your app > How apps and server code find the emulators, the ports and hosts to use, demo project ids, and the console's Connect dialog with ready snippets. Firenook serves the same protocols on the same ports as the official emulators, so an app connects to it exactly as it would to them. This page covers what every platform has in common: how an SDK finds the emulators, which host and port to give it, and which project id to use. The platform pages after it have the code. ## How an app finds the emulators There are two ways, and which one applies depends on where the code runs. **Client SDKs** (web, Flutter, Android, Apple platforms, React Native) need one call per product, made once, before the app reads or writes anything: | Platform | The calls, for Authentication, Firestore, Storage and Functions | | --- | --- | | Web, React Native | `connectAuthEmulator`, `connectFirestoreEmulator`, `connectStorageEmulator`, `connectFunctionsEmulator` | | Flutter | `useAuthEmulator`, `useFirestoreEmulator`, `useStorageEmulator`, `useFunctionsEmulator` | | Android, Apple platforms | `useEmulator` on each product's instance | **Server code** (the Admin SDKs and Google's Cloud client libraries) reads environment variables instead. Set them before the SDK starts, or run the code with `firenook emulators:exec`, which sets them for you: | Variable | With the default ports | Read by | | --- | --- | --- | | `FIRESTORE_EMULATOR_HOST` | `127.0.0.1:8080` | Admin SDKs, Firestore libraries | | `FIREBASE_AUTH_EMULATOR_HOST` | `127.0.0.1:9099` | Admin SDKs | | `FIREBASE_STORAGE_EMULATOR_HOST` | `127.0.0.1:9199` | Admin SDKs for Node.js and Go | | `STORAGE_EMULATOR_HOST` | `http://127.0.0.1:9199` | Cloud Storage libraries, and so the Python Admin SDK | | `PUBSUB_EMULATOR_HOST` | `127.0.0.1:8085` | Pub/Sub libraries | | `FIREBASE_EMULATOR_HUB` | `127.0.0.1:4400` | Tools that find the emulators | | `GCLOUD_PROJECT` | The project id | Admin SDKs, Cloud libraries | | `GOOGLE_CLOUD_PROJECT` | The project id | Admin SDKs, Cloud libraries | | `GOOGLE_APPLICATION_CREDENTIALS` | A demo credentials file | SDKs that need credentials | | `CLOUD_TASKS_EMULATOR_HOST` | `127.0.0.1:9499` | The Admin SDK's task queues | | `CLOUD_EVENTARC_EMULATOR_HOST` | `http://127.0.0.1:9299` | The Admin SDK's Eventarc client | `emulators:exec` sets the variables of the emulators that run, as the official command does, the last two when Functions run, and writes the demo credentials file. Write the hosts without `http://`, except the two that the table shows with it. Your Functions need none of this: Firenook starts them with the emulator hosts already set, the last two included. [Server SDKs](https://firenook.dev/docs/get-started/connect/server) has the details per language. ## Ports Every emulator listens on the port `firebase.json` gives it under `emulators`, or on the official default when it gives none. The ones an app connects to: | Emulator | Default port | | --- | --- | | Firestore | 8080 | | Authentication | 9099 | | Cloud Storage | 9199 | | Cloud Functions | 5001 | | Pub/Sub | 8085 | | Eventarc | 9299 | | Cloud Tasks | 9499 | | Console (the Emulator UI port) | 4000 | Every port, with the hub, logging and the Firestore WebSocket, is in [Ports](https://firenook.dev/docs/reference/ports). ## Which host to use Firenook listens on the loopback address unless you tell it otherwise, as the official emulator does: Firestore, Auth, Storage and the other emulators on `127.0.0.1`, and the hub and the console on `::1` as well. Use `127.0.0.1` rather than `localhost`: a client that tries `::1` first without falling back cannot reach Firestore, Auth or Storage. | Where the app runs | Host to use | | --- | --- | | The same machine: a browser, Node.js, the iOS Simulator | `127.0.0.1` | | The Android emulator | `10.0.2.2`, the emulator's name for your machine | | A phone, tablet or another computer on your network | Your machine's network address, with Firenook started with `--host 0.0.0.0` | To reach Firenook from another device, make it listen on every interface: ```sh npx firenook emulators:start --host 0.0.0.0 ``` It warns you before it starts: ```console Firenook: binding 0.0.0.0; the emulators have no authentication, so every service, the data and arbitrary Functions execution are reachable from any device that can reach this host. ``` Take that literally, and only do it on a network you trust. `firebase.json` can set the host instead, with `"host": "0.0.0.0"` on an emulator's entry. Firenook listens on one host for every emulator, so the host you set on one entry applies to all of them, and two different hosts are an error: ```console Firenook: Unsupported host for auth: 0.0.0.0 differs from 192.168.1.50; the Firenook suite listens on one host (use --host or the same emulators..host everywhere). ``` When Firenook listens on `0.0.0.0`, the variables `emulators:exec` sets still name `127.0.0.1`, since that is how code on the same machine reaches it. ## Use the same project id The app must use the project id Firenook runs with: the one in `.firebaserc`, or the one you pass with `--project`. A `demo-` project id, such as `demo-shop`, never reaches a real Firebase project, and the SDKs need no real configuration for it: - Any string works as the `apiKey`, but the client SDKs want one to be there. - Cloud Storage's default bucket is `.appspot.com`, so a demo app sets `storageBucket: 'demo-shop.appspot.com'`. With a real project id, keep your app's own configuration and add the connect calls after it. Products you do not point at an emulator still talk to Google. If the ids differ, Firenook logs a warning ("Multiple projectIds are not recommended in single project mode"), and the console shows a banner that the app is using another project, with a **How to fix** button. Data written without a signed-in user lands under the other project id, where the console does not show it. Firestore requests from a signed-in user fail with `UNAUTHENTICATED: emulator JWT claims do not match this project`, because the Auth emulator issues tokens for Firenook's own project. ## The console's Connect dialog The console writes the connection code for you, with your ports, host and project id filled in. Open the console (`http://127.0.0.1:4000/console`, or press `o` in the terminal) and select **Connect** in the top bar. The dialog lists the platforms down its side: - **Apps**: Web, Flutter, Apple, Android - **Servers**: Admin (Node), Python, Go, Java - **Configuration**: Environment variables, and the `emulators` block of `firebase.json` with the ports this run uses - **Agents**: the [MCP server](https://firenook.dev/docs/tools/mcp) for a coding agent The **Host** menu at the top switches the snippets between this machine, the address you opened the console at (when that is another machine's address), `10.0.2.2` for the Android emulator, or a host you type. The browser remembers your choice. A snippet leaves out a product that is not running, so it never points at a port nothing listens on. For a `demo-` project the snippet is complete and runs as printed; for a real project it keeps your configuration and adds the connect lines. The address names the platform, so a link opens the dialog on it: `http://127.0.0.1:4000/console?connect=android`. The search box in the top bar copies a snippet without opening the dialog: type `snippet` and pick **Copy the Web snippet**, or another platform's. ## Platforms - [Web](https://firenook.dev/docs/get-started/connect/web): the Firebase JS SDK in a browser app, with Vite and Next.js. - [Flutter](https://firenook.dev/docs/get-started/connect/flutter): FlutterFire on Android, iOS and the web. - [Android](https://firenook.dev/docs/get-started/connect/android): Kotlin and Java, the Android emulator and cleartext traffic. - [Apple platforms](https://firenook.dev/docs/get-started/connect/apple): Swift and Objective-C, the Simulator and devices. - [React Native](https://firenook.dev/docs/get-started/connect/react-native): React Native Firebase, or the JS SDK in Expo. - [Server SDKs](https://firenook.dev/docs/get-started/connect/server): the Admin SDKs for Node.js, Python, Go and Java, and Google's Cloud client libraries. ## Next Start with your app's platform above, then [run your tests](https://firenook.dev/docs/get-started/tests) against the same emulators. # Connect a web app > Point the Firebase JS SDK at Firenook with one connect call per product, once, and only in development, with patterns for Vite and Next.js. A web app built on the Firebase JS SDK (the modular API, `firebase` on npm) reaches Firenook through four functions, one per product. You call them once, right after you create each service and before anything reads or writes. The same code works in Node.js, for scripts and tests that use the client SDK. ## Connect Keep Firebase's set-up in one module and import the services from it everywhere else. For a `demo-` project, this is all the configuration the app needs: ```js import { initializeApp } from 'firebase/app' import { connectAuthEmulator, getAuth } from 'firebase/auth' import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore' import { connectStorageEmulator, getStorage } from 'firebase/storage' import { connectFunctionsEmulator, getFunctions } from 'firebase/functions' export const app = initializeApp({ projectId: 'demo-shop', apiKey: 'demo-api-key', storageBucket: 'demo-shop.appspot.com', }) export const auth = getAuth(app) export const db = getFirestore(app) export const storage = getStorage(app) export const functions = getFunctions(app) connectAuthEmulator(auth, 'http://127.0.0.1:9099') connectFirestoreEmulator(db, '127.0.0.1', 8080) connectStorageEmulator(storage, '127.0.0.1', 9199) connectFunctionsEmulator(functions, '127.0.0.1', 5001) ``` The project id must be the one Firenook runs with. The Auth emulator accepts any `apiKey`, but the SDK wants one, and Cloud Storage needs the default bucket, which is `.appspot.com`. With a real project, pass your app's own configuration to `initializeApp` and keep the four connect lines. Use the ports from your `firebase.json` if they are not the defaults. The console's **Connect** dialog prints this code with your ports and project id filled in; see [Connect your app](https://firenook.dev/docs/get-started/connect#the-consoles-connect-dialog). ## Only in development Wrap the connect calls in a check, so a production build talks to your real project. With Vite, `import.meta.env.DEV` is true under `vite` and false in `vite build`: ```js if (import.meta.env.DEV) { connectAuthEmulator(auth, 'http://127.0.0.1:9099') connectFirestoreEmulator(db, '127.0.0.1', 8080) connectStorageEmulator(storage, '127.0.0.1', 9199) connectFunctionsEmulator(functions, '127.0.0.1', 5001) } ``` To switch the emulators on and off by hand instead, use a variable. Vite gives client code only the variables that start with `VITE_`, and Next.js only those that start with `NEXT_PUBLIC_`, which it writes into the bundle when it builds: ```sh # Vite VITE_USE_EMULATORS=true # Next.js NEXT_PUBLIC_USE_EMULATORS=true ``` Then test it where the example above tests `import.meta.env.DEV`: `import.meta.env.VITE_USE_EMULATORS === 'true'` with Vite, or `process.env.NEXT_PUBLIC_USE_EMULATORS === 'true'` with Next.js. In Next.js, the client SDK belongs in client components. Route handlers, server actions and server components use the Admin SDK, which finds the emulators through environment variables; see [Server SDKs](https://firenook.dev/docs/get-started/connect/server). ## What each call expects - **`connectAuthEmulator(auth, url)`** takes a URL with its scheme, `http://127.0.0.1:9099`. It shows a banner at the bottom of the page, "Running in emulator mode. Do not use with production credentials." Pass `{ disableWarnings: true }` as the third argument to hide it. - **`connectFirestoreEmulator(db, host, port)`** takes the host and the port apart, with no scheme. - **`connectStorageEmulator(storage, host, port)`** does too. Download URLs then point at the emulator, for example `http://127.0.0.1:9199/v0/b/demo-shop.appspot.com/o/users%2F…?alt=media&token=…`. - **`connectFunctionsEmulator(functions, host, port)`** connects the `functions` instance you pass. If your functions run in another region, connect the instance for that region: `getFunctions(app, 'europe-west1')`. An HTTP function is at `http://127.0.0.1:5001/demo-shop/us-central1/helloWorld`, project, region and name in the path. Calling `connectFirestoreEmulator` or `connectAuthEmulator` a second time with the same arguments, as a hot reload that runs the module again does, is harmless. Calling `connectAuthEmulator` with a different URL after the app has used Auth throws `auth/emulator-config-failed`. ## Open the app from another device On a phone, `127.0.0.1` is the phone. To try the app on another device, let both the dev server and Firenook listen on your network, and take the host from the address the page was opened at: ```sh npx firenook emulators:start --host 0.0.0.0 npx vite --host ``` ```js const host = location.hostname === 'localhost' ? '127.0.0.1' : location.hostname connectAuthEmulator(auth, `http://${host}:9099`) connectFirestoreEmulator(db, host, 8080) connectStorageEmulator(storage, host, 9199) connectFunctionsEmulator(functions, host, 5001) ``` Firenook has no authentication: with `--host 0.0.0.0`, anyone on the network can read and change the data. Do it only on a network you trust. ## Check the connection Sign a user up and write a document, then open the console at `http://127.0.0.1:4000/console`. The user is under Authentication and the document under Firestore. If the console shows a banner that your app is using another project, the project id in `initializeApp` is not the one Firenook runs with. ## Next Connect your [server code](https://firenook.dev/docs/get-started/connect/server), or [run your tests](https://firenook.dev/docs/get-started/tests) against the emulators. # Connect a Flutter app > Point FlutterFire at Firenook with useAuthEmulator, useFirestoreEmulator, useStorageEmulator and useFunctionsEmulator on Android, iOS and the web. Each FlutterFire plugin has a method that points it at an emulator. Call them in `main()`, after `Firebase.initializeApp` and before the app uses Firebase. The same code serves Android, iOS and the web. ## Connect ```dart import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_functions/cloud_functions.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'firebase_options.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (kDebugMode) { const host = '127.0.0.1'; await FirebaseAuth.instance.useAuthEmulator(host, 9099); FirebaseFirestore.instance.useFirestoreEmulator(host, 8080); await FirebaseStorage.instance.useStorageEmulator(host, 9199); FirebaseFunctions.instance.useFunctionsEmulator(host, 5001); } runApp(const MyApp()); } ``` `useAuthEmulator` and `useStorageEmulator` return a `Future`; await them. `kDebugMode` keeps the calls out of release builds. Use the ports from your `firebase.json` if they are not the defaults; the console's **Connect** dialog prints this code with them filled in. If your functions run in another region, connect that region's instance: `FirebaseFunctions.instanceFor(region: 'europe-west1')`. ## A demo project without FlutterFire's configuration A `demo-` project needs no `firebase_options.dart`. Initialize with options like these, the ones the console's Flutter snippet prints: ```dart await Firebase.initializeApp( options: const FirebaseOptions( apiKey: 'demo-api-key', appId: '1:000000000000:ios:0000000000000000', messagingSenderId: '0', projectId: 'demo-shop', storageBucket: 'demo-shop.appspot.com', ), ); ``` The project id must be the one Firenook runs with, and the storage bucket is `.appspot.com`. The app id only has to have the shape the platform SDKs check for. With a real project, keep `DefaultFirebaseOptions.currentPlatform`. ## The host on each platform | Where the app runs | Host | | --- | --- | | iOS Simulator, macOS, the web | `127.0.0.1` | | Android emulator | `127.0.0.1` works: FlutterFire changes it to `10.0.2.2` | | A phone or tablet | Your machine's network address, with Firenook started with `--host 0.0.0.0` | On Android, each of the four methods replaces `127.0.0.1` and `localhost` with `10.0.2.2`, the Android emulator's name for your machine. They take `automaticHostMapping: false` to turn that off. A network address is left as it is, so on a real device pass your machine's address. One way is to give it at launch: ```sh npx firenook emulators:start --host 0.0.0.0 flutter run --dart-define=EMULATOR_HOST=192.168.1.20 ``` ```dart const host = String.fromEnvironment('EMULATOR_HOST', defaultValue: '127.0.0.1'); ``` With `--host 0.0.0.0`, anyone on your network can reach the emulators and the data, so use it only on a network you trust. On Windows, `useStorageEmulator` prints a message and returns without connecting: FlutterFire does not support the Storage emulator there. ## Android: allow cleartext traffic The emulators speak plain HTTP, and Android blocks cleartext traffic by default for apps that target Android 9 (API level 28) or later. Allow it to the emulator's host in a network security config that only debug builds use: ```xml 10.0.2.2 ``` ```xml ``` Add a `` line for your machine's address when you use a real device. [Android](https://firenook.dev/docs/get-started/connect/android#allow-cleartext-traffic) explains the file. ## iOS: allow local network connections Authentication, Storage and Functions requests are plain HTTP, which App Transport Security governs. Add `NSAllowsLocalNetworking` to `ios/Runner/Info.plist`, and on a real device a reason for the Local Network prompt. [Apple platforms](https://firenook.dev/docs/get-started/connect/apple#app-transport-security) has both keys. ## The offline cache Firestore keeps an offline cache on the device, and the SDK does not clear it when the emulator's data changes. Without `--keep-data`, every Firenook start is empty, so the cache can still hold documents from the last run. Google suggests turning persistence off while you use the emulator. Set it before `useFirestoreEmulator`: ```dart FirebaseFirestore.instance.settings = const Settings(persistenceEnabled: false); FirebaseFirestore.instance.useFirestoreEmulator(host, 8080); ``` ## Next The same set-up for native apps: [Android](https://firenook.dev/docs/get-started/connect/android) and [Apple platforms](https://firenook.dev/docs/get-started/connect/apple). # Connect an Android app > Point the Firebase Android SDKs at Firenook with useEmulator in Kotlin or Java, reach your machine at 10.0.2.2, and allow cleartext in debug builds. Each Firebase Android SDK has a `useEmulator(host, port)` method. Call it once for each product, before the app uses that product. The `Application` class is a good place, since it runs before any activity. ## Connect **Kotlin** ```kotlin package com.example.shop import android.app.Application import com.google.firebase.Firebase import com.google.firebase.auth.auth import com.google.firebase.firestore.firestore import com.google.firebase.functions.functions import com.google.firebase.storage.storage class ShopApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { // 10.0.2.2 is the Android emulator's name for your machine. Firebase.auth.useEmulator("10.0.2.2", 9099) Firebase.firestore.useEmulator("10.0.2.2", 8080) Firebase.storage.useEmulator("10.0.2.2", 9199) Firebase.functions.useEmulator("10.0.2.2", 5001) } } } ``` **Java** ```java package com.example.shop; import android.app.Application; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.functions.FirebaseFunctions; import com.google.firebase.storage.FirebaseStorage; public class ShopApplication extends Application { @Override public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { // 10.0.2.2 is the Android emulator's name for your machine. FirebaseAuth.getInstance().useEmulator("10.0.2.2", 9099); FirebaseFirestore.getInstance().useEmulator("10.0.2.2", 8080); FirebaseStorage.getInstance().useEmulator("10.0.2.2", 9199); FirebaseFunctions.getInstance().useEmulator("10.0.2.2", 5001); } } } ``` Name the class in the manifest's ``. `BuildConfig.DEBUG` keeps the calls out of release builds; since version 8 of the Android Gradle plugin, it needs `buildFeatures { buildConfig = true }` in the app's `build.gradle.kts`. Use the ports from your `firebase.json` if they are not the defaults. The console's **Connect** dialog prints the Kotlin lines with your ports: pick **Android emulator** in its **Host** menu to get `10.0.2.2`. If your functions run in another region, connect that region's instance: `Firebase.functions("europe-west1")` in Kotlin, `FirebaseFunctions.getInstance("europe-west1")` in Java. Firestore's `useEmulator` also turns SSL off for that connection, so it needs no other setting. ## Which host | Where the app runs | Host | | --- | --- | | The Android emulator | `10.0.2.2` | | A phone or tablet on your network | Your machine's network address, with Firenook started with `--host 0.0.0.0` | Inside the Android emulator, `127.0.0.1` is the emulator itself, not your machine. For a real device, start Firenook so it listens on your network: ```sh npx firenook emulators:start --host 0.0.0.0 ``` Firenook has no authentication: with `--host 0.0.0.0`, anyone on the network can reach the emulators and the data, so use it only on a network you trust. ## Allow cleartext traffic The emulators speak plain HTTP. Android blocks cleartext traffic by default for apps that target Android 9 (API level 28) or later, so Authentication, Storage and Functions requests fail until you allow it for the emulator's host. Do it in a network security config that only debug builds use, so a release build keeps blocking cleartext: ```xml 10.0.2.2 192.168.1.20 ``` ```xml ``` Files under `app/src/debug/` go into debug builds only, and Gradle merges this manifest into the main one. ## The offline cache Firestore keeps an offline cache on the device, and the SDK does not clear it when the emulator's data changes. Without `--keep-data`, every Firenook start is empty, so the cache can still hold documents from the last run. Google suggests a memory cache while you use the emulator: **Kotlin** ```kotlin import com.google.firebase.firestore.firestoreSettings import com.google.firebase.firestore.memoryCacheSettings Firebase.firestore.firestoreSettings = firestoreSettings { setLocalCacheSettings(memoryCacheSettings {}) } ``` **Java** ```java import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.MemoryCacheSettings; FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder() .setLocalCacheSettings(MemoryCacheSettings.newBuilder().build()) .build(); FirebaseFirestore.getInstance().setFirestoreSettings(settings); ``` Set it in the same place as `useEmulator`, before the app reads or writes. Settings and `useEmulator` combine, in either order. ## Next Building for iOS too? See [Apple platforms](https://firenook.dev/docs/get-started/connect/apple). For cross-platform apps, see [Flutter](https://firenook.dev/docs/get-started/connect/flutter) or [React Native](https://firenook.dev/docs/get-started/connect/react-native). # Connect an Apple app > Point the Firebase Apple SDKs at Firenook with useEmulator in Swift or Objective-C, turn off Firestore's SSL, and reach your Mac from a device. Each Firebase Apple SDK has a `useEmulator(withHost:port:)` method. Call it once for each product, right after `FirebaseApp.configure()` and before the app uses Firebase. The same code serves iOS, iPadOS and macOS apps. ## Connect **Swift** ```swift import FirebaseAuth import FirebaseCore import FirebaseFirestore import FirebaseFunctions import FirebaseStorage import SwiftUI @main struct ShopApp: App { init() { FirebaseApp.configure() #if DEBUG let host = "127.0.0.1" Auth.auth().useEmulator(withHost: host, port: 9099) let db = Firestore.firestore() db.useEmulator(withHost: host, port: 8080) let settings = db.settings settings.isSSLEnabled = false settings.cacheSettings = MemoryCacheSettings() db.settings = settings Storage.storage().useEmulator(withHost: host, port: 9199) Functions.functions().useEmulator(withHost: host, port: 5001) #endif } var body: some Scene { WindowGroup { ContentView() } } } ``` **Objective-C** ```objc @import FirebaseAuth; @import FirebaseCore; @import FirebaseFirestore; @import FirebaseFunctions; @import FirebaseStorage; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [FIRApp configure]; #if DEBUG NSString *host = @"127.0.0.1"; [[FIRAuth auth] useEmulatorWithHost:host port:9099]; FIRFirestore *db = [FIRFirestore firestore]; [db useEmulatorWithHost:host port:8080]; FIRFirestoreSettings *settings = db.settings; settings.sslEnabled = NO; settings.cacheSettings = [[FIRMemoryCacheSettings alloc] init]; db.settings = settings; [[FIRStorage storage] useEmulatorWithHost:host port:9199]; [[FIRFunctions functions] useEmulatorWithHost:host port:5001]; #endif return YES; } ``` `#if DEBUG` keeps the calls out of release builds. Use the ports from your `firebase.json` if they are not the defaults; the console's **Connect** dialog prints the Swift lines with your ports filled in. **Firestore needs two settings more.** On Apple platforms, Firestore's `useEmulator` sets only the host: SSL stays on, and the emulator does not speak SSL. Turn it off in Firestore's settings, as Google's own sample does. The memory cache is optional. Firestore's offline cache is not cleared when the emulator's data changes, and without `--keep-data` every Firenook start is empty, so a memory cache keeps the last run's documents from showing up. If your functions run in another region, connect that region's instance: `Functions.functions(region: "europe-west1")`. ## A demo project without GoogleService-Info.plist A `demo-` project needs no configuration file. Configure Firebase with options like these, the ones the console's Apple snippet prints: **Swift** ```swift let options = FirebaseOptions(googleAppID: "1:000000000000:ios:0000000000000000", gcmSenderID: "0") options.apiKey = "demo-api-key" options.projectID = "demo-shop" options.storageBucket = "demo-shop.appspot.com" FirebaseApp.configure(options: options) ``` **Objective-C** ```objc FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:000000000000:ios:0000000000000000" GCMSenderID:@"0"]; options.APIKey = @"demo-api-key"; options.projectID = @"demo-shop"; options.storageBucket = @"demo-shop.appspot.com"; [FIRApp configureWithOptions:options]; ``` The project id must be the one Firenook runs with, and the storage bucket is `.appspot.com`. The app id only has to have the shape the SDK checks for. With a real project, keep `GoogleService-Info.plist` and plain `FirebaseApp.configure()`. ## Simulator or device | Where the app runs | Host | | --- | --- | | The iOS Simulator, or a Mac app | `127.0.0.1` | | An iPhone or iPad | Your Mac's network address, with Firenook started with `--host 0.0.0.0` | The Simulator shares your Mac's network, so `127.0.0.1` is your Mac. A device needs your Mac's address, and Firenook must listen on the network: ```sh npx firenook emulators:start --host 0.0.0.0 ``` Firenook has no authentication: with `--host 0.0.0.0`, anyone on the network can reach the emulators and the data, so use it only on a network you trust. ## App Transport Security Authentication, Storage and Functions requests are plain HTTP, which App Transport Security governs. Since iOS 17 and macOS 14, it no longer allows connections to IP addresses by default. Add `NSAllowsLocalNetworking`, which allows plain connections to IP addresses, `.local` names and names without a domain: ```xml NSAppTransportSecurity NSAllowsLocalNetworking ``` On a device, the first connection to your Mac's address also asks the user for Local Network access. Add a reason for that prompt; the Simulator never shows it: ```xml NSLocalNetworkUsageDescription Connects to the Firebase emulators on your development Mac. ``` ## Next Building for Android too? See [Android](https://firenook.dev/docs/get-started/connect/android). For cross-platform apps, see [Flutter](https://firenook.dev/docs/get-started/connect/flutter) or [React Native](https://firenook.dev/docs/get-started/connect/react-native). # Connect a React Native app > Point React Native Firebase or the Firebase JS SDK at Firenook, choose the host for simulators and devices, and pick the SDK that suits Expo. React Native apps reach Firebase through one of two SDKs, and both connect to Firenook the way they connect to the official emulators. This page shows each, and the host to use on simulators, emulators and devices. ## Which SDK to use - **React Native Firebase** (`@react-native-firebase/*`) wraps the native Android and Apple SDKs, so it also has the products the JS SDK lacks on mobile, such as Analytics and Crashlytics. It needs native code: in an Expo project, that means a development build, not Expo Go. - **The Firebase JS SDK** (`firebase`) is plain JavaScript. It runs in Expo Go and covers Authentication, Firestore, Storage and Functions, the four products a client SDK connects to an emulator. If the app already uses one, keep it: the emulators work the same with both. ## React Native Firebase React Native Firebase's modular API has the same connect functions as the web SDK. Call them once, early, before the app uses Firebase: ```js import { connectAuthEmulator, getAuth } from '@react-native-firebase/auth' import { connectFirestoreEmulator, getFirestore } from '@react-native-firebase/firestore' import { connectStorageEmulator, getStorage } from '@react-native-firebase/storage' import { connectFunctionsEmulator, getFunctions } from '@react-native-firebase/functions' if (__DEV__) { connectAuthEmulator(getAuth(), 'http://127.0.0.1:9099') connectFirestoreEmulator(getFirestore(), '127.0.0.1', 8080) connectStorageEmulator(getStorage(), '127.0.0.1', 9199) connectFunctionsEmulator(getFunctions(), '127.0.0.1', 5001) } ``` React Native Firebase takes the project from your native configuration files, `google-services.json` and `GoogleService-Info.plist`. Firenook must run with that project's id: put it in `.firebaserc` or pass it with `--project`. Firenook accepts any project id, and starts your Functions without Google credentials. On Android, each of these functions replaces `127.0.0.1` and `localhost` with `10.0.2.2`, the Android emulator's name for your machine, and logs a line saying so. The same code therefore works on the iOS Simulator and the Android emulator. To turn the replacement off, set React Native Firebase's flag in the `firebase.json` at your project's root: ```json { "react-native": { "android_bypass_emulator_url_remap": true } } ``` When the app and the emulators share that `firebase.json`, Firenook accepts the `react-native` key and ignores it. ## The Firebase JS SDK, as in Expo Go The JS SDK does not change the host for you, so choose it by platform: ```js import { Platform } from 'react-native' import { initializeApp } from 'firebase/app' import { connectAuthEmulator, getAuth } from 'firebase/auth' import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore' import { connectStorageEmulator, getStorage } from 'firebase/storage' import { connectFunctionsEmulator, getFunctions } from 'firebase/functions' export const app = initializeApp({ projectId: 'demo-shop', apiKey: 'demo-api-key', storageBucket: 'demo-shop.appspot.com', }) export const auth = getAuth(app) export const db = getFirestore(app) export const storage = getStorage(app) export const functions = getFunctions(app) if (__DEV__) { const host = Platform.OS === 'android' ? '10.0.2.2' : '127.0.0.1' connectAuthEmulator(auth, `http://${host}:9099`) connectFirestoreEmulator(db, host, 8080) connectStorageEmulator(storage, host, 9199) connectFunctionsEmulator(functions, host, 5001) } ``` This is the [web set-up](https://firenook.dev/docs/get-started/connect/web) with a platform-dependent host. For a `demo-` project these options are all the configuration it needs; with a real project, pass your app's own. ## On a phone or tablet On a real device, `127.0.0.1` is the device itself, and `10.0.2.2` leads nowhere. Use your machine's network address, and start Firenook so it listens on your network: ```sh npx firenook emulators:start --host 0.0.0.0 ``` React Native Firebase leaves a network address as it is, on Android too. Firenook has no authentication: with `--host 0.0.0.0`, anyone on the network can reach the emulators and the data, so use it only on a network you trust. ## Plain HTTP on Android and iOS The emulators speak plain HTTP. Apps made from the React Native template already allow it in development: the React Native Gradle plugin allows cleartext traffic in debug builds and blocks it in release builds, and the template's `Info.plist` sets `NSAllowsLocalNetworking`. If your app changed those settings, see [Android](https://firenook.dev/docs/get-started/connect/android#allow-cleartext-traffic) and [Apple platforms](https://firenook.dev/docs/get-started/connect/apple#app-transport-security). ## Next Connect the [server code](https://firenook.dev/docs/get-started/connect/server) your app talks to, or [run your tests](https://firenook.dev/docs/get-started/tests) against the emulators. # Connect server code > Point the Admin SDKs for Node.js, Python, Go and Java, and Google's Cloud client libraries, at Firenook with environment variables or emulators:exec. Server code finds the emulators through environment variables, which the Admin SDKs and Google's Cloud client libraries read when they start. Run the code with `firenook emulators:exec` and it sets them for you, or set them yourself for a server you start on its own. ## Let emulators:exec set them ```sh npx firenook emulators:exec -- node scripts/seed.js npx firenook emulators:exec "npm test" ``` `emulators:exec` starts the emulators, runs the command once they are ready, then stops them and exits with the command's exit code. The command gets the variables the official `emulators:exec` sets, for the emulators that run and no others: | Variable | Value with the default ports | Set when | | --- | --- | --- | | `FIRESTORE_EMULATOR_HOST`, `FIREBASE_FIRESTORE_EMULATOR_ADDRESS` | `127.0.0.1:8080` | Firestore runs | | `FIREBASE_AUTH_EMULATOR_HOST` | `127.0.0.1:9099` | Auth runs | | `FIREBASE_STORAGE_EMULATOR_HOST` | `127.0.0.1:9199` | Storage runs | | `STORAGE_EMULATOR_HOST` | `http://127.0.0.1:9199` | Storage runs | | `PUBSUB_EMULATOR_HOST` | `127.0.0.1:8085` | Pub/Sub runs | | `CLOUD_TASKS_EMULATOR_HOST` | `127.0.0.1:9499` | Functions run | | `CLOUD_EVENTARC_EMULATOR_HOST` | `http://127.0.0.1:9299` | Functions run | | `FIREBASE_EMULATOR_HUB` | `127.0.0.1:4400` | Always | | `GCLOUD_PROJECT`, `GOOGLE_CLOUD_PROJECT` | The project id | Always | | `GOOGLE_APPLICATION_CREDENTIALS` | Demo credentials in the run's folder | Unless you set it | | `CLOUDSDK_CONFIG` | A path in the run's folder, where gcloud finds no login of yours | Always | The demo credentials keep the SDKs away from your own Google login, and none of them sends a token request to Google: the file holds the emulators' own token, and names the hub as the place to refresh it. A Node.js SDK refreshes at Google whatever the file says, so `emulators:exec` also adds a `--require` of its token helper to `NODE_OPTIONS`, which answers that request in each Node.js process of the command. Each run's folder is under `.firenook/runs/`. When your environment already sets `GOOGLE_APPLICATION_CREDENTIALS`, `emulators:exec` keeps it, as the official command does, and writes no demo credentials. [Run your tests](https://firenook.dev/docs/get-started/tests#the-variables-your-command-gets) has the details. ## Set them yourself For a server you run next to `firenook emulators:start`, such as a dev server in another terminal, export the variables first: ```sh export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 export FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 export FIREBASE_STORAGE_EMULATOR_HOST=127.0.0.1:9199 export STORAGE_EMULATOR_HOST=http://127.0.0.1:9199 export PUBSUB_EMULATOR_HOST=127.0.0.1:8085 export GCLOUD_PROJECT=demo-shop ``` Or put them in the `.env` file your framework loads in development. Use the ports from your `firebase.json` if they are not the defaults; the console's **Connect** dialog prints these lines, under **Environment variables**, with yours filled in. The project id must be the one Firenook runs with. Never set them in production. With `FIREBASE_AUTH_EMULATOR_HOST` set, the Admin SDKs accept unsigned ID tokens, which the Auth emulator issues. ## Node.js The Admin SDK for Node.js reads `FIRESTORE_EMULATOR_HOST`, `FIREBASE_AUTH_EMULATOR_HOST` and `FIREBASE_STORAGE_EMULATOR_HOST`, and needs no credentials when they are set: ```js import { initializeApp } from 'firebase-admin/app' import { getAuth } from 'firebase-admin/auth' import { getFirestore } from 'firebase-admin/firestore' import { getStorage } from 'firebase-admin/storage' initializeApp({ projectId: 'demo-shop', storageBucket: 'demo-shop.appspot.com' }) const user = await getAuth().createUser({ email: 'ada@example.com', password: 'secret-pass' }) await getFirestore().doc(`users/${user.uid}`).set({ name: 'Ada' }) await getStorage().bucket().file('reports/today.txt').save('hello') ``` ```sh npx firenook emulators:exec -- node scripts/seed.js ``` `createCustomToken` works too: with the Auth emulator, the SDK signs nothing, and the client SDK connected to the same emulator accepts the token. ## Python The Admin SDK for Python reads `FIRESTORE_EMULATOR_HOST` and `FIREBASE_AUTH_EMULATOR_HOST`. Its `storage` module does not read `FIREBASE_STORAGE_EMULATOR_HOST`: the Cloud Storage library under it reads `STORAGE_EMULATOR_HOST`, a URL with `http://`. The SDK also looks for default credentials when it starts. Run it with `emulators:exec`, which sets all of these: ```python import firebase_admin from firebase_admin import auth, firestore, storage firebase_admin.initialize_app(options={"projectId": "demo-shop", "storageBucket": "demo-shop.appspot.com"}) user = auth.create_user(email="ada@example.com", password="secret-pass") firestore.client().document(f"users/{user.uid}").set({"name": "Ada"}) storage.bucket().blob("reports/today.txt").upload_from_string("hello") ``` ```sh npx firenook emulators:exec -- python seed.py ``` To run it on its own, next to `firenook emulators:start`, set the variables and point `GOOGLE_APPLICATION_CREDENTIALS` at a demo credentials file like the one `emulators:exec` writes. It holds the emulators' own token, which the SDK uses as it is, so it never asks Google for one. With the default hub port: ```json {"type":"authorized_user","client_id":"demo","client_secret":"demo","refresh_token":"demo","token_uri":"http://127.0.0.1:4400/firenook/oauth2/token","token":"owner","expiry":"2999-12-31T23:59:59Z"} ``` ```sh export GOOGLE_APPLICATION_CREDENTIALS=demo-credentials.json export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 export FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 export STORAGE_EMULATOR_HOST=http://127.0.0.1:9199 python seed.py ``` The console's **Connect** dialog prints the Python lines with your ports and this file's content filled in. > **Caution** > > Without `STORAGE_EMULATOR_HOST`, Python's Storage calls go to the real Cloud > Storage at `storage.googleapis.com`, with whatever credentials your machine > has. `FIREBASE_STORAGE_EMULATOR_HOST` alone does not stop that. ## Go The Admin SDK for Go reads `FIRESTORE_EMULATOR_HOST` and `FIREBASE_AUTH_EMULATOR_HOST`, and copies `FIREBASE_STORAGE_EMULATOR_HOST` into `STORAGE_EMULATOR_HOST` when that is not set. Run the program with `emulators:exec`, or set the variables and the demo credentials file yourself: ```go app, err := firebase.NewApp(ctx, &firebase.Config{ ProjectID: "demo-shop", StorageBucket: "demo-shop.appspot.com", }) ``` ```sh npx firenook emulators:exec -- go run . ``` ## Java A Java program cannot change its own environment, so the variables, and `GOOGLE_APPLICATION_CREDENTIALS` pointing at the demo credentials file, must be set before the JVM starts. `emulators:exec` does that for the command it runs. Java's Storage and Pub/Sub clients read no emulator variable, so build them on the emulator; the console's Java snippet shows how. ```java FirebaseOptions options = FirebaseOptions.builder() .setProjectId("demo-shop") .setStorageBucket("demo-shop.appspot.com") .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); FirebaseApp.initializeApp(options); ``` ```sh npx firenook emulators:exec -- ./gradlew run ``` ## Google Cloud client libraries Google's Cloud client libraries read the same variables: the Firestore libraries `FIRESTORE_EMULATOR_HOST`, the Cloud Storage libraries `STORAGE_EMULATOR_HOST`, and the Pub/Sub libraries `PUBSUB_EMULATOR_HOST`. With Pub/Sub, for example, no code changes: ```js import { PubSub } from '@google-cloud/pubsub' const pubsub = new PubSub({ projectId: 'demo-shop' }) const [topic] = await pubsub.createTopic('orders') const [subscription] = await topic.createSubscription('orders-worker') await topic.publishMessage({ json: { orderId: 'A-1001' } }) subscription.once('message', async (message) => { console.log(message.data.toString()) message.ack() await subscription.close() await pubsub.close() }) ``` ```sh PUBSUB_EMULATOR_HOST=127.0.0.1:8085 node scripts/publish.js ``` With `PUBSUB_EMULATOR_HOST` set, the library needs no credentials. ## Cloud Tasks and Eventarc The Admin SDK sends task queue calls to `CLOUD_TASKS_EMULATOR_HOST` (`127.0.0.1:9499`, no scheme) and Eventarc events to `CLOUD_EVENTARC_EMULATOR_HOST` (`http://127.0.0.1:9299`, with the scheme). Firenook gives both to your Functions, and `emulators:exec` sets both for its command when Functions run. A script you run on its own sets them itself. The Node.js SDK reaches both, and Python's reaches Cloud Tasks. An Eventarc publish from Node.js asks for an access token first: under `emulators:exec` it gets the emulators' own; run on its own, it asks your default credentials. See [Cloud Tasks](https://firenook.dev/docs/services/tasks) and [Eventarc](https://firenook.dev/docs/services/eventarc). ## Next [Run your tests](https://firenook.dev/docs/get-started/tests) with `emulators:exec`, or run them in [CI](https://firenook.dev/docs/get-started/ci). # Run your tests > Run integration tests against the emulators with firenook emulators:exec. The two command forms, the variables it sets, exit codes, seed data and an example. `firenook emulators:exec` starts the emulators, waits until every one is ready, runs your test command with the emulator variables set, then stops the emulators and exits with your command's exit code. Use it for integration tests on your machine and in [CI](https://firenook.dev/docs/get-started/ci). ```sh npx firenook emulators:exec "npm test" ``` ## The two command forms Give the command as one quoted string, and Firenook runs it through the system shell (`sh`, or `cmd.exe` on Windows), as the official `emulators:exec` does. You can use `&&`, pipes and variables, and Firenook says on stderr that a shell runs it: ```console $ npx firenook emulators:exec "npm run build && npm test" Firenook: running the script through the shell as the official CLI does; use -- for an argv command without shell interpretation ``` Or put the command after `--`, and Firenook starts the program directly, with the arguments as they are and no shell in between: ```sh npx firenook emulators:exec -- node --test test/orders.test.mjs ``` Two or more words without quotes and without `--` are refused, with the command written the right way: ```console $ npx firenook emulators:exec npm test Firenook: emulators:exec received 2 arguments without --; run: firenook emulators:exec -- npm test ``` On Windows, start programs directly after `--` (`-- node test.mjs`). For a `.cmd` script such as `npm`, use the quoted form or name its interpreter: `-- cmd.exe /c npm test`. `emulators:exec` takes the options of `emulators:start`: `--only`, `--import`, `--export-on-exit`, `--project` and the rest. The console stays off unless you pass `--ui` or `firebase.json` sets `emulators.ui.enabled` to `true`, as `firenook init` does. ## Build TypeScript first A TypeScript codebase runs from what the compiler wrote into `lib/`, which is not committed. Add `--build`, and `emulators:exec` runs the codebase's own `build` script before the emulators start: ```sh npx firenook emulators:exec --build "npm test" ``` When the code does not compile, the tests do not run, and the run exits with code 1. Trimmed: ```console $ npx firenook emulators:exec --build "npm test" Build: functions runs its build script before the emulators start build functions: src/index.ts:16:9 TS2322: Type 'number' is not assignable to type 'string'. build functions: build failed (exit code 2) Firenook: the build in functions failed (npm run build); the tests did not run ``` A JavaScript codebase has no build script, and `--build` leaves it as it is. [TypeScript](https://firenook.dev/docs/functions/typescript) covers `--build` with `emulators:start` too. ## An example In the project `firenook init` set up, a function adds up each new order. The test writes an order with the Admin SDK and waits for the function to fill in the total. It uses Node's built-in test runner, `node:test`. Add the function to `functions/src/index.ts`: ```ts import {onDocumentCreated} from "firebase-functions/firestore"; type Item = {sku: string; price: number; qty: number}; // Adds up a new order's items and writes the total back. export const orderTotal = onDocumentCreated("orders/{orderId}", (event) => { const items: Item[] = event.data?.data().items ?? []; const total = items.reduce((sum, item) => sum + item.price * item.qty, 0); return event.data?.ref.update({total}); }); ``` ```js import { test } from 'node:test'; import assert from 'node:assert/strict'; import { initializeApp } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; // emulators:exec sets GCLOUD_PROJECT and FIRESTORE_EMULATOR_HOST, // so the Admin SDK talks to the emulator, never to a real project. const db = getFirestore(initializeApp()); test('a document reads back as written', async () => { const ref = db.doc('products/mug'); await ref.set({ name: 'Mug', price: 18 }); const snapshot = await ref.get(); assert.deepEqual(snapshot.data(), { name: 'Mug', price: 18 }); }); test('a new order gets its total', async () => { const ref = await db.collection('orders').add({ items: [{ sku: 'mug', price: 18, qty: 2 }], }); // The orderTotal function runs on the write; wait for its update. const total = await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('no total after 5 s')), 5000); const stop = ref.onSnapshot((snapshot) => { if (snapshot.get('total') === undefined) return; clearTimeout(timer); stop(); resolve(snapshot.get('total')); }); }); assert.equal(total, 36); }); ``` Install the Admin SDK for the tests, and add two scripts: ```sh npm install --save-dev firebase-admin ``` ```json { "scripts": { "test": "node --test", "test:integration": "firenook emulators:exec --build \"npm test\"" } } ``` `npm test` alone has no emulators to talk to; `npm run test:integration` builds the functions and starts the emulators around it. Trimmed: ```console $ npm run test:integration Firenook: running the script through the shell as the official CLI does; use -- for an argv command without shell interpretation Firenook 0.2.0; engine 7a9614bea49f8208f96d2126b29ef11ee2f2810b; disk/WAL state ~/demo-shop/.firenook/runs/session-Syhid2/state Services: firestore, auth, storage, functions, pubsub; Emulator UI on; host localhost Build: functions runs its build script before the emulators start firenook functions: Loaded functions definitions from source: helloWorld, orderTotal. firenook functions[us-central1-orderTotal]: firestore function initialized. All emulators ready ✔ a document reads back as written (75.62125ms) firenook functions: Beginning execution of "us-central1-orderTotal" ✔ a new order gets its total (179.281458ms) firenook functions: Finished "us-central1-orderTotal" in 178ms ℹ tests 2 ℹ pass 2 ℹ fail 0 firenook functions host: stopping after the suite shutdown request firenook suite stopped cleanly: {"functions":2,"schedules":0,"taskQueues":0,"firestoreDocuments":2,"authUsers":0,"storageObjects":0,"storageBytes":0, …} ``` ### With Vitest Vitest runs the same way. This suite clears Firestore before each test through the emulator's own endpoint, so each test starts from an empty database: ```js import { describe, it, expect, beforeEach } from 'vitest'; import { initializeApp } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; const db = getFirestore(initializeApp()); const project = process.env.GCLOUD_PROJECT; const firestore = process.env.FIRESTORE_EMULATOR_HOST; beforeEach(async () => { await fetch(`http://${firestore}/emulator/v1/projects/${project}/databases/(default)/documents`, { method: 'DELETE' }); }); describe('products', () => { it('starts empty', async () => { expect((await db.collection('products').get()).size).toBe(0); }); it('reads back what it wrote', async () => { await db.doc('products/mug').set({ name: 'Mug', price: 18 }); expect((await db.doc('products/mug').get()).data()).toEqual({ name: 'Mug', price: 18 }); }); }); ``` ```sh npx firenook emulators:exec "npx vitest run" ``` Auth clears the same way, with `DELETE` on `/emulator/v1/projects//accounts` on its port. From a shell, `npx firenook firestore:delete --all-collections --force` clears Firestore on the running emulators. ## The variables your command gets Your command runs with your own environment plus the variables the official `emulators:exec` sets, for the emulators that run and no others. With every emulator running, on the default ports, for project `demo-shop`: | Variable | Value | Set when | | --- | --- | --- | | `FIRESTORE_EMULATOR_HOST`, `FIREBASE_FIRESTORE_EMULATOR_ADDRESS` | `127.0.0.1:8080` | Firestore runs | | `FIREBASE_AUTH_EMULATOR_HOST` | `127.0.0.1:9099` | Auth runs | | `FIREBASE_STORAGE_EMULATOR_HOST` | `127.0.0.1:9199` | Storage runs | | `STORAGE_EMULATOR_HOST` | `http://127.0.0.1:9199` | Storage runs | | `PUBSUB_EMULATOR_HOST` | `127.0.0.1:8085` | Pub/Sub runs | | `CLOUD_TASKS_EMULATOR_HOST` | `127.0.0.1:9499` | Functions run | | `CLOUD_EVENTARC_EMULATOR_HOST` | `http://127.0.0.1:9299` | Functions run | | `FIREBASE_EMULATOR_HUB` | `127.0.0.1:4400` | Always | | `GCLOUD_PROJECT`, `GOOGLE_CLOUD_PROJECT` | `demo-shop` | Always | | `FIREBASE_CONFIG` | The project id, its default bucket and database URL, as JSON | Unless you set it | | `GOOGLE_APPLICATION_CREDENTIALS` | Demo credentials in the run's folder | Unless you set it | | `CLOUDSDK_CONFIG` | A path in the run's folder, where gcloud finds no login of yours | Always | | `NODE_OPTIONS` | Yours, plus a `--require` of Firenook's token helper | With the demo credentials | The Admin SDKs and the Google Cloud client libraries read these, so they connect to the emulators with no code change, and a service that does not run gets no variable. The addresses are the ones clients connect to: `127.0.0.1` even when the emulators listen on `0.0.0.0`. The demo credentials are an `authorized_user` file, which every Admin SDK reads, and they never send a token request to Google. The file holds the emulators' own token for the Python SDK, and names the hub as the place to refresh it, for Go and Java. A Node.js SDK sends its refresh to Google whatever the file says, so the `NODE_OPTIONS` helper answers that one request in each Node.js process of your command with the emulators' token. Only an Eventarc publish from Node.js and Python's Storage calls ask for a token at all. When your environment already sets `GOOGLE_APPLICATION_CREDENTIALS`, `emulators:exec` passes it on unchanged, as the official command does, writes no demo credentials and says so: ```text Firenook: keeping your GOOGLE_APPLICATION_CREDENTIALS (~/my-key.json), as the official exec does; a call that needs an access token gets it with those credentials ``` A call that needs a token then gets one from Google, with your credentials. ## Rules tests Security Rules tests written with `@firebase/rules-unit-testing` run against Firenook as they do against the official emulator: `initializeTestEnvironment` loads your rules, `authenticatedContext()` and `unauthenticatedContext()` are ruled as a signed-in and a signed-out client, and `withSecurityRulesDisabled()` skips the rules. Run them inside `emulators:exec` like any other test. [Security Rules](https://firenook.dev/docs/services/rules) has the set-up and an example. ## Exit codes | Code | When | | --- | --- | | Your command's code | The emulators started, your command ran, and the emulators stopped cleanly | | `1` | The emulators could not start, your command could not be started, or the emulators exited before they were asked to stop | | `130` | Your command was ended by Ctrl-C (SIGINT) | | Another code from Firenook | The emulators failed while stopping, for example during an export | A command that cannot be found ends the run with code 1: ```console $ npx firenook emulators:exec -- no-such-command Firenook: spawn no-such-command ENOENT ``` ## Start from known data `--import` loads an export before your command runs. Every run starts from it again, and nothing is written back to it: ```sh npx firenook emulators:exec --import ./seed "npm test" ``` One way to make the seed is to write the data and export it inside `emulators:exec` itself: ```sh npx firenook emulators:exec "node ./seed.mjs && npx firenook emulators:export ./seed" ``` Add `--export-on-exit ./after` to keep what the tests wrote, in the same official format. [Seeds and imports](https://firenook.dev/docs/data/seeds) covers both. ## Working files Each run keeps its working files in `.firenook/runs/session-*`: the launch record, the data, and `firenook-debug.log` when you pass `--debug`. Firenook never deletes them. Each one is small, but they add up: remove old runs when you no longer need them. ## Next Run the same command in [CI and GitHub Actions](https://firenook.dev/docs/get-started/ci). # CI and GitHub Actions > Run your integration tests against Firenook in CI. A GitHub Actions workflow, caching, the log CI gets, the Linux glibc requirement, and other CI systems. In CI you run your tests the way you run them on your machine, with `npx firenook emulators:exec`. A CI machine needs Node 22, 24 or 26 and, on Linux, glibc 2.28 or later. Nothing else: no Java, no firebase-tools, no login. ## GitHub Actions ```yaml name: Integration tests on: push: pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 cache: npm cache-dependency-path: | package-lock.json functions/package-lock.json - run: npm ci - run: npm ci --prefix functions - run: npx firenook emulators:exec --build "npm test" ``` - `setup-node` installs Node 24. The emulators run your Functions on it, so use the Node your Functions target: 22, 24 or 26. - The first `npm ci` installs Firenook and, from your lockfile, the engine for the runner: the lockfile lists every platform's engine, so one written on a Mac works here. The second installs your Functions' dependencies. - `--build` compiles a TypeScript codebase first, since `lib/` is not committed, and fails the job without running the tests when it does not compile. A JavaScript codebase has no build script, and `--build` leaves it as it is. - `emulators:exec` starts the emulators, runs `npm test`, stops them and exits with your tests' exit code, so failing tests fail the job. See [run your tests](https://firenook.dev/docs/get-started/tests). Firenook runs on `ubuntu-latest`, `ubuntu-24.04`, `ubuntu-22.04`, `ubuntu-24.04-arm`, `macos-latest` and `windows-latest`. ## Caching `cache: npm` keeps npm's download folder between runs, keyed on the lockfiles you list. The Linux engine is a download of about 25 MB, and after the first run it comes from the cache. There is no need to cache `node_modules`, which `npm ci` removes first, or `.firenook/`, which holds the working data of one run. When your tests need data, commit an export and pass `--import`, as in [start from known data](https://firenook.dev/docs/get-started/tests#start-from-known-data). ## The log CI gets In CI, Firenook prints plain lines instead of drawing its terminal screen, and never checks npm for a newer release (GitHub Actions and most CI systems set the `CI` variable; `emulators:exec` prints plain lines in any case). A run looks like this, trimmed: ```text Firenook: running the script through the shell as the official CLI does; use -- for an argv command without shell interpretation Firenook 0.2.0; engine 7a9614bea49f8208f96d2126b29ef11ee2f2810b; disk/WAL state /.firenook/runs/session-Syhid2/state Services: firestore, auth, storage, functions, pubsub; Emulator UI on; host localhost Demo project demo-shop: no cloud service is contacted, but user Functions can still reach external providers; this CLI is not a network sandbox. Working data and launch receipt are preserved in /.firenook/runs/session-Syhid2. No automatic deletion. Build: functions runs its build script before the emulators start firenook: Starting emulators: auth, functions, firestore, pubsub, storage firenook functions: Loaded functions definitions from source: helloWorld, orderTotal. All emulators ready ✔ a document reads back as written (75.62125ms) ✔ a new order gets its total (179.281458ms) firenook suite stopped cleanly: {"functions":2,"schedules":0,"taskQueues":0,"firestoreDocuments":2, …} ``` `All emulators ready` stands on a line of its own: your command starts right after it. If you start the emulators with `emulators:start` in the background instead, wait for that line before you connect. When a run fails and the log is not enough, add `--debug`. Firenook then writes every log record to `firenook-debug.log` in the run's folder and prints its path. Upload the runs as an artifact when the job fails: ```yaml - run: npx firenook emulators:exec --debug "npm test" - if: failure() uses: actions/upload-artifact@v7 with: name: firenook-runs path: .firenook/runs/ include-hidden-files: true ``` `include-hidden-files` is needed because `.firenook` starts with a dot. ## Linux needs glibc Firenook's Linux engine needs glibc 2.28 or later. glibc is the C library of most Linux distributions: RHEL 8 and its rebuilds, Debian 10, Ubuntu 20.04 and every later release have one new enough. Each release is started on AlmaLinux 8, Debian 11, Ubuntu 22.04 and `node:24-slim` before it is published. Alpine Linux, and the `-alpine` Node images built on it, use musl instead. On a musl system every command that needs the engine stops and says so: ```console $ npx firenook emulators:exec "npm test" Firenook: running the script through the shell as the official CLI does; use -- for an argv command without shell interpretation Firenook: the Linux engine needs glibc 2.28 or newer, and this system has no glibc: its C library is musl, as on Alpine Linux. Use a glibc system or image instead, such as node:24 or node:24-slim rather than node:24-alpine ``` A system with an older glibc, such as Amazon Linux 2 (glibc 2.26), is refused the same way, with the glibc it has. Installing glibc compatibility packages on Alpine does not change this. For a job that runs in a container, pick a glibc image with Node: | Image | System | glibc | Runs Firenook | | --- | --- | --- | --- | | `node:24-slim`, `node:24` | Debian 12 | 2.36 | Yes | | `node:24-trixie-slim`, `node:24-trixie` | Debian 13 | 2.41 | Yes | | `node:24-alpine` | Alpine (musl) | none | No | `node:24-slim` is the smallest that works, and the one Firenook's own checks start the engine in. For Node 22 or 26, pick the same variant of `node:22` or `node:26`. ## Other CI systems Every CI system runs the same three steps: Node 22, 24 or 26, `npm ci` for the project and its Functions, and `npx firenook emulators:exec`. What differs is where the job runs and how it caches: - **Jobs in a container** (GitLab CI, Bitbucket Pipelines, CircleCI's Docker executor and others): pick an image from the table above. - **Caching:** keep npm's cache folder, keyed on your lockfiles, as `cache: npm` does on GitHub. - **The log:** most systems set `CI`; where one does not, `emulators:exec` still prints plain lines. In GitLab CI, for example: ```yaml integration: image: node:24-slim variables: npm_config_cache: "$CI_PROJECT_DIR/.npm" cache: key: files: - package-lock.json paths: - .npm/ script: - npm ci - npm ci --prefix functions - npx firenook emulators:exec --build "npm test" ``` ## Several runs on one machine A self-hosted runner that runs jobs side by side needs its own ports for each job: give each its own copy of `firebase.json` with other ports. The jobs may keep the same project id. The second suite to start with an id warns that several run, and each still stops cleanly: ```text firenook: It seems that you are running multiple instances of the emulator suite for project demo-shop. This may result in unexpected behavior. Firenook commands run in this project's folder still reach this suite (elsewhere, pass --hub-port 4400); the official CLI reaches the other one. ``` Commands such as `emulators:export` run in a job's own folder reach that job's suite. Run elsewhere, they ask which one you mean; pass `--hub-port` with that suite's hub port. GitHub-hosted runners give each job a machine of its own, so none of this arises there. ## Next Run the emulators in a container with [Docker](https://firenook.dev/docs/get-started/docker). # Docker > Run Firenook in a container. Pick a glibc Node image such as node:24-slim, listen on all interfaces, publish the ports, and keep the data in a volume. Firenook runs in a Linux container as it runs on your machine. Four things matter: an image with Node 22, 24 or 26 and glibc 2.28 or later, the emulators listening on all interfaces, the ports published, and a volume for the data you want to keep. ## Pick the image Use `node:24-slim`. Firenook's Linux engine needs glibc 2.28 or later, and the official Node images built on Debian have it: - `node:24-slim` (Debian 12, glibc 2.36) works, and is the image Firenook's own release checks start the engine in. `node:24` is the same system with more tools. - `node:24-trixie-slim` (Debian 13, glibc 2.41) works too. - `node:24-alpine` uses musl, not glibc, and Firenook has no engine for it. For Node 22 or 26, use the same variant of `node:22` or `node:26`. [CI and GitHub Actions](https://firenook.dev/docs/get-started/ci#linux-needs-glibc) shows what Firenook prints on a musl image. ## A Dockerfile This one is for a project `firenook init` set up: Firenook in the root `package.json` and a TypeScript Functions codebase in `functions/`, each with its lockfile. ```dockerfile # A glibc image with Node: Debian, not Alpine. FROM node:24-slim WORKDIR /app # Firenook and its engine for this platform (linux-x64 or linux-arm64). COPY package.json package-lock.json ./ RUN npm ci # Your Functions' own dependencies. COPY functions/package.json functions/package-lock.json functions/ RUN npm ci --prefix functions COPY . . # Compile the TypeScript codebase into functions/lib. RUN npm run build --prefix functions # Every emulator on all interfaces, data kept in .firenook/state. CMD ["npx", "firenook", "emulators:start", "--host", "0.0.0.0", "--keep-data"] ``` A JavaScript codebase has no build script: leave out the `RUN npm run build` line. ```text node_modules functions/node_modules functions/lib .firenook ``` The lockfile lists the engine of every platform, so `npm ci` installs the Linux engine in the image even when you wrote the lockfile on a Mac. Keep `node_modules` out of the build context: the engine installed on your machine is not the one the container needs. ## Listen on all interfaces By default the emulators listen on the loopback address, `127.0.0.1` (the hub and the console on `::1` as well), which inside a container only the container itself can reach. `--host 0.0.0.0` makes every emulator, the hub and the console listen on all interfaces. Firenook warns you as it starts, because the emulators have no authentication: ```text Firenook: binding 0.0.0.0; the emulators have no authentication, so every service, the data and arbitrary Functions execution are reachable from any device that can reach this host. ``` Setting the same `host` on every entry under `emulators` in `firebase.json` does the same, as with the official emulator. Firenook listens on one address for the whole suite, so the `host` values must match. The hub and the variables Firenook sets still give clients `127.0.0.1` as the address, the one that reaches a port published to your machine. Bound to `0.0.0.0`, the console answers whatever host name you reach it by. `--allowed-hosts NAME,...` limits it to those names and the loopback ones. ## Publish the ports and keep the data Publish each port under the same number, since clients use the ports that `firebase.json` names. Mount a volume on `/app/.firenook` to keep the data `--keep-data` saves in `.firenook/state`: ```yaml services: firebase: build: . ports: - "4000:4000" # console - "4400:4400" # hub - "4500:4500" # logging - "5001:5001" # Functions - "8080:8080" # Firestore - "8085:8085" # Pub/Sub - "9099:9099" # Auth - "9150:9150" # Firestore WebSocket - "9199:9199" # Storage - "9299:9299" # Eventarc - "9499:9499" # Cloud Tasks volumes: - firenook-data:/app/.firenook stop_grace_period: 1m volumes: firenook-data: ``` ```sh docker compose up --build ``` Your app on your machine connects as usual, for example with `connectFirestoreEmulator(db, '127.0.0.1', 8080)`, and the console is at `http://127.0.0.1:4000/console`. Another container in the same Compose project reaches the emulators by the service name, for example `FIRESTORE_EMULATOR_HOST=firebase:8080`. The volume holds each start's working files in `.firenook/runs/` too. Firenook never deletes them; remove old runs when you no longer need them. ## Stop it cleanly `docker compose stop` and `docker stop` send SIGTERM. Firenook passes it to the engine, which closes the data and stops cleanly; with `--export-on-exit`, it exports first. Docker waits 10 seconds by default before it kills the container. A large data set can need longer to export, so the Compose file above allows a minute (`docker stop -t 60` does the same). After a kill, the next start checks the data and repairs it if it needs to. ## Run your tests in the image `emulators:exec` works in the container as it does anywhere: ```sh docker run --rm $(docker build -q .) npx firenook emulators:exec "npm test" ``` ## Next Run only the emulators you need with [one service on its own](https://firenook.dev/docs/get-started/one-service). # Run one service on its own > Start only the emulators you need with --only, learn which services lean on others, and run Pub/Sub, Auth or Firestore alone with no project and no Java. You rarely need every emulator at once. A test of your Firestore rules needs Firestore; a worker that pulls from Pub/Sub needs Pub/Sub. This page shows two ways to start less: `--only` in a project, and `firenook native` with no project at all. ## Choose services with `--only` `--only` takes the official emulator names, separated by commas: ```sh npx firenook emulators:start --only firestore npx firenook emulators:start --only firestore,functions npx firenook emulators:exec --only pubsub "npm test" ``` | Name | Starts | | --- | --- | | `firestore` | Firestore, with its WebSocket port | | `auth` | Authentication | | `storage` | Cloud Storage | | `functions` | Cloud Functions, with Eventarc and Cloud Tasks | | `pubsub` | Pub/Sub | | `extensions` | The same as `functions` | The hub, the logging port and the console start with any of them, unless `firebase.json` turns the console off. `eventarc`, `tasks`, `hub`, `ui` and `logging` follow the services above and start nothing by themselves. `database`, `hosting`, `dataconnect` and `apphosting` are refused, since Firenook does not emulate them. A service also has to be configured in `firebase.json`. One that is not gets a warning and does not start: ```console $ npx firenook emulators:exec --only firestore,pubsub -- npm test Firenook: not starting the pubsub emulator; firebase.json does not configure it (add "pubsub": {"port": 8085} under "emulators" in firebase.json, as firebase init emulators writes it). ``` When nothing is left to start, Firenook stops with `No emulators to start, run firenook init to get started.` ## What each service needs from the others Each service starts and answers on its own. Some features reach into another service, and work only when that one runs too: | Service | Needs another service for | | --- | --- | | Firestore | Nothing. Functions triggered by its writes need `functions`. | | Auth | Nothing. Blocking functions and user triggers need `functions`. | | Storage | Rules that call `firestore.get()` or `firestore.exists()` read the local Firestore. Without `firestore` they find no document. | | Functions | HTTP and callable functions run alone. Firestore, Storage and Auth triggers need their service; Pub/Sub triggers and scheduled functions need `pubsub`. | | Pub/Sub | Nothing. Functions that handle its messages need `functions`. | A trigger whose service is not running is ignored, as the official emulator ignores it, and the log names the service it waits for. Trimmed: ```console $ npx firenook emulators:start --only functions --plain firenook functions: Loaded functions definitions from source: checkout, sendReceipt, stripeWebhook, nightlyReport. firenook functions[us-central1-checkout]: http function initialized (http://127.0.0.1:5001/demo-shop/us-central1/checkout). firenook functions[us-central1-stripeWebhook]: http function initialized (http://127.0.0.1:5001/demo-shop/us-central1/stripeWebhook). firenook functions[us-central1-sendReceipt]: function ignored because the firestore 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. All emulators ready ``` Here `sendReceipt` waits for Firestore writes and `nightlyReport` for its schedule, which Pub/Sub runs: list `firestore` and `pubsub` in `--only` as well. ## Keeping data with fewer services `--keep-data` keeps Firestore, Auth and Storage together, so it needs all three in the list: ```console $ npx firenook emulators:start --only firestore --keep-data suite failed: --resume-state requires the firestore, auth and storage services (the native state receipt covers all three) ``` Start `--only firestore,auth,storage --keep-data` and add the others you need. For one service alone, use `--import` and `--export-on-exit`; see [export and snapshots](https://firenook.dev/docs/data/export). ## Pub/Sub alone, without Java The official Pub/Sub emulator comes with the Google Cloud CLI and needs a Java runtime. `firenook native pubsub` serves the same Pub/Sub that runs inside Firenook's suite, on one port, with no `firebase.json`, no project and no Java. It speaks gRPC and HTTP/JSON, so the Google Cloud client libraries connect to it through `PUBSUB_EMULATOR_HOST`. ```console $ npx firenook native pubsub --project-id demo-shop --port 8085 Pub/Sub emulator ready at 127.0.0.1:8085 (project demo-shop) ``` In another terminal, a publisher and a subscriber with the Node.js client: ```js import { PubSub } from '@google-cloud/pubsub'; // PUBSUB_EMULATOR_HOST points the client at the emulator. const pubsub = new PubSub({ projectId: 'demo-shop' }); const [topic] = await pubsub.createTopic('orders'); const [subscription] = await topic.createSubscription('orders-worker'); subscription.on('message', (message) => { console.log(`received ${message.data} (${message.attributes.kind})`); message.ack(); subscription.close().then(() => process.exit(0)); }); const id = await topic.publishMessage({ data: Buffer.from('order 1001'), attributes: { kind: 'created' } }); console.log(`published ${id}`); ``` ```console $ PUBSUB_EMULATOR_HOST=127.0.0.1:8085 node publisher.mjs published 1 received order 1001 (created) ``` | Option | Default | Does | | --- | --- | --- | | `--port` | `8085` | The port for gRPC and HTTP/JSON | | `--host` | `127.0.0.1` | The address to listen on | | `--project-id` | `demo-firenook` | The project the emulator reports itself under; requests may name any project | Topics, subscriptions and messages live in memory and are gone when it stops. Ctrl-C stops it. `firenook native` runs on any Node from 20 up, and you can run it without installing Firenook first: `npx firenook native pubsub --port 8085` gets the latest release. The [Pub/Sub service page](https://firenook.dev/docs/services/pubsub) covers what it supports. ## Auth alone `firenook native auth` serves Firenook's Auth emulator by itself, with every operation the suite's Auth has. Accounts live in memory, or in a JSON file with `--state-file`, which keeps accounts, codes and configuration between runs: ```console $ npx firenook native auth --project-id demo-shop --port 9099 --state-file ./auth.json Auth emulator ready at http://127.0.0.1:9099 (project demo-shop) ``` Clients connect with `FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099`, or `connectAuthEmulator(auth, 'http://127.0.0.1:9099')` in the web SDK. `--functions-origin http://127.0.0.1:5001` sends user creation and deletion events to a Functions emulator running elsewhere. ## Firestore alone `firenook native firestore` serves Firestore by itself. Its options follow the official Firestore emulator's, such as `--project_id`, `--rules` and `--seed_from_export`, and `--data-dir` keeps the data on disk; `npx firenook native firestore --help` lists them all. ```console $ npx firenook native firestore --project_id demo-shop --port 8080 WARNING: firenook Security Rules are not configured; client access is open firenook Firestore listening on 127.0.0.1:8080 with 4 runtime worker thread(s); mimalloc purge delay 100 ms, decommit true ``` The Admin SDK connects with `FIRESTORE_EMULATOR_HOST=127.0.0.1:8080`. > **Caution** > > `firenook native` starts the engine directly. It skips the checks > `emulators:start` makes for you, such as reading `firebase.json` and keeping > the project, the data and your credentials apart. Prefer > `emulators:start --only` inside a project. ## Next See what each emulator covers in the [services](https://firenook.dev/docs/services/firestore) pages, starting with Firestore. # Firestore > What the Firestore emulator covers, how to connect, who a request is from, and where it differs from the official one on queries, rules and REST. Firenook's Firestore emulator serves Cloud Firestore in Native mode, Standard edition, on the port your `firebase.json` gives it. Read this page to see what it covers, how your SDKs reach it, and the few places where it behaves differently from the official emulator. ## Set it up Firestore starts when `firebase.json` has a `firestore` section. The `emulators.firestore` entry sets its ports: ```json { "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, "emulators": { "firestore": { "port": 8080, "websocketPort": 9150 } } } ``` | Setting | Default | What it is | | --- | --- | --- | | `emulators.firestore.port` | `8080` | gRPC, the web SDK's WebChannel and REST, all on one port | | `emulators.firestore.websocketPort` | `9150` | The requests feed that tools read | Your app connects as it does to the official emulator. In a web app: ```js import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'; const db = getFirestore(app); connectFirestoreEmulator(db, '127.0.0.1', 8080); ``` Admin and server SDKs read `FIRESTORE_EMULATOR_HOST=127.0.0.1:8080`, which `firenook emulators:exec` and your Functions get set for them. [Connect your app](https://firenook.dev/docs/get-started/connect) has the snippet for each platform. ## What it emulates - **Every SDK transport.** The Admin and native SDKs over gRPC, the web SDK in a browser over WebChannel, and the REST API. - **Reads and writes.** Gets, sets, updates, deletes, batched writes, transactions, field transforms such as `increment()` and `serverTimestamp()`. - **Realtime listeners** on documents and queries, with added, modified and removed changes. - **Queries.** Filters, `or` queries, ordering, cursors, limits, collection group queries, `count()` aggregations and vector search with `findNearest()`. - **Named databases**, each with its own rules (see [below](#named-databases)). - **Security Rules**, with request tracing and coverage ([Security Rules](https://firenook.dev/docs/services/rules)). - **Import and export** in the official format ([Export and snapshots](https://firenook.dev/docs/data/export)). A short run with the web SDK in Node, writing, reading, listening, querying and running a transaction: ```js await setDoc(doc(db, 'orders', 'o1'), { status: 'paid', total: 42 }); const snap = await getDoc(doc(db, 'orders', 'o1')); console.log('read', snap.id, snap.data()); const paid = query(collection(db, 'orders'), where('status', '==', 'paid')); onSnapshot(paid, (s) => { console.log('listen', s.size, s.docChanges().map((c) => `${c.type} ${c.doc.id}`).join(', ')); }); await setDoc(doc(db, 'orders', 'o2'), { status: 'paid', total: 7 }); const sorted = await getDocs(query(paid, orderBy('total', 'desc'))); console.log('query', sorted.docs.map((d) => `${d.id}:${d.data().total}`).join(' ')); await runTransaction(db, async (tx) => { const order = await tx.get(doc(db, 'orders', 'o1')); tx.update(order.ref, { total: order.data().total + 1 }); }); console.log('after transaction', (await getDoc(doc(db, 'orders', 'o1'))).data().total); ``` ```console $ node orders.mjs read o1 { status: 'paid', total: 42 } listen 1 added o1 listen 2 added o2 query o1:42 o2:7 listen 2 modified o1 after transaction 43 ``` Timestamps are stored to the microsecond, as production stores them: a finer value is rounded down before rules see the write. ### What it does not emulate - **The Enterprise edition.** Pipeline queries need it, and the suite runs Standard, so `db.pipeline()` fails as it would on a Standard database: `9 FAILED_PRECONDITION: Pipeline Operations are only available for Firestore databases in Enterprise edition.` - **Index enforcement.** Like the official emulator, Firenook answers a query whether or not production would need a composite index for it. The next section shows how it tells you. ## Queries and indexes Firenook reads the `indexes` file that `firebase.json` names when it starts. Like the official emulator, it never uses the file to refuse a query, and a file it cannot read does not stop the start; it says so and goes on: ```text firenook: the Firestore indexes in ~/demo-shop/firestore.indexes.json were not read (invalid index JSON: invalid type: string "oops", expected a sequence at line 2 column 19); the console explains the queries of database "(default)" as if it declared none ``` The console's query plan, on the Firestore page, uses the file: for the query you are looking at, it shows how the engine answers it, the composite index production would need, whether your `firestore.indexes.json` already declares it, and the entry to copy into the file. So a query that works here and would fail after a deploy shows up before you deploy. ### The field index Saved data keeps its own index over every document's top-level fields, sorted by value. Firenook uses it in two ways: - **Ordered and range queries.** A query ordered by one top-level field, with a limit, a range filter on that field or a cursor, reads documents in that order and stops at its limit, instead of reading and sorting the whole collection. - **Equality.** A query with `==` or `in` on a top-level field reads only the documents that hold the value, and equality on other fields is checked against the index before a document is read. Results are the same either way. `FIRENOOK_FIELD_INDEXES=0` turns the index off for a comparison, and queries then read the whole collection. Data an earlier release saved builds the index once, on the first start of 0.2.0, and says so. Here 50,000 documents 0.2.0-next.8 wrote, kept with `--keep-data`: ```text firenook: indexed the top-level fields of 50000 Firestore documents in 0.2 s ``` On the benchmark server that took 7.4 s for 211,202 documents and 49.3 s for 2.7 million; every start after it opens the data at once. 0.2.0-next.8 can still open data 0.2.0 saved, and builds its own index first. [The benchmarks](https://firenook.dev/benchmarks) compare the query shapes with the official emulator. ## Named databases Firenook serves every database a client names, and each database listed in `firebase.json` gets its own rules: ```json { "firestore": [ { "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, { "database": "reports", "rules": "reports.rules" } ] } ``` An entry without `database` is `(default)`, and a project may list only named databases. Connect to one by name, `getFirestore(app, 'reports')` in the web and Admin SDKs. An export holds every database, and an import restores them all. A database your config does not list follows the rules of the `(default)` entry, or of the first entry when there is no `(default)` one, and follows them as they change. While any rules are configured, no database is open to every request. A project-wide rules upload (`PUT /emulator/v1/projects/:securityRules`) replaces the rules of every database, listed or not. The official emulator refuses more than one database ("Cloud Firestore Emulator does not support multiple databases yet.") and then loads no rules at all. ## Security Rules Firenook loads each rules file `firebase.json` names, watches it, and reloads it one second after you save: ```text firenook firestore: database "(default)" rules reloaded from ~/demo-shop/firestore.rules ``` A file that does not compile leaves the rules in force serving requests, and the log says where: ```text firenook firestore: database "(default)" rules in ~/demo-shop/firestore.rules did not compile; the rules in force are unchanged: 9:37: expected a rules expression ``` ### Who a request is from Firenook decides who sent a request as the official emulator does, the same way over gRPC, WebChannel and REST: | The request carries | The rules see | | --- | --- | | No `Authorization` header | A signed-out client. The client SDKs send none while nobody is signed in. | | `Authorization: Bearer owner` | The owner: the rules are skipped. The Admin SDKs send this. | | Any other `Bearer` token | A signed-in user. The token is read, not verified: `request.auth.uid` is its `sub` claim and `request.auth.token` its payload, whatever its times, project or signature. | So a signed-out client is checked by your rules on every transport, and the tokens `@firebase/rules-unit-testing` makes work as they do on the official emulator. Listing collection ids needs the owner, as on the official emulator. ### When the rules refuse A refused request fails with `PERMISSION_DENIED` and production's message, then, on the next line, the allow statements that refused it, as the official emulator adds them. With these rules: ```txt rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /orders/{orderId} { allow read: if request.auth != null && resource.data.owner == request.auth.uid; allow create: if request.auth != null && request.resource.data.owner == request.auth.uid; } match /notes/{noteId} { allow read, write: if true; } } } ``` the web SDK in Node, signed out, reads an order and a path no rule matches: ```js for (const path of ['orders/o1', 'users/u1']) { try { await getDoc(doc(db, path)); } catch (e) { console.log(path, e.code); console.log(e.message); } } ``` ```console $ node signed-out.mjs orders/o1 permission-denied Missing or insufficient permissions. false for 'get' @ L5 users/u1 permission-denied Missing or insufficient permissions. No matching allow statements ``` `@ L5` is the line of the rules file. Several allows that each refused are listed one after another, and `No matching allow statements` means no allow applied to the path. The console's log names the file as well: ```text Rules denied get on orders/o1 for an unauthenticated client at firestore.rules:5 ``` [Security Rules](https://firenook.dev/docs/services/rules) covers reloading, testing and tracing. ## REST The REST API answers on the same port: `GET`, `PATCH` and `DELETE` on a document, `POST` to a collection to create one, listing a collection with `GET`, and `:commit`, `:batchGet`, `:batchWrite`, `:beginTransaction`, `:rollback`, `:runQuery`, `:runAggregationQuery` and `:listCollectionIds`. Who a request is from follows [the table above](#who-a-request-is-from). `POST` to a collection creates a document with a 20-character automatic id, or the id you give in `documentId`. The rules check it as a create: ```console $ curl -X POST -H 'Content-Type: application/json' \ --data '{"fields":{"title":{"stringValue":"Hello"}}}' \ 'http://127.0.0.1:8080/v1/projects/demo-shop/databases/(default)/documents/notes' {"createTime":"2026-10-22T11:29:13.999205Z","fields":{"title":{"stringValue":"Hello"}},"name":"projects/demo-shop/databases/(default)/documents/notes/6M5S28wz7ql6FtTobfMv","updateTime":"2026-10-22T11:29:13.999205Z"} $ curl -X POST -H 'Content-Type: application/json' \ --data '{"fields":{"title":{"stringValue":"Hello"}}}' \ 'http://127.0.0.1:8080/v1/projects/demo-shop/databases/(default)/documents/notes?documentId=n1' {"createTime":"2026-10-22T11:29:14.010774Z","fields":{"title":{"stringValue":"Hello"}},"name":"projects/demo-shop/databases/(default)/documents/notes/n1","updateTime":"2026-10-22T11:29:14.010774Z"} ``` The same request again answers `409 ALREADY_EXISTS`, and a create the rules refuse answers with the line that refused it: ```json {"error":{"code":403,"message":"Missing or insufficient permissions.\nfalse for 'create' @ L6","status":"PERMISSION_DENIED"}} ``` With the owner token, a seeding script skips the rules, here to write an order the rules refuse to a signed-out client: ```sh curl -X PATCH -H 'Authorization: Bearer owner' -H 'Content-Type: application/json' \ --data '{"fields":{"owner":{"stringValue":"grace"}}}' \ 'http://127.0.0.1:8080/v1/projects/demo-shop/databases/(default)/documents/orders/o5' ``` ## Deliberate differences - **Transactions are optimistic.** A write that conflicts with a read-write transaction succeeds, and the transaction's commit then fails with `ABORTED`, so the SDK runs it again. The official emulator holds a lock instead and aborts the write. A test that races a write against a transaction sees a different side fail. - **REST follows production** where the official emulator's REST adapter departs from it: Firenook answers `:partitionQuery`, reads `readTime` and `transaction` given to a get or a list, and ends a `:runQuery` stream as production does. Most error messages are Firenook's own words, with the official status codes. ## Durability Firestore keeps its data on disk, not in memory. A write is acknowledged once it is journaled, and the journal reaches the drive once a second and at every stop. Killing the emulator loses nothing; a kernel crash or a power cut can lose the last second of writes, and the data stays consistent. To sync every write before it is acknowledged, at the drive's flush time (typically 10 to 30 ms a write instead of under a millisecond): ```sh npx firenook emulators:start --durability per-commit ``` `write-behind` is the default and the only other value. Without `--keep-data`, each start begins with a new, empty store; [Keep data between runs](https://firenook.dev/docs/data/keep-data) explains how to carry it over. ## Next - [Security Rules](https://firenook.dev/docs/services/rules): reloading, testing and tracing. - [Inspect and compact](https://firenook.dev/docs/data/inspect): the size of the saved store. - [The console](https://firenook.dev/docs/tools/console): browse, edit and query your data. # Authentication > Sign-up and sign-in, test providers, codes and links, custom claims, MFA, passkeys, tenants and blocking functions in Firenook's Auth emulator. Firenook's Auth emulator implements every operation the official Auth emulator does. Read this page to set it up, sign users in with test credentials, read the codes and links Auth would have sent, and see what it leaves out. ## Set it up Auth has no top-level section in `firebase.json`, so it starts when `emulators.auth` is there: ```json { "emulators": { "auth": { "port": 9099 } } } ``` Point your app at it. In a web app the address includes `http://`: ```js import { getAuth, connectAuthEmulator } from 'firebase/auth'; const auth = getAuth(app); connectAuthEmulator(auth, 'http://127.0.0.1:9099'); ``` The Admin SDK reads `FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099`, which your Functions and `firenook emulators:exec` get set for them. ## What it emulates All 61 operations of the official Auth emulator, checked against a recorded run of it: - **Sign-in methods:** email and password, anonymous, custom tokens, email links, phone numbers, and test sign-in with Google, Apple and other providers. - **Accounts:** update and delete, session cookies, and the Admin SDK's batch create, get, delete and query. - **Multi-factor and passkeys:** SMS second factors, and passkeys. - **Tenants** and project and tenant configuration. - **Blocking functions** (`beforeUserCreated` and `beforeUserSignedIn`) and the `user.create` and `user.delete` triggers. - **Export and import** in the official format. - **The emulator's own routes:** the codes and links it issued, its configuration, and the popup and redirect helper pages. It does not do what the official emulator answers with `501` either: identity provider, SAML and OIDC configuration, `initializeAuth`, IAM on tenants, Game Center, reCAPTCHA enforcement and TOTP second factors. It sends no real email or SMS, and its ID tokens are unsigned, as the official emulator's are, so production token verification does not apply. ## Sign a user up The SDK calls are the ones you use in production. A sign-up, an email verification read back from the emulator, and a custom claim set through the Admin SDK: ```js const { user } = await createUserWithEmailAndPassword(auth, 'ada@example.com', 'secret-password'); console.log('signed up', user.uid, user.email); await sendEmailVerification(user); const res = await fetch('http://127.0.0.1:9099/emulator/v1/projects/demo-shop/oobCodes'); const { oobCodes } = await res.json(); console.log('oobCodes', oobCodes.map((c) => `${c.requestType} ${c.email}`)); await getAdminAuth().setCustomUserClaims(user.uid, { role: 'admin' }); const token = await user.getIdTokenResult(true); console.log('claims', token.claims.role); ``` ```console $ node signup.mjs signed up Zy9R83B4S4AMfCNTfnAt5JwmeB3Z ada@example.com oobCodes [ 'VERIFY_EMAIL ada@example.com' ] claims admin ``` Custom claims are held to the 1,000-character limit, counted as the official emulator counts them: the compact JSON's length in UTF-16 code units, so `é` counts one and an emoji two. ## Test sign-in with providers Sign in with Google, Apple or another provider by handing the SDK a credential made of plain JSON claims. Nothing reaches the provider: ```js const credential = GoogleAuthProvider.credential( JSON.stringify({ sub: 'g-123', email: 'grace@example.com', email_verified: true }), ); const { user } = await signInWithCredential(auth, credential); console.log('google', user.email, user.providerData.map((p) => p.providerId)); ``` ```console google grace@example.com [ 'google.com' ] ``` `signInWithPopup` and `signInWithRedirect` open the emulator's own helper page, where you pick an account or add a new one. ## Codes and links Every email link and SMS code Auth would have sent is printed in the log: ```text firenook auth: To verify the email address ada@example.com, follow this link: http://127.0.0.1:9099/emulator/action?mode=verifyEmail&lang=en&oobCode=SuGQzETq…&apiKey=fake-api-key firenook auth: To verify the phone number +15555550100, use the code 511430. ``` The console's Authentication page keeps them in an inbox, and the emulator's REST routes return them to your tests. These routes exist only on the emulator: | Route | What it does | | --- | --- | | `GET /emulator/v1/projects/{project}/oobCodes` | Email links: verification, password reset, email sign-in, email change | | `GET /emulator/v1/projects/{project}/verificationCodes` | SMS codes for phone sign-in and SMS second factors | | `GET` and `PATCH /emulator/v1/projects/{project}/config` | The emulator's settings, such as `signIn.allowDuplicateEmails` | | `DELETE /emulator/v1/projects/{project}/accounts` | Delete every account | The codes and the account deletion also take a `/tenants/{tenant}/` form for a tenant. For example: ```console $ curl http://127.0.0.1:9099/emulator/v1/projects/demo-shop/verificationCodes {"verificationCodes":[{"code":"511430","phoneNumber":"+15555550100","sessionInfo":"4UQ4ly4D…"}]} ``` The action links always carry the Auth emulator's own address, including a code asked for from the console. ## Blocking functions A blocking function in your Functions codebase runs on every matching sign-up or sign-in, and what it throws reaches the client: ```js const { beforeUserCreated, HttpsError } = require('firebase-functions/identity'); exports.checkDomain = beforeUserCreated((event) => { if (event.data.email?.endsWith('@blocked.test')) { throw new HttpsError('permission-denied', 'Sign-ups from blocked.test are closed.'); } return { customClaims: { plan: 'free' } }; }); ``` A sign-up from `blocked.test` then fails with the function's message, and any other sign-up gets the claim: ```console $ node blocking.mjs blocked: auth/internal-error Firebase: ((HTTP request to http://127.0.0.1:5001/demo-shop/us-central1/checkDomain returned HTTP error 403: {"error":{"message":"Sign-ups from blocked.test are closed.","status":"PERMISSION_DENIED"}})) (auth/internal-error). allowed, claims: free ``` ## Your users between runs With `--keep-data`, accounts, codes and configuration are kept in `.firenook/state/auth-state.json` and come back at the next start. The file is written within 250 ms of a change, not before the response, and at once on a clean stop; a tool that reads it while the suite runs can see a state that much older. [Keep data between runs](https://firenook.dev/docs/data/keep-data) has the details. Like the official emulator's, exported password hashes use a reversible development format: keep real passwords and real user exports out of it. ## Next - [Run one service on its own](https://firenook.dev/docs/get-started/one-service): Auth without the rest of the suite. - [The console](https://firenook.dev/docs/tools/console): users, their activity and the inbox. - [Security Rules](https://firenook.dev/docs/services/rules): use these users in your rules tests. # Cloud Storage > Firenook's Storage emulator: the Firebase and Cloud Storage APIs, rules evaluated without Java, buckets and targets, and its limits. Firenook's Storage emulator serves the Firebase Storage API that the client SDKs use and the Cloud Storage JSON API that the Admin SDK and Google's client libraries use. Read this page to set up its rules and buckets, see an upload checked by the rules, and learn what it leaves out. ## Set it up Storage starts when `firebase.json` has a `storage` section or an `emulators.storage` entry: ```json { "storage": { "rules": "storage.rules" }, "emulators": { "storage": { "port": 9199 } } } ``` Connect the client SDK and the Admin SDK the usual way: ```js import { getStorage, connectStorageEmulator } from 'firebase/storage'; const storage = getStorage(app); connectStorageEmulator(storage, '127.0.0.1', 9199); ``` The Admin SDK reads `FIREBASE_STORAGE_EMULATOR_HOST=127.0.0.1:9199`, and Google's Cloud Storage libraries read `STORAGE_EMULATOR_HOST=http://127.0.0.1:9199`. Your Functions and `firenook emulators:exec` get both. The `storage` section takes the same three forms as the official CLI: | Form | What it does | | --- | --- | | `{ "rules": "storage.rules" }` | One rules file for every bucket; the default bucket is `.appspot.com` | | `[{ "target": "uploads", "rules": "uploads.rules" }, …]` | A rules file per target, for every bucket the target maps to (see [buckets and targets](#buckets-and-targets)) | | No section | A `demo-` project gets open default rules; a real project id does not start, as on the official emulator | ## What it emulates - **Both APIs:** uploads (simple, multipart and resumable), downloads, metadata and custom metadata, download tokens, listing with pagination, copies and deletes, and gzip. - **Security Rules, evaluated in the engine.** No Java and no rules runtime to download. Version 1 and `rules_version = '2'` files are compiled natively, including `firestore.get()` and `firestore.exists()` against the local Firestore. - **Several buckets** with a rules file each. - **Storage triggers** for your Functions: an object finalized, its metadata updated, or deleted. - **Import and export** in the official format. It does not claim every Cloud Storage feature. Two rules behaviours follow production on purpose where the official emulator does not: a `list` request is matched against your rules as production matches it, not with the official emulator's prefix templates, and rules can read `request.method`, which the official emulator leaves out. ## An upload checked by the rules These rules let a signed-in user write to their own folder, and let a user whose Firestore staff document says `editor` write to `products/`: ```txt rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /users/{uid}/{file} { allow read, write: if request.auth != null && request.auth.uid == uid; } match /products/{file} { allow read; allow write: if request.auth != null && firestore.get(/databases/(default)/documents/staff/$(request.auth.uid)).data.role == 'editor'; } } } ``` A user signed in through the Auth emulator uploads to their folder, is refused elsewhere, and can write to `products/` once the Admin SDK has written their staff document: ```console $ node upload.mjs own folder upload: allowed; hello other folder upload: storage/unauthorized products before staff doc: storage/unauthorized products after staff doc: allowed ``` A rule that fails while it runs, here reading a staff document that did not exist yet, is logged with the request: ```text firenook Storage rules: create on gs://demo-shop.appspot.com/products/lamp.txt failed in the rules: Null value error ``` Requests with `Authorization: Bearer owner`, which the Admin SDK sends, skip the rules, as on the official emulator. ## Rules files Each rules file is watched and reloaded when you save it, whether or not Firestore runs: ```text firenook storage: rules for every bucket reloaded from ~/demo-shop/storage.rules ``` A file that does not compile leaves the previous rules in force. Tests can also replace the rules over HTTP with `PUT /internal/setRules`, which keeps the official behaviour: a body that does not compile leaves no rules at all until the next valid one. [Security Rules](https://firenook.dev/docs/services/rules) covers testing them. ## Buckets and targets To give buckets their own rules, list targets in `firebase.json` and map each target to its buckets in `.firebaserc`: ```json { "storage": [ { "target": "uploads", "rules": "uploads.rules" }, { "target": "public", "rules": "public.rules" } ] } ``` ```console $ npx firenook target:apply storage uploads demo-shop-uploads demo-shop-avatars Applied storage target uploads to demo-shop-uploads, demo-shop-avatars for demo-shop Updated: uploads (demo-shop-avatars,demo-shop-uploads) $ npx firenook target:apply storage public demo-shop-public Applied storage target public to demo-shop-public for demo-shop Updated: public (demo-shop-public) ``` `target:apply` only edits `.firebaserc`; it needs no network and no login. A target's rules govern every bucket it maps to, as on the official emulator: here `uploads.rules` decides every request to both `demo-shop-uploads` and `demo-shop-avatars`. A bucket no target maps to has no rules, and every client request to it is refused: ```json {"error":{"code":403,"message":"Permission denied. Storage Emulator has no loaded ruleset."}} ``` `--storage-bucket target=bucket` maps a target for one run without touching `.firebaserc`, and can be repeated: ```sh npx firenook emulators:start --storage-bucket public=demo-shop-public ``` When a target maps to no bucket at all, a `demo-` project starts with open rules on every bucket and ignores every target, as the official emulator does, and says so: ```text firenook storage: Detected demo project ID "demo-shop", using a default (open) rules configuration. Storage targets in firebase.json will be ignored. ``` A project with a real id does not start until every target has a bucket. ## Other differences - **Storage events need Functions.** An upload, a metadata change or a delete queues an event only when the suite runs Functions, as on the official emulator. With `--only storage`, nothing is queued. - **Unfinished resumable uploads expire.** A resumable upload session left idle for seven days is removed with its bytes, as production expires one after a week. The official emulator keeps them for ever. - **The console works as admin.** The console reads and writes files through its own routes, also in a bucket no rules govern, where every client request is refused; the page says so. A zero-byte `folder/` object shows as an empty folder, and an upload from the console mints a download token and sends the finalize event, as an SDK upload does. ## Next - [Security Rules](https://firenook.dev/docs/services/rules): reload, test and trace your rules. - [Cloud Functions](https://firenook.dev/docs/services/functions): react to uploads. - [Export and snapshots](https://firenook.dev/docs/data/export): files in the official export format. # Cloud Functions > How Firenook runs your Functions: the triggers it delivers, Node 22, 24 and 26, codebases, TypeScript, reload on save, and functions:invoke. 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. ## Set it up Functions start when `firebase.json` has a `functions` section (or `extensions`, which run on the same runtime): ```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](https://firenook.dev/docs/functions/typescript)). Client apps reach callable functions with `connectFunctionsEmulator(functions, '127.0.0.1', 5001)`, and HTTP functions at `http://127.0.0.1:5001///`. ### Node 22, 24 or 26 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: ```text 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: ```text 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. ## Triggers it delivers | 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](https://firenook.dev/docs/functions/scheduled)) | | Task queues (`onTaskDispatched`) | Yes ([Cloud Tasks](https://firenook.dev/docs/services/tasks)) | | Eventarc custom events (`onCustomEventPublished`) | Yes ([Eventarc](https://firenook.dev/docs/services/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`: ```text 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. ## HTTP, callable and event functions With the starter `helloWorld` and a callable `addMessage`, both answer on the Functions port: ```console $ 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: ```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: ```text 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](https://firenook.dev/docs/functions/environment) covers your own `.env` and `.secret.local` files, and params with their defaults. ## Reload on save Firenook watches each codebase and reloads it when you save. The other emulators keep running, and so does your data: ```text 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](https://firenook.dev/docs/functions/debugging#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: ```text 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](https://firenook.dev/docs/functions/debugging) covers `--inspect-functions` and attaching a debugger. ## Call any function with functions:invoke `firenook functions:invoke` calls a function on the running suite. Without `--event-data` it sends an HTTP request, or a callable body with `--data`: ```console $ 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: ```console $ 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](https://firenook.dev/docs/reference/cli) lists every flag. `functions:shell` itself does not exist: Firenook never starts a second Functions runtime. `functions:invoke` finds the running suite the way the official CLI does, through a file named after the project id in your temporary folder, and each Firenook suite also records its own folder. Run in a project's folder, the command reaches the suite started there, even when another suite runs with the same project id. Elsewhere, with two such suites running, it asks for `--hub-port` to pick one. ## Every call, kept 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](https://firenook.dev/docs/tools/mcp), with the error and stack trace of a call that failed. ## What it does not do - **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. ## Next - [Scheduled functions](https://firenook.dev/docs/functions/scheduled) - [Environment variables and secrets](https://firenook.dev/docs/functions/environment) - [TypeScript functions](https://firenook.dev/docs/functions/typescript) - [Debugging and reload on save](https://firenook.dev/docs/functions/debugging) - [Extensions](https://firenook.dev/docs/services/extensions), which run on the same runtime # Extensions > Run the Extensions in your firebase.json with Firenook from local, vendored, cached or registry sources, and make them start offline with ext:vendor. Firenook runs the Extensions instances listed in your `firebase.json` as extra Functions backends, with the official emulator's rules for their parameters, specs and triggers. Read this page to see where their code comes from, how to make them start with no network and no login, and what is not supported. ## Where an instance's code comes from Instances are listed as the official CLI lists them: ```json { "extensions": { "search": "algolia/firestore-algolia-search@1.2.10", "thumbnails": "./extensions-src/thumbnails" } } ``` `extensions` counts as Functions, so the Functions port serves them and `--only functions` includes them. Firenook looks for each instance's code in this order: | Source | Where | Needs network | | --- | --- | --- | | Local | The path you wrote, read from disk | No | | Vendored | `extensions/.sources//@` in your project | No | | Cached | The firebase-tools cache, `~/.cache/firebase/extensions` (or `FIREBASE_EXTENSIONS_CACHE_PATH`) | No | | Registry | Downloaded once, then built with `npm install` and `npm run gcp-build` | Yes, and a login | Parameters come from `extensions/.env`, `.env.`, `.env.`, `.env.local` and `.secret.local`, with the official precedence, defaults and `${param:X}` substitution. While the suite runs, a change to an instance's parameter files reloads that instance; a change that does not load is logged with its error. ## Start offline with ext:vendor `firenook ext:vendor` copies each resolved source into the project and records how unpinned references resolved, so every later start reads it from there: ```console $ npx firenook ext:vendor search: vendored algolia/firestore-algolia-search@1.2.10 at ./extensions/.sources/algolia/firestore-algolia-search@1.2.10 ``` Commit `extensions/.sources/` and your teammates and CI start the same Extensions with no network and no Firebase login. `firenook doctor` shows, for every instance, where its source comes from and whether it starts offline: ```json { "instanceId": "search", "written": "algolia/firestore-algolia-search@1.2.10", "kind": "registry", "version": "1.2.10", "source": "vendored", "offlineReady": true, "paramsFound": true, "note": null } ``` An instance with no vendored or cached source reads `"offlineReady": false` and the note `no vendored or cached source; the first start downloads it (network and a Firebase CLI login or FIREBASE_TOKEN)`. `firenook ext:vendor --offline` never contacts the registry: it copies only what the shared cache already holds, with its `firenook-registry.json`, and names what it could not copy: ```console $ npx firenook ext:vendor --offline firenook extensions: counter (firebase/firestore-counter@0.2.10) could not be vendored: firebase/firestore-counter@0.2.10 is not vendored under ~/demo-shop/extensions/.sources/firebase/firestore-counter@0.2.10 and not in the cache at ~/.cache/firebase/extensions/firebase/firestore-counter@0.2.10; run `firenook ext:vendor` with network access ``` To make sure nothing is fetched, start with `--offline` (or set `FIRENOOK_OFFLINE=1`). An instance that would need the registry then stops the start and says what to do: ```text suite failed: Extensions failed to load: firebase/storage-resize-images@0.2.10 is not vendored under ./extensions/.sources/firebase/storage-resize-images@0.2.10 and not in the cache at ~/.cache/firebase/extensions/firebase/storage-resize-images@0.2.10; run `firenook ext:vendor` with network access ``` ## What needs a login Only the registry. To read an Extension's metadata and, the first time, its source archive, Firenook uses the Firebase CLI's stored login or a `FIREBASE_TOKEN` refresh token, the same credential the official emulator uses. It reads the login where the Firebase CLI writes it: `$XDG_CONFIG_HOME/configstore/firebase-tools.json`, else `~/.config/configstore/firebase-tools.json`, with `~` your home folder (`%USERPROFILE%` on Windows). It then keeps the registry's answers next to the source (`firenook-registry.json`), so later starts stay offline. A source vendored or cached under Firenook's former name keeps its old file and still starts offline. Local, vendored and cached instances need no login at all; [Without a Google login](https://firenook.dev/docs/help/no-login) covers working that way. Secret Manager is never contacted. Put secret parameters in `extensions/.secret.local`. ## What it does not run - **Lifecycle tasks** (`onInstall`, `onUpdate`, `onConfigure`): an Extension's task-queue functions are not delivered, as on the official emulator. - **Triggers for a service the suite does not emulate or does not start.** The whole instance is left out, with the official emulator's lines. Here `--only functions` leaves out Firestore: ```text firenook extensions[audit]: ignored becuase it includes firestore triggered functions, and the firestore emulator does not exist or is not running. firenook extensions: No Cloud Functions for these instances will be emulated, because partially emulating an Extension can lead to unexpected behavior. ``` - **Dynamic Extensions** defined in your Functions code, and Extensions written in Python or Dart. - **Secret Manager**, as above. ## Next - [Cloud Functions](https://firenook.dev/docs/services/functions): the runtime Extensions share. - [Eventarc](https://firenook.dev/docs/services/eventarc): events an Extension publishes. - [CI and GitHub Actions](https://firenook.dev/docs/get-started/ci): vendored Extensions in CI. # Pub/Sub > Firenook's Pub/Sub emulator answers every RPC of the official one over gRPC and HTTP/JSON. Set it up, publish and pull, and see where it differs. Firenook's Pub/Sub emulator answers every RPC of the official Pub/Sub emulator, over gRPC and HTTP/JSON on one port. Read this page to set it up, publish and receive messages, and see the few places where it behaves differently. ## Set it up Pub/Sub has no top-level section in `firebase.json`, so it starts when `emulators.pubsub` is there: ```json { "emulators": { "pubsub": { "port": 8085 } } } ``` Google's Pub/Sub client libraries connect when `PUBSUB_EMULATOR_HOST` is set, as they do to the official emulator: ```sh PUBSUB_EMULATOR_HOST=127.0.0.1:8085 node publisher.mjs ``` Your Functions and `firenook emulators:exec` get the variable set for them. ## What it emulates Every RPC of the official emulator's `google.pubsub.v1` Publisher, Subscriber and SchemaService, and the `google.iam.v1` policy service, checked against a recorded run of it: - Topics, publishing and subscriptions with acknowledgement deadlines and redelivery. - Pull, streaming pull and push delivery with retries. - Ordering keys, filters, retention, seek and snapshots. - Dead-letter topics and retry policies. - Avro schemas with revisions, bound to topics. - Delivery to your `onMessagePublished` functions, through a subscription named `emulator-sub-`, and the schedules of your `onSchedule` functions, in the time zone each declares. It does not do what the official emulator answers with `501` either: Protocol Buffer schemas, IAM, `detachSubscription` and `updateSnapshot`. It has no BigQuery, Cloud Storage or Bigtable export subscriptions, and, like the official emulator, it keeps no messages across a restart. ## Publish and receive The client library works unchanged: ```js import { PubSub } from '@google-cloud/pubsub'; const pubsub = new PubSub({ projectId: 'demo-shop' }); const topic = pubsub.topic('orders'); const [subscription] = await topic.createSubscription('orders-email'); subscription.on('message', (m) => { console.log('received', m.data.toString(), m.attributes); m.ack(); }); const id = await topic.publishMessage({ json: { orderId: 'o43' }, attributes: { kind: 'paid' } }); console.log('published', id); ``` ```console $ PUBSUB_EMULATOR_HOST=127.0.0.1:8085 node pubsub.mjs received {"orderId":"o43"} { kind: 'paid' } published 2 ``` The same port speaks HTTP/JSON, so `curl` works too. Here it pulls from `orders-audit`, another subscription of the topic (output trimmed to one message): ```console $ curl -X POST -H 'Content-Type: application/json' --data '{"maxMessages":10}' \ http://127.0.0.1:8085/v1/projects/demo-shop/subscriptions/orders-audit:pull { "receivedMessages": [ { "ackId": "projects/demo-shop/subscriptions/orders-audit:2", "message": { "data": "eyJvcmRlcklkIjoibzQyIn0=", "attributes": { "kind": "paid" }, "messageId": "1", "publishTime": "2026-10-21T15:20:26.220070+00:00" } } ] } ``` ## Filters and dead letters A subscription with a filter and a dead-letter policy behaves as in production. Here it receives only `paid` messages, the handler refuses each one, and after five attempts the message moves to the dead-letter topic with the attributes Pub/Sub adds: ```js const [events] = await pubsub.createTopic('events'); const [dead] = await pubsub.createTopic('events-dead'); const [paid] = await events.createSubscription('paid', { filter: 'attributes.kind = "paid"', deadLetterPolicy: { deadLetterTopic: dead.name, maxDeliveryAttempts: 5 }, }); await events.publishMessage({ data: Buffer.from('a'), attributes: { kind: 'paid' } }); await events.publishMessage({ data: Buffer.from('b'), attributes: { kind: 'refunded' } }); paid.on('message', (m) => { console.log('paid sub got', m.data.toString(), 'attempt', m.deliveryAttempt); m.nack(); }); ``` ```console $ node dead-letter.mjs paid sub got a attempt 1 paid sub got a attempt 2 paid sub got a attempt 3 paid sub got a attempt 4 paid sub got a attempt 5 dead-letter topic got a { CloudPubSubDeadLetterSourceDeliveryCount: '5', CloudPubSubDeadLetterSourceSubscription: 'paid', CloudPubSubDeadLetterSourceSubscriptionProject: 'demo-shop', CloudPubSubDeadLetterSourceTopicPublishTime: '2026-10-21T15:37:04.216Z', kind: 'paid' } ``` ## Where it differs Three behaviours differ on purpose. Each follows production, or the official emulator's design, where the recorded run of the official emulator says nothing: - **Acknowledged messages are dropped** once nothing can bring them back: the subscription does not retain acknowledged messages, no snapshot holds them, and no other subscription of the topic still needs them. Turning `retainAckedMessages` on later does not bring earlier acknowledgements back to a seek. - **A topic's history is bounded**: the newest 5,000 messages and 16 MiB, or its retention window up to 100,000 messages and 256 MiB, and 512 MiB across all topics. Topic retention seeds a new subscription from that history. - **Dead-lettered messages reach functions** listening on the dead-letter topic, as any publish does. No recorded run of the official emulator confirms this yet. The console's Pub/Sub page can also peek at a backlog, force an acknowledgement and run a schedule now. Those are console controls; no client protocol reaches them. ## Use it on its own For tests that need only Pub/Sub, run it without the rest of the suite: ```console $ npx firenook native pubsub --project-id demo-shop --port 8085 Pub/Sub emulator ready at 127.0.0.1:8085 (project demo-shop) ``` [Run one service on its own](https://firenook.dev/docs/get-started/one-service) has the details. ## Next - [Cloud Functions](https://firenook.dev/docs/services/functions): `onMessagePublished` triggers. - [Scheduled functions](https://firenook.dev/docs/functions/scheduled): schedules run on Pub/Sub. - [Cloud Tasks](https://firenook.dev/docs/services/tasks): queues for one-off work. # Cloud Tasks > Queues for your onTaskDispatched functions with the official emulator's retries and rate limits, how to enqueue from a script, and what Firenook leaves out. Firenook's Cloud Tasks emulator gives every `onTaskDispatched` function its queue and dispatches tasks to it with the official emulator's retries and rate limits. Read this page to enqueue tasks from your code and your scripts, and to see how retries behave. ## Set it up Cloud Tasks runs with Functions; there is nothing to add but its port, which defaults to 9499: ```json { "emulators": { "tasks": { "port": 9499 } } } ``` Each `onTaskDispatched` export gets a queue when your functions load: ```text firenook tasks: Created queue with key: queue:demo-shop-us-central1-sendReceipt ``` ## Enqueue a task A task queue function and an HTTP function that enqueues for it: ```js const { onRequest } = require('firebase-functions/https'); const { onTaskDispatched } = require('firebase-functions/tasks'); const logger = require('firebase-functions/logger'); const { getFunctions } = require('firebase-admin/functions'); exports.sendReceipt = onTaskDispatched( { retryConfig: { maxAttempts: 3, minBackoffSeconds: 1 }, rateLimits: { maxConcurrentDispatches: 2 } }, async (request) => { logger.info('task', { data: request.data, retryCount: request.retryCount }); }, ); exports.enqueueReceipt = onRequest(async (req, res) => { await getFunctions().taskQueue('sendReceipt').enqueue({ orderId: req.query.order }); res.send('queued'); }); ``` The Admin SDK finds the emulator through `CLOUD_TASKS_EMULATOR_HOST`. Firenook sets it inside Functions, and `firenook emulators:exec` sets it for its command, as the official `emulators:exec` does. A script that enqueues, run under exec, and the function it reaches (trimmed to the task): ```js import { initializeApp } from 'firebase-admin/app'; import { getFunctions } from 'firebase-admin/functions'; initializeApp({ projectId: 'demo-shop' }); await getFunctions().taskQueue('sendReceipt').enqueue({ orderId: 'o50' }, { id: 'receipt-o50' }); console.log('enqueued receipt-o50'); ``` ```console $ npx firenook emulators:exec "node enqueue.mjs && sleep 1" All emulators ready enqueued receipt-o50 firenook functions: Beginning execution of "us-central1-sendReceipt" firenook functions: {"data":{"orderId":"o50"},"retryCount":0,"severity":"INFO","message":"task"} ``` The `sleep` keeps the emulators up until the task has run; a test waits for what the task does instead. A script you run by hand sets `CLOUD_TASKS_EMULATOR_HOST=127.0.0.1:9499` itself. `firenook functions:invoke sendReceipt --event-data '{"orderId":"o8"}'` enqueues a task the same way, through this emulator. ## Retries and rate limits A task whose function fails, or answers anything but a 2xx status, is retried with the backoff its `retryConfig` sets. The emulator reproduces the official one's ladder, including its off-by-one: a queue with `maxAttempts: 3` runs a failing task four times. ```text firenook functions: {"retryCount":0,"executionCount":0,"severity":"INFO","message":"charge attempt"} firenook functions: {"retryCount":1,"executionCount":0,"severity":"INFO","message":"charge attempt"} firenook functions: {"retryCount":2,"executionCount":0,"severity":"INFO","message":"charge attempt"} firenook functions: {"retryCount":3,"executionCount":0,"severity":"INFO","message":"charge attempt"} ``` `maxRetrySeconds` keeps a task retrying past `maxAttempts` until its time runs out. `rateLimits` caps how many tasks run at once (`maxConcurrentDispatches`) and how many start each second (`maxDispatchesPerSecond`), and a task enqueued with `dispatchDeadlineSeconds` gets that long for each attempt. Every dispatch carries the `X-CloudTasks-*` headers Cloud Tasks sends. `GET /queueStats` on the Tasks port reports each queue: ```console $ curl http://127.0.0.1:9499/queueStats {"queue:demo-shop-us-central1-sendReceipt":{"numberOfTasks":0,"tasksAdded":0.2,"completedLastMin":1,"failedTasks":0,"runningTasks":2,"maxRate":500,"maxConcurrent":2}} ``` ## The official emulator's quirks, kept These come from the official emulator and are kept on purpose, because clients and tests can see them: - `runningTasks` is the number of dispatch slots, that is `maxConcurrentDispatches`, not the tasks running now. - `tasksAdded` and `failedTasks` are five-minute counts divided by five; `completedLastMin` counts one minute. - A `scheduleTime` never delays a task. - A generated task name starts with a slash, while a delete builds the name without one, so a task with a generated name cannot be deleted. Name your tasks (`enqueue(data, { id })`) if you delete them. - A 5xx answer leaves `executionCount` as it is; any other failure adds one. - Registering a queue again, as a reload does, replaces it and drops its pending tasks. ## What it does not do The same things the official emulator does not: no `scheduleTime` delay, no OIDC tokens, no queue pause, resume, purge or task listing through an API, none of the Cloud Tasks v2 API beyond creating and deleting tasks, and no tasks kept across a restart. The console can pause, resume and purge queues, run, stop and delete tasks, and hold a scheduled task until its time, but those are console controls, not an API. ## Next - [Cloud Functions](https://firenook.dev/docs/services/functions): the functions your tasks run. - [Eventarc](https://firenook.dev/docs/services/eventarc): custom events between functions. - [The console](https://firenook.dev/docs/tools/console): queues and tasks as they run. # Eventarc > Publish custom events and deliver them to your onCustomEventPublished functions with Firenook's Eventarc emulator, from Functions or from a script. Firenook's Eventarc emulator takes the custom events your code publishes and delivers each one to the `onCustomEventPublished` functions that listen for its type. Read this page to publish an event from a function or a script and to see what it keeps. ## Set it up Eventarc runs with Functions; there is nothing to add but its port, which defaults to 9299: ```json { "emulators": { "eventarc": { "port": 9299 } } } ``` ## Listen for an event A handler names the event type, and optionally a channel and filters on the event's attributes: ```js const { onCustomEventPublished } = require('firebase-functions/eventarc'); const logger = require('firebase-functions/logger'); exports.onShipped = onCustomEventPublished('shop.order.shipped', (event) => { logger.info('eventarc', { type: event.type, data: event.data }); }); ``` When your functions load, the handler is registered with the emulator: ```text firenook functions[us-central1-onShipped]: eventarc function initialized. ``` ## Publish an event Publish with the Admin SDK's Eventarc client. The default channel is the `firebase` channel of your project: ```js import { initializeApp } from 'firebase-admin/app'; import { getEventarc } from 'firebase-admin/eventarc'; initializeApp({ projectId: 'demo-shop' }); await getEventarc().channel().publish({ type: 'shop.order.shipped', source: '/shop/orders', subject: 'orders/o42', data: { orderId: 'o42', carrier: 'post' }, }); console.log('published'); ``` Inside Functions, Firenook sets `CLOUD_EVENTARC_EMULATOR_HOST` for you, and so does `firenook emulators:exec` for its command, as the official `emulators:exec` does. Run the script under it, and the handler runs (trimmed to the publish): ```console $ npx firenook emulators:exec "node publish.mjs" All emulators ready firenook functions: Beginning execution of "us-central1-onShipped" published firenook functions: {"type":"shop.order.shipped","data":{"carrier":"post","orderId":"o42"},"severity":"INFO","message":"eventarc"} ``` The Admin SDK for Node.js asks for an access token before it publishes. Under `emulators:exec` it gets the emulators' own: exec gives the command demo credentials, never yours, and in each Node.js process of the command (through `NODE_OPTIONS`) answers that token request itself, so the publish never reaches Google. A runtime that does not read `NODE_OPTIONS`, such as Bun or Deno, misses that step. Inside Functions the worker does the same. A script you run by hand must set the variable itself, as a URL (`CLOUD_EVENTARC_EMULATOR_HOST=http://127.0.0.1:9299`). Without it, the Admin SDK sends the event to the real Eventarc service. With it, the SDK still asks your default Google credentials for a token first, so prefer `emulators:exec`. The Admin SDK also needs a `source`. In production the Functions runtime supplies one; in a plain function, outside an Extension, pass it yourself or the SDK refuses with `CloudEvent 'source' is required.`, as it does on the official emulator. ## What it emulates - Trigger registration and the `getTriggers` and `publishEvents` routes, on the native `google` channel and on named channels. - Delivery by event type, channel and filters to every matching handler. - Events published by Extensions, on their channels. - A record of every event, with what each subscription did with it, including events that could not be read and events nothing listens for: the last 1,000, at most 16 MiB. The console's Eventarc page shows them and can publish or replay one. To see what is registered, ask the emulator: ```console $ curl http://127.0.0.1:9299/google/getTriggers {"shop.order.shipped-projects/demo-shop/locations/us-central1/channels/firebase":[{"eventTrigger":{"channel":"projects/demo-shop/locations/us-central1/channels/firebase","eventFilters":{},"eventType":"shop.order.shipped"},"projectId":"demo-shop","triggerName":"us-central1-onShipped-0-projects/demo-shop/locations/us-central1/channels/firebase"}]} ``` `firenook functions:invoke onShipped --event-data '{"orderId":"o7"}'` delivers an event straight to one handler; `--event-type` overrides the type. It does not deliver Firebase Alerts on the native channel, so a function that listens for an alert never runs. ## Next - [Cloud Functions](https://firenook.dev/docs/services/functions): the handlers events reach. - [Extensions](https://firenook.dev/docs/services/extensions): Extensions that publish events. - [Connect your coding agent](https://firenook.dev/docs/tools/mcp): list, publish and replay events from your agent. # Security Rules > Load, reload and test Firestore and Storage Security Rules with Firenook, read coverage, see why a request was denied, and ask your agent. Firenook evaluates your Firestore and Storage Security Rules in its own engine, with no Java. Read this page to see how rules are loaded and reloaded, how to test them with `@firebase/rules-unit-testing`, and how to find out why a request was denied. ## Where the rules come from Firenook reads the rules files `firebase.json` names when it starts, as the official CLI does: ```json { "firestore": { "rules": "firestore.rules" }, "storage": { "rules": "storage.rules" } } ``` - **Firestore:** the `firestore` object's `rules`, or one file per database when `firestore` is an array ([named databases](https://firenook.dev/docs/services/firestore#named-databases)). - **Storage:** one file for every bucket, or one file per target for every bucket the target maps to ([buckets and targets](https://firenook.dev/docs/services/storage#buckets-and-targets)). One engine compiles and evaluates both. Storage takes version 1 and `rules_version = '2'` files, and its rules can call `firestore.get()` and `firestore.exists()` against the local Firestore. A request with no `Authorization` header is a signed-out client, and the Admin SDKs' owner token skips the rules, as on the official emulator. [Who a request is from](https://firenook.dev/docs/services/firestore#who-a-request-is-from) has the details. ## Reload on save Each rules file is watched. One second after the last change, Firenook compiles it and installs it: ```text firenook firestore: database "(default)" rules reloaded from ~/demo-shop/firestore.rules firenook storage: rules for every bucket reloaded from ~/demo-shop/storage.rules ``` A file that does not compile leaves the rules in force serving requests, and says where it went wrong: ```text firenook storage: rules for every bucket in ~/demo-shop/storage.rules did not compile; the rules in force are unchanged: 5:53: expected a rules expression ``` Tests can also replace the rules over HTTP. On the Firestore port, `PUT /emulator/v1/projects/:securityRules` replaces the rules of every database of the project, as on the official emulator. It reads the body as JSON whatever its content type, as the official emulator does, so it takes the rules `@firebase/rules-unit-testing` sends. On the Storage port, `PUT /internal/setRules` does the same for Storage. A body that does not compile there leaves no rules until the next valid one, as on the official emulator. ## Test your rules `@firebase/rules-unit-testing` runs against Firenook as it does against the official emulator: `initializeTestEnvironment` loads your rules, `authenticatedContext()` and `unauthenticatedContext()` are checked as a signed-in and a signed-out client, and `withSecurityRulesDisabled()` skips the rules. 1. Install the library and the web SDK next to Firenook: ```sh npm install --save-dev @firebase/rules-unit-testing firebase ``` 2. Write the rules. These let a customer create and read only their own orders, and upload only to their own folder: ```txt title="firestore.rules" rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /orders/{orderId} { allow read: if request.auth != null && resource.data.owner == request.auth.uid; allow create: if request.auth != null && request.resource.data.owner == request.auth.uid; } } } ``` ```txt title="storage.rules" rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /users/{uid}/{file} { allow read, write: if request.auth != null && request.auth.uid == uid; } } } ``` 3. Write the tests: ```js title="test/rules.test.mjs" import { readFileSync } from 'node:fs'; import { after, before, beforeEach, test } from 'node:test'; import { assertFails, assertSucceeds, initializeTestEnvironment, } from '@firebase/rules-unit-testing'; import { doc, getDoc, setDoc } from 'firebase/firestore'; import { ref, uploadString } from 'firebase/storage'; let env; before(async () => { // emulators:exec tells the library where the emulators are. env = await initializeTestEnvironment({ projectId: 'demo-shop', firestore: { rules: readFileSync('firestore.rules', 'utf8') }, storage: { rules: readFileSync('storage.rules', 'utf8') }, }); }); beforeEach(() => env.clearFirestore()); after(() => env.cleanup()); test('a customer creates and reads their own order', async () => { const ada = env.authenticatedContext('ada').firestore(); await assertSucceeds(setDoc(doc(ada, 'orders/o1'), { owner: 'ada', total: 42 })); await assertSucceeds(getDoc(doc(ada, 'orders/o1'))); }); test('a customer cannot create an order for someone else', async () => { const ada = env.authenticatedContext('ada').firestore(); await assertFails(setDoc(doc(ada, 'orders/o2'), { owner: 'grace' })); }); test("a customer cannot read another customer's order", async () => { await env.withSecurityRulesDisabled(async (admin) => { await setDoc(doc(admin.firestore(), 'orders/o3'), { owner: 'grace' }); }); const ada = env.authenticatedContext('ada').firestore(); await assertFails(getDoc(doc(ada, 'orders/o3'))); }); test('a signed-out visitor cannot read an order', async () => { await env.withSecurityRulesDisabled(async (admin) => { await setDoc(doc(admin.firestore(), 'orders/o4'), { owner: 'ada' }); }); const visitor = env.unauthenticatedContext().firestore(); await assertFails(getDoc(doc(visitor, 'orders/o4'))); }); test('uploads go only to your own folder', async () => { const ada = env.authenticatedContext('ada').storage(); await assertSucceeds(uploadString(ref(ada, 'users/ada/avatar.txt'), 'hi')); await assertFails(uploadString(ref(ada, 'users/grace/avatar.txt'), 'hi')); const visitor = env.unauthenticatedContext().storage(); await assertFails(uploadString(ref(visitor, 'users/ada/avatar.txt'), 'hi')); }); ``` 4. Run them under `emulators:exec`, which starts the emulators, tells the library where they are (`FIRESTORE_EMULATOR_HOST`, `FIREBASE_STORAGE_EMULATOR_HOST` and the hub's address), and stops when the tests end. Trimmed to the tests: ```console $ npx firenook emulators:exec --only firestore,storage "node --test test/rules.test.mjs" All emulators ready ✔ a customer creates and reads their own order (80.504375ms) [2026-10-22T11:41:38.513Z] @firebase/firestore: Firestore (13.1.0): GrpcConnection RPC 'Write' stream 0x127c9e7c error. Code: 7 Message: 7 PERMISSION_DENIED: Missing or insufficient permissions. false for 'create' @ L6 ✔ a customer cannot create an order for someone else (20.507625ms) ✔ a customer cannot read another customer's order (26.373375ms) ✔ a signed-out visitor cannot read an order (24.686708ms) ✔ uploads go only to your own folder (18.29025ms) ℹ tests 5 ℹ pass 5 ℹ fail 0 ``` The SDK logs the refused write, with the rule line that refused it; the test expected the refusal and passes. Two things the library does that surprise people, on the official emulator as on Firenook: `clearStorage()` deletes only the files at the bucket's root, not those in folders, and a test that expects a delete to be allowed fails with `storage/object-not-found` when the file was never uploaded. Upload it first, with the rules disabled. Rules tests exported from the console run the same way. [Run your tests](https://firenook.dev/docs/get-started/tests) covers test runners and CI. ## Coverage While the suite runs, Firenook records which rule expressions your requests evaluated, and how they came out. Open the report on the Firestore port: ```text http://127.0.0.1:8080/emulator/v1/projects/demo-shop:ruleCoverage.html ``` Leave out `.html` for the same report as JSON. Recording is on by default. The report can contain document data and decoded auth claims, so keep it local; `--no-diagnostics` turns recording off, and the coverage route then answers that it is unavailable. ## Find out why a request was denied ### The message A Firestore request the rules refuse fails with production's message and, on the next line, what refused it, as on the official emulator: | The rules | The message's second line | | --- | --- | | An allow statement that came out false | `false for 'get' @ L5`, the method and the line | | Several, each false | `false for 'get' @ L13, false for 'get' @ L14` | | No allow statement for the path | `No matching allow statements` | | A condition that failed while it ran | The error and the line, such as `field "uid" is not available on this value for 'get' @ L10` | The official emulator words a failed condition its own way (`Null value error.`, for one), so a test should not match that text. A signed-out read with the web SDK throws an error whose `code` is `permission-denied` and whose `message` is: ```text Missing or insufficient permissions. false for 'get' @ L5 ``` Over REST, the same read answers: ```json {"error":{"code":403,"message":"Missing or insufficient permissions.\nfalse for 'get' @ L5","status":"PERMISSION_DENIED"}} ``` Storage denials name no line, as on the official emulator. Over HTTP, a refused upload answers `403` with `{"error":{"code":403,"message":"Permission denied. No WRITE permission."}}`, and the SDK throws an error whose `code` is `storage/unauthorized` and whose `message` is: ```text Firebase Storage: User does not have permission to access 'users/grace/avatar.txt'. (storage/unauthorized) ``` ### The console The console's log has a line for every Firestore denial, with the rules file and line: ```text Rules denied get on orders/o1 for an unauthenticated client at firestore.rules:5 Rules denied create on orders/o9 for ada at firestore.rules:6 ``` A signed-in user's uid takes the place of "an unauthenticated client". The plain output (`--plain`, pipes and CI) does not print these lines; open the console's log to see them. Each service's Rules page, Firestore › Rules and Storage › Rules, lists every decision as it happens: the request, who made it, and the verdict. Open one and the lines that decided it are lit in the editor, with the value of each expression. On a denied read in the Firestore data view, "See why" explains it in place. The page's playground tries a request as any user against the rules in the editor, saved or not, and saved tests run again on every edit. [The console](https://firenook.dev/docs/tools/console) has the rest. ### Your coding agent `firenook mcp` gives an agent three rules tools: `rules_get` for the rules in force, `rules_list_denials` for recent denials with the lines that denied them, and `rules_evaluate` to ask whether a request as a given user would be allowed, with the deciding line and each expression's value. Nothing is written. [Connect your coding agent](https://firenook.dev/docs/tools/mcp) sets it up. ## Next - [Firestore](https://firenook.dev/docs/services/firestore): named databases and per-database rules. - [Cloud Storage](https://firenook.dev/docs/services/storage): rules per bucket. - [MCP tools](https://firenook.dev/docs/reference/mcp-tools): every tool the agent gets. # TypeScript functions > Compile TypeScript functions as you save with --build, see type errors in Firenook's log, and stop a test run when the build fails. A TypeScript codebase loads from what its compiler writes, not from your sources: `main` in `functions/package.json` is `lib/index.js`. So the code has to be compiled before it loads, and again after every save. With `--build`, Firenook runs your codebase's own compiler for you, in the same terminal as the emulators. This page shows what it does, what a type error looks like, and how it stops a test run. ## A TypeScript project `firenook init` writes a TypeScript codebase by default, from the official template. Without a terminal, `--yes` does the set-up too: it installs the codebase's packages and builds it once (`--no-pin` here leaves Firenook out of the project's `package.json`): ```console $ npx firenook init --language typescript --project demo-shop --yes --no-pin Firenook init: project demo-shop in ~/demo-shop wrote ~/demo-shop/firebase.json wrote ~/demo-shop/.firebaserc wrote ~/demo-shop/firestore.rules wrote ~/demo-shop/firestore.indexes.json wrote ~/demo-shop/storage.rules wrote ~/demo-shop/functions/package.json wrote ~/demo-shop/functions/tsconfig.json wrote ~/demo-shop/functions/src/index.ts wrote ~/demo-shop/functions/.gitignore wrote ~/demo-shop/.gitignore done Agents: .mcp.json for Claude Code (0.0 s) done Packages: npm install in functions/, 241 packages (2.3 s) done Build: tsc, into functions/lib (0.8 s) Next steps: npx firenook emulators:start --keep-data --build ``` Its scripts, in part of `functions/package.json`, are the ones `--build` runs: ```json { "main": "lib/index.js", "scripts": { "build": "tsc", "build:watch": "tsc --watch", "serve": "firenook emulators:start --only functions --build" } } ``` `firebase.json` gets a `predeploy` that runs `build`, as a deploy needs. [Start a new project](https://firenook.dev/docs/get-started/new-project) covers the rest of `init`. ## Compile as you save with --build ```sh npx firenook emulators:start --build ``` For each Functions codebase, Firenook runs its `build:watch` script beside the emulators, waits for the first compile, then loads the codebase. The compiler keeps running: save `src/index.ts`, the compiler writes `lib/`, and the codebase reloads. In the plain output: ```text Build: functions runs its build:watch script, and Firenook loads what it writes on every save build functions: compiled, no errors firenook: Starting emulators: auth, functions, firestore, pubsub, storage ``` In a terminal, the start-up card has a row for the compile: ```text │ Build ✓ 525 ms functions · build:watch │ ``` A save that changes `Hello from Firebase!` was answered with the new text 1.4 s later, with no restart: ```text 19:35:24 ↻ reloaded 276 ms 1 function · default ``` A codebase with a `build` script but no `build:watch` is built once, before the start. Firenook compiles nothing itself: the scripts are your project's, the same ones a deploy's `predeploy` runs, so what loads locally is what a deploy builds. ## Type errors A compile with errors puts them in the log, with where they are and what the compiler said. On the terminal screen: ```text 19:35:27 ✕ build 1 error · functions │ src/index.ts:6:9 Type 'string' is not assignable to type 'number'. 19:35:28 ↻ reloaded 195 ms 1 function · default 19:35:32 ✓ build no errors · functions 19:35:33 ↻ reloaded 179 ms 1 function · default ``` and in the plain output: ```text build functions: src/index.ts:6:9 TS2322: Type 'string' is not assignable to type 'number'. build functions: 1 error ``` `tsc` still writes `lib/` when there are type errors, so the code it wrote loads and answers, as the `reloaded` line after the error shows. The error is a warning to read, not a stop. Once the next save compiles cleanly, the log says `no errors`. ## Run your tests on a fresh build `emulators:exec --build` runs each codebase's `build` script once, before the emulators start, and runs the tests only when it succeeds: ```console $ npx firenook emulators:exec --build "npm test" Build: functions runs its build script before the emulators start build functions: src/index.ts:6:9 TS2322: Type 'string' is not assignable to type 'number'. build functions: build failed (exit code 2) Firenook: the build in functions failed (npm run build); the tests did not run ``` Trimmed to the build. The command exits with 1, so CI fails at the build, not at a test that ran stale code. When the build succeeds, the tests run as usual: ```console $ npx firenook emulators:exec --build "npm test" Build: functions runs its build script before the emulators start All emulators ready ✔ helloWorld answers (24.208875ms) ℹ tests 1 ℹ pass 1 ℹ fail 0 ``` Trimmed. [Run your tests](https://firenook.dev/docs/get-started/tests) covers test runners and CI. ## From the functions folder The `serve` script `init` writes starts only Functions, with `--build`. Run it from `functions/`: Firenook finds the project's `firebase.json` in the folder above, as the official CLI does. ```console $ cd functions && npm run serve Build: functions runs its build:watch script, and Firenook loads what it writes on every save build functions: compiled, no errors firenook: Starting emulators: functions ``` Trimmed. ## Running the compiler yourself Without `--build`, run the compiler in watch mode in another terminal, `npm run build:watch` in `functions/`, and start the emulators as usual. Firenook reloads the codebase whenever the compiler writes `lib/`, so a save reaches your functions either way. `--build` adds three things: - **One terminal.** The compiler starts and stops with the emulators. - **No stale start.** The codebase loads after the first compile, not from whatever `lib/` held before. - **Errors where you look.** Type errors show in Firenook's log and on its screen, next to the calls and reloads they affect. ## Next - [Debugging and reload on save](https://firenook.dev/docs/functions/debugging): reloads, a debugger and each call's logs. - [Cloud Functions](https://firenook.dev/docs/services/functions): the triggers Firenook delivers. # Scheduled functions > Firenook runs onSchedule functions on their own clock, in their time zone. Run one now from the CLI or the console, or pause it. Firenook runs your scheduled functions on time, as Cloud Scheduler does in production. This page explains when they run, how to run one now, how to pause one, and how time zones work. The official emulator never runs a schedule by itself: a scheduled function runs there only when you trigger it. In Firenook, a function that runs every minute does run every minute, and writes what it writes. Pause it while you work if you don't want that. ## A scheduled function ```js const {onSchedule} = require("firebase-functions/scheduler"); const logger = require("firebase-functions/logger"); exports.nightlyReport = onSchedule( {schedule: "every day 02:30", timeZone: "Europe/Paris"}, async () => { logger.info("nightly report ran"); }, ); exports.cleanup = onSchedule("every 1 minutes", async () => { logger.info("cleanup ran"); }); ``` ## How Firenook runs a schedule Each scheduled function gets a Pub/Sub topic named `firebase-schedule-`, as in the official emulator. On every tick, Firenook's scheduler publishes a message to that topic, and the function runs. The start-up screen counts the schedules on the Pub/Sub row: ```text Functions ✓ 213 ms 2 functions Pub/Sub ✓ 1 ms 2 schedules ``` So schedules run only while the Pub/Sub emulator runs. Your `firebase.json` needs an `emulators.pubsub` entry, and if you start with `--only`, the list must include `pubsub`. Without Pub/Sub nothing runs on time, but you can still run each function by hand. A schedule's clock starts when its function loads: - `every 5 minutes` runs five minutes after the start, then every five minutes. - A clock time, such as `every day 02:30`, runs at the next 02:30 in the schedule's time zone. - A reload keeps an unchanged schedule's next run. A changed schedule starts its clock again. - Runs that fall while Firenook is stopped are not made up. If your computer sleeps through several runs, the schedule runs once when it wakes, then carries on. Each run is a line in the terminal's log, like any other call: ```text 19:39:46 ✓ cleanup 12 ms schedule · scheduled tick │ cleanup ran ``` ## Schedule expressions Firenook reads the expressions Cloud Scheduler takes for Firebase: | Form | Examples | | --- | --- | | Every N minutes or hours | `every 5 minutes`, `every 15 mins`, `every 2 hours` | | Every N minutes or hours within a daily window | `every 2 hours from 10:00 to 14:00` | | Aligned to midnight | `every 15 mins synchronized` | | Every day at a time | `every day 02:30` | | Given weekdays at a time | `every monday 09:00`, `every mon,wed,fri 09:00` | | Cron, five fields | `0 9 * * 1-5` | When Firenook cannot run an expression on a clock, the function still loads, and you can still run it by hand. The plain log (`--plain`, or any pipe) says why: ```text firenook pubsub: schedule of us-central1-fastPoll not run automatically (unsupported Firebase schedule expression: every 30 seconds); run it from the console or through the Functions trigger route ``` In a terminal, it is one of the start-up warnings: the ready summary says `1 start-up warning · press w to show it`, and `w` shows it in the log: ```text 19:38:33 ! schedule of us-central1-fastPoll not run automatically (unsupported Firebase schedule expression: every 30 seconds); run it from the console or through the Functions trigger route ``` The console marks such a schedule **Not automatic**. ## Run one now From the command line, while the emulators run: ```console $ npx firenook functions:invoke nightlyReport --event-data '{}' Firenook: schedule event pubsub for nightlyReport (us-central1, gcfv2) via trigger key us-central1-nightlyReport-0 200 OK http://127.0.0.1:5001/functions/projects/demo-shop/triggers/us-central1-nightlyReport-0 ``` A schedule takes no data, so `--event-data '{}'` is enough. In 0.2.0 it is also needed: without it, `functions:invoke` sends a plain HTTPS request, which a scheduled function does not answer, and the command exits with 1: ```console $ npx firenook functions:invoke nightlyReport 404 Not Found http://127.0.0.1:5001/demo-shop/us-central1/nightlyReport Function us-central1-nightlyReport does not exist, valid functions are: us-central1-helloWorld, us-central1-addMessage, us-central1-onOrderCreated-0, us-central1-onUpload-0, us-central1-nightlyReport-0, us-central1-sendReceipt, us-central1-onShipped-0-projects/demo-shop/locations/us-central1/channels/firebase ``` In the [console](https://firenook.dev/docs/tools/console), open **Functions** and pick the function. Its Schedule tab shows the expression, the zone it runs in, the next five runs and the recent ones, with **Run now**, **Pause** and **Resume**. Run now sends the same tick as the clock, and works without Pub/Sub too. The list of functions shows each schedule's next run. A coding agent has the same controls in the `schedules_list` and `schedules_run` [MCP tools](https://firenook.dev/docs/reference/mcp-tools). Publishing a message to the function's topic, `firebase-schedule-`, also runs it, as it does in the official emulator. The terminal has no key that runs a schedule. ## Pause a schedule **Pause** on the Schedule tab stops a schedule's clock until you press **Resume**. Run now still works while it is paused. A pause lasts until Firenook stops: every start begins with every schedule running. ## Time zones Clock times and cron fields are read in the schedule's `timeZone`. Firenook carries its own copy of the IANA time-zone database, so `Europe/Paris` runs at the same moments on every machine, including containers that have no `/usr/share/zoneinfo`. Daylight saving follows the zone. A time the change skips runs right after the gap, and a time it repeats runs once, the first time. For `every day 02:30` in `Europe/Paris`, the next runs Firenook lists in UTC move by an hour when summer time ends on 25 October 2026: ```text 2026-10-24T00:30:00.000Z 2026-10-25T00:30:00.000Z 2026-10-26T01:30:00.000Z ``` Without `timeZone`, Firenook uses UTC. That is the production default for `onSchedule`. A first-generation `functions.pubsub.schedule()` without `.timeZone()` runs in `America/Los_Angeles` in production, but in UTC in Firenook, so name the zone when the hour matters. A zone that is not in the database stops the schedule from running on its own: ```text firenook pubsub: schedule of us-central1-marsReport not run automatically (time zone Mars/Olympus is not in this engine's time-zone database); run it from the console or through the Functions trigger route ``` Your function's code runs with `TZ=UTC`, so `new Date().getHours()` gives the UTC hour whatever zone the schedule is in. ## Next Give your functions their settings with [environment variables and secrets](https://firenook.dev/docs/functions/environment), or see how [Pub/Sub](https://firenook.dev/docs/services/pubsub) delivers the ticks. # Environment variables and secrets > Which .env files Firenook reads for your functions and in what order, how secrets and params get their values, and the variables the emulator sets. Your functions read their settings from `.env` files, secrets from `.secret.local`, and the emulators' addresses from variables Firenook sets. This page lists what Firenook reads, in which order, and what your code sees. ## The .env files Put the files in your codebase's folder, next to its `package.json`: ```ini SHOP_NAME=Demo Shop SUPPORT_EMAIL=help@example.com ``` Firenook reads these files, as the official CLI does. A later file wins over an earlier one for the same key: | File | What it is for | | --- | --- | | `.env` | Values for every project | | `.env.`, such as `.env.demo-shop` | Values for one project, by its id | | `.env.`, such as `.env.staging` | Values for one project, by its `.firebaserc` alias | | `.env.local` | Values for the emulators only, never deployed | The start says which files it loaded: ```text firenook functions: Loaded environment variables from .env, .env.demo-shop, .env.local. ``` When `firebase.json` gives the codebase a `configDir`, the `.env` files are read from that folder instead. ### The project id or its alias Which of `.env.` and `.env.` is read depends on how the project was chosen. With this `.firebaserc`: ```json { "projects": { "default": "demo-shop", "staging": "demo-shop-staging" } } ``` | You start with | Files read | | --- | --- | | `--project staging`, an alias | `.env`, `.env.staging`, `.env.local` | | No `--project`, after `firebase use staging` in this folder | `.env`, `.env.staging`, `.env.local` | | `--project demo-shop`, a project id | `.env`, `.env.demo-shop`, `.env.local` | | No `--project`, so the `default` alias | `.env`, `.env.demo-shop` or `.env.default`, `.env.local` | A project named by its id has no alias. Having both files for one project, its id's and its alias's, stops the codebase loading, as on the official emulator. Here with `.env.demo-shop-staging` and `.env.staging` and `--project staging`: ```text firenook functions: Failed to load function definition from source: FirebaseError: Can't have both dotenv files with projectId (env.demo-shop-staging) and projectAlias (.env.staging) as extensions. ``` Keep one of the two. Without `--project`, `.env.` and `.env.default` count as such a pair. The files use the same dialect as the official CLI: `KEY=value` lines, an optional `export` in front, `#` comments, double quotes with `\n` escapes that can span lines, and single quotes kept as written. A key starts with an uppercase letter or an underscore and holds only uppercase letters, digits and underscores. Keys that start with `X_GOOGLE_`, `FIREBASE_` or `EXT_`, and names the runtime uses, such as `PORT`, `K_SERVICE` and `GCLOUD_PROJECT`, are refused, and the codebase does not load: ```text firenook functions: Failed to load function definition from source: FirebaseError: Failed to load environment variables from .env. Validation failed Failed to validate key FIREBASE_REGION: Key FIREBASE_REGION starts with a reserved prefix (X_GOOGLE_ FIREBASE_ EXT_) ``` ## When you change a file Firenook watches the `.env` files and `.secret.local`, but a change does not reload your functions, as in the official CLI. Your functions keep the old values until the next reload. Press `r` in the terminal to reload, or use **Reload** in the console. Until you do, the codebase's page in the console names the file that changed after the last load. ## Secrets Declare a secret with `defineSecret` and bind it to the functions that use it: ```js const {onRequest} = require("firebase-functions/https"); const {defineSecret} = require("firebase-functions/params"); const stripeKey = defineSecret("STRIPE_KEY"); exports.checkout = onRequest({secrets: [stripeKey]}, (request, response) => { // stripeKey.value() is the value from .secret.local }); ``` Give it a value in `.secret.local`, in the codebase's source folder (never in `configDir`): ```ini STRIPE_KEY=sk_test_123 ``` Firenook never contacts Secret Manager. The official emulator tries your production secrets first when `.secret.local` lacks one; Firenook leaves a missing secret empty, and the SDK logs a warning when your code reads it: ```text No value found for secret parameter "STRIPE_KEY". A function can only access a secret if you include the secret in the function's dependency array. ``` `firenook init` writes a `functions/.gitignore` with `*.local`, which keeps `.env.local` and `.secret.local` out of Git. ## Params Params declared with `defineString`, `defineInt`, `defineBoolean`, `defineList` and the like take their values from the same `.env` files. Call `.value()` in your function to read one: ```js const {onRequest} = require("firebase-functions/https"); const {defineString, defineInt} = require("firebase-functions/params"); const greeting = defineString("GREETING", {default: "hello"}); const pageSize = defineInt("PAGE_SIZE", {default: 20}); exports.settings = onRequest((req, res) => { res.json({shop: process.env.SHOP_NAME, greeting: greeting.value(), pageSize: pageSize.value()}); }); ``` Firenook never asks for a value. It resolves params as the official emulator does when no terminal is attached: - A value in an `.env` file wins. - Otherwise the param takes its `default`, in `.value()` and in a function's options, such as `region`, `memory` or `timeoutSeconds`. The start names the params that took theirs: ```text firenook functions: Params no env file sets take their default: GREETING, PAGE_SIZE. ``` and the function answers with them: ```console $ curl http://127.0.0.1:5001/demo-shop/us-central1/settings {"shop":"Demo Shop","greeting":"hello","pageSize":20} ``` - A param with no value and no default stops its codebase loading, with the official question and where to put the answer. Here `defineString("API_URL")` has neither: ```text firenook functions: Failed to load function definition from source: FirebaseError: Question "Enter a string value for API_URL:" does not have a default and cannot be answered in non-interactive mode. Firenook never prompts: set the value in .env, .env.demo-shop or .env.local, or give the param a default. ``` When it is the only codebase, the start stops there. Two differences are deliberate. The official emulator writes the answers it took, defaults included, to `.env.local`; Firenook writes nothing and takes the default again at every load, so a changed default applies on the next reload. And a list param reaches your code as the JSON array the SDK's `.value()` parses, as a deployed function gets it. The console's Environment tab, on a function's page, lists every param with the file its value came from, marks the ones that took their default, and can write a value to `.env.local` for you. It also lists the secrets a function declares and can add a missing one to `.secret.local`. Code that reads `process.env` while it loads, outside a function, sees the `.env` values too, as on the official emulator: a `region` taken from `process.env.SHOP_REGION` is the same at load and in the function. Defaults and secrets are not there yet; the function has them. `PROJECT_ID`, `GCLOUD_PROJECT`, `STORAGE_BUCKET` and `DATABASE_URL` are built in: options that use them get the project's values. ## What your functions see Firenook starts one Node worker per codebase. Its environment is yours, with your `.env` values over it and the emulator's own variables over both. `FIREBASE_TOKEN` is removed, and `GOOGLE_APPLICATION_CREDENTIALS` and `CLOUDSDK_CONFIG` point at demo credentials in the run's folder, so your code never holds your Google credentials, and a token it asks for comes from the emulators. A `GOOGLE_APPLICATION_CREDENTIALS` your own `.env` sets is kept, and requests made with it go to Google. These are the variables Firenook sets, with the values a project on the default ports gets: | Variable | Value | | --- | --- | | `FUNCTIONS_EMULATOR` | `true` | | `FIRESTORE_EMULATOR_HOST` | `127.0.0.1:8080` | | `FIREBASE_AUTH_EMULATOR_HOST` | `127.0.0.1:9099` | | `FIREBASE_STORAGE_EMULATOR_HOST` | `127.0.0.1:9199` | | `STORAGE_EMULATOR_HOST` | `http://127.0.0.1:9199` | | `PUBSUB_EMULATOR_HOST` | `127.0.0.1:8085` | | `CLOUD_TASKS_EMULATOR_HOST` | `127.0.0.1:9499` | | `CLOUD_EVENTARC_EMULATOR_HOST` | `http://127.0.0.1:9299` | | `FIREBASE_EMULATOR_HUB` | `127.0.0.1:4400` | | `GCLOUD_PROJECT` | your project id, such as `demo-shop` | | `FIREBASE_CONFIG` | `{"storageBucket":"demo-shop.appspot.com","databaseURL":"https://demo-shop.firebaseio.com","projectId":"demo-shop"}` | | `TZ` | `UTC` | | `K_SERVICE`, `FUNCTION_TARGET` | the name of the function being called | The Admin SDK reads the `*_EMULATOR_HOST` variables, so `initializeApp()` with no arguments talks to the emulators. The ports follow your `firebase.json`. ## Next Change a value and see it in a call: [debugging and reload on save](https://firenook.dev/docs/functions/debugging) covers reloads, the debugger and the logs. # Debugging and reload on save > Reload on save and the r key, a debugger with --inspect-functions in Chrome or VS Code, and where to read each call's logs and errors. Firenook reloads your functions when you save, can hold a call at a breakpoint, and keeps every call's logs and errors. This page shows how to use each while you work on a function. ## Reload on save Firenook watches each codebase's folder. When you save a file, it waits until a second passes with no further change, then loads that codebase again in a new worker. The terminal says so: ```text 19:47:46 ↻ reloaded 229 ms 3 functions · default ``` `default` is the codebase. The watcher leaves out files and folders whose names start with a dot (so `.env` files too), `node_modules`, `venv`, `*.log` files, and the patterns in the codebase's `ignore` list in `firebase.json`. A reload that fails keeps the version that loaded before, so your app keeps working while you fix the error: ```text 19:47:49 ✕ reload failed 121 ms default │ SyntaxError: Unexpected token ';' functions/index.js:20 │ exports.oops = (; │ ^ │ still running the version loaded at 19:47:46 · open in console ``` Save the fix and the next reload takes over. For TypeScript, Firenook runs the compiled JavaScript your `package.json` points at. Start with `--build` and Firenook runs your compiler in watch mode beside the emulators, so every save is compiled and reloaded; [TypeScript functions](https://firenook.dev/docs/functions/typescript) has the details. If your sources change but the compiled files do not, the console's page for the codebase points it out. ### If a save seems to change nothing In 0.2.0, a save that stops a codebase loading, such as code that throws while it loads or a param with no default and no value, is reported only on the terminal screen. The plain output (`--plain`, pipes and CI) and the `--debug` log print nothing about it, and the version loaded before keeps answering, so a call still gets the old behaviour. The screen shows it: ```text 19:36:17 ✕ reload failed 117 ms default │ Functions codebase could not be analyzed successfully. It may have a syntax or runtime │ error │ Error: SHOP_REGION is not set │ still running the version loaded at 19:35:59 · open in console ``` When you run with `--plain`, open the codebase in the console's Functions page after a save: it says the codebase failed to load, with the error, and that the previous version is still serving. Calling the function you changed shows the same thing. ## Reload by hand Press `r` in the terminal to reload every codebase of yours, or use **Reload** on a codebase in the [console](https://firenook.dev/docs/tools/console). A reload by hand is how a change to a `.env` file or `.secret.local` reaches your functions, since Firenook does not reload on those ([environment variables and secrets](https://firenook.dev/docs/functions/environment)). ## Attach a debugger Start with `--inspect-functions` to open Node's inspector on each codebase's worker: ```sh npx firenook emulators:start --inspect-functions ``` The first worker listens on `127.0.0.1:9229`, the next on 9230, and so on. To choose the first port, give it: `--inspect-functions=9333`. With one codebase, the plain log names its port: ```text firenook functions: Using debug port 9229 for functions codebase default. firenook functions: Debugger listening on ws://127.0.0.1:9229/27e0016a-a53d-4dc1-8fa2-1d8dbc90e235 ``` Extensions take the first ports, then your codebases. So with an Extension in `firebase.json`, the port you give goes to the Extension, and your code gets the next one: ```text firenook functions: Using debug port 9229 for functions codebase myext. firenook functions: Using debug port 9230 for functions codebase default. You may need to add manually add this port to your inspector. firenook functions: Debugger listening on ws://127.0.0.1:9230/fbe26b0c-0897-4cef-a4f0-de7742ee7ef8 firenook extensions[myext]: Debugger listening on ws://127.0.0.1:9229/0b80ac38-04f5-4411-a598-d0bba6e30439 ``` Attach your debugger to the port the log gives `default` (or your codebase's name), here 9230. While the debugger is on, calls have no timeout, so a breakpoint can hold one for as long as you need. Without it, a call stops at the function's `timeoutSeconds`. A reload starts a new worker, and with it a new debugger session on the same port. After a reload, the new worker starts at the next call. Use a debugger that attaches again by itself. ### Chrome DevTools 1. Start the emulators with `--inspect-functions`. 2. Open `chrome://inspect` in Chrome. Under **Devices**, choose **Configure** and check that `127.0.0.1:9229` (or your port) is in the list. 3. Choose **Open dedicated DevTools for Node**. That window attaches to the worker and attaches again after each reload. 4. In **Sources**, open your function's file, set a breakpoint, and call the function from your app or with `npx firenook functions:invoke`. ### VS Code Add an attach configuration, start the emulators with `--inspect-functions`, then run it from the Run and Debug view: ```json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach to Firenook functions", "address": "127.0.0.1", "port": 9229, "restart": true, "skipFiles": ["/**"] } ] } ``` `"restart": true` makes VS Code attach again after every reload. With several codebases, add one configuration per port. ## Function logs ### In the terminal Each call is a line: the time, a tick or a cross, the function, how long it took and what called it. The lines your function logged follow it, with structured fields after the message: ```text 19:47:43 ✓ helloWorld 7 ms http · GET / · 200 │ Hello logs! structuredData true 19:47:43 ✕ brokenCheckout 11 ms http · POST / · 500 │ checking out order null │ Unhandled error TypeError: Cannot read properties of null (reading 'items') │ at functions/index.js:12:31 │ + 9 more lines · open in console ``` A failed call shows the error and the first line of the stack that is in your own code. **open in console** leads to the whole call. With `--plain`, or when the output goes to a pipe or CI, Firenook prints the official emulator's lines instead (the stack trimmed): ```text firenook functions: Beginning execution of "us-central1-brokenCheckout" firenook functions: {"order":null,"severity":"INFO","message":"checking out"} firenook functions: {"severity":"ERROR","message":"Unhandled error TypeError: Cannot read properties of null (reading 'items')\n at ~/demo-shop/functions/index.js:12:31\n ..."} firenook functions: Finished "us-central1-brokenCheckout" in 12ms ``` ### In the console **Functions › Invocations** in the console lists every call, live. Open one to see how it ended and why, its timing, the request or event it got, the response, its own log lines and the full stack of an error. **Replay** sends the same request again to the code loaded now; **Edit and replay** lets you change it first. Firenook keeps the last 2,000 calls. ### With --debug `--debug` writes Firenook's whole log, with times, to `.firenook/runs/session-*/firenook-debug.log`. In plain mode the start prints the file's path: ```text Debug log: ~/demo-shop/.firenook/runs/session-HRKIKS/firenook-debug.log ``` ```text [INFO] [2026-10-22T11:48:02.206861Z] [functions] functions: Beginning execution of "us-central1-brokenCheckout" [INFO] [2026-10-22T11:48:02.216076Z] [functions] functions: {"order":null,"severity":"INFO","message":"checking out"} [INFO] [2026-10-22T11:48:02.219083Z] [functions] functions: Finished "us-central1-brokenCheckout" in 12ms ``` Attach this file when you [report a problem](https://firenook.dev/docs/help/report). ## Read a failed call Start with the terminal: the error's message and the line in your code that threw. For the rest, open the call in the console. There you see the whole stack, the request that caused it and the response your app got, and after a fix you can replay the same request. A call that runs past its `timeoutSeconds` is stopped, and the caller gets a 500: ```text firenook functions[us-central1-quote]: Your function timed out after ~2s. To configure this timeout, see https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation. ``` ## Next See everything the terminal shows in [the terminal](https://firenook.dev/docs/tools/terminal), or trigger any function by hand with `functions:invoke`, described with [Cloud Functions](https://firenook.dev/docs/services/functions). # Keep data between runs > Keep Firestore, Auth and Storage between starts with --keep-data, where the data lives, how to stop cleanly, and how to start fresh. By default, every start of Firenook begins with empty data. Add `--keep-data` and Firestore, Auth and Storage pick up where you left them, with no export at the stop and no import at the start. This page explains where the data lives, what is kept, and how to start fresh. ## Start with --keep-data ```sh npx firenook emulators:start --keep-data ``` The first start begins empty and says where the data goes: ```text firenook 0.2.0 demo-shop · a fresh start Node 24.20.0 · data in .firenook/state ``` Stop with `q` or Ctrl-C, and Firenook saves the data before it exits: ```text 19:41:54 ✓ stopped 102 ms everything is kept in .firenook/state ``` Every later start picks the data up, says when it was saved, and counts what it holds: ```text firenook 0.2.0 demo-shop · picking up the data saved just now Node 24.20.0 · data in .firenook/state Firestore ✓ 32 ms 5 documents Auth ✓ 32 ms 1 user Storage ✓ 32 ms 1 object ``` The "saved" time is when the data's files were last written: `just now` under a minute, then `12 minutes ago`, `3 hours ago` and `2 days ago`. ## Without --keep-data Each start gets a new, empty working directory, `.firenook/runs/session-/state`, and says so: ```text firenook 0.2.0 demo-shop · a fresh start Node 24.20.0 · data in .firenook/runs/session-VLd3PD, for this run ``` ```text 19:42:21 ✓ stopped 63 ms this run's data stays in .firenook/runs/session-VLd3PD ``` Old runs stay on disk and are never loaded again. Firenook never deletes them, so remove old `session-*` folders yourself when you no longer need them. ## Where the data lives - .firenook/ - state/ data kept with --keep-data - firestore/ - firenook.redb the Firestore store - storage/ objects and their metadata - auth-state.json - native-state.json the receipt: project, buckets and seed - runs/ - session-Uj2lKW/ one folder per start, with its launch receipt - exports/ snapshots taken with the e key `firenook init` adds `.firenook/` to your `.gitignore`. The data is a working store, not an export: copy it to another machine as an [export](https://firenook.dev/docs/data/export) instead. ## What is kept | Kept | Not kept | | --- | --- | | Firestore documents | Pub/Sub topics, subscriptions and messages | | Auth users, tenants and the Auth configuration | Cloud Tasks queues and tasks | | Storage objects and their metadata | Paused schedules: every start runs them all | The official emulator keeps no Pub/Sub or Cloud Tasks data either. Because the receipt covers all three, `--keep-data` needs Firestore, Auth and Storage running together. Starting with `--only` and without one of them fails: ```text suite failed: --resume-state requires the firestore, auth and storage services (the native state receipt covers all three) ``` The start command `firenook init` writes uses `--keep-data`. If you set a project up without Auth or Storage, remove `--keep-data` from its `emulators` script, and keep data with an export instead ([export and snapshots](https://firenook.dev/docs/data/export)). ## Keep data from a seed `--keep-data` works with `--import`. The first start imports the seed into `.firenook/state`, every later start picks the data up, and the seed folder is never written: ```sh npx firenook emulators:start --import ./data --keep-data ``` [Seeds and imports](https://firenook.dev/docs/data/seeds) covers what happens when the seed changes. ## Several sets of data `--keep-data` is short for `--state-dir ./.firenook/state --resume-state`. Name another folder with `--state-dir` to keep separate data, for example one per seed or per experiment: ```sh npx firenook emulators:start --state-dir ./.firenook/checkout-tests --resume-state ``` The state folder must not hold your seed, your export folder or the project itself. Without `--resume-state`, Firenook refuses a folder that already holds data, rather than reset it or import into it again: ```text Firenook: Existing nonempty state requires explicit --resume-state (or --keep-data); never silently reimport or reset it ``` Only one Firenook can use a state folder at a time. A second start on the same folder stops with `in use by another Firenook`; see [recovery](https://firenook.dev/docs/data/recovery). ## Stop cleanly Press `q` or Ctrl-C in the terminal. Firenook stops the emulators, saves the data and closes its files, then exits: ```text ⠋ stopping 958 ms saving your data ``` Closing the terminal window stops it the same way. In a script, send `SIGINT` or `SIGTERM` to the `firenook` process and wait for it to exit. A `firenook` command ended with `kill -9` stops its engine too: the engine saves the data, closes it and frees the ports within a moment, and the next start picks the data up as after any stop. Killing the engine itself is different: Firestore loses nothing, but the next start checks the store and repairs it if it needs to. See [recovery after a crash](https://firenook.dev/docs/data/recovery). ## Start fresh Firenook never deletes kept data. To start again from nothing, stop Firenook, then move or delete the folder: ```sh rm -rf .firenook/state ``` Or keep the old data and start a new set beside it with another `--state-dir`. To keep a copy you can load anywhere, take an [export](https://firenook.dev/docs/data/export) first. ## Earlier releases Data an earlier release kept opens in 0.2.0 as it is. On that first start, 0.2.0 builds its ordered field index once, with one read of every document: 7.4 s for 211,202 documents, and 49 s for 2.7 million, on the benchmark server. The plain log says when it is done: ```text firenook: indexed the top-level fields of 1000 Firestore documents in 0.0 s ``` Every start after that opens the data at once. Data saved by 0.2.0 opens in 0.2.0-next.8, which rebuilds its own index the same way. Data kept without a seed needs 0.2.0-next.7 or later: 0.2.0-next.6 refuses it and changes nothing. ## Next Start your kept data from an export with [seeds and imports](https://firenook.dev/docs/data/seeds). # Seeds and imports > Start from an export with --import, keep a seed's data with --keep-data, and what Firenook does when the seed changes after it was imported. A seed is a folder of data in the official export format that your project starts from: test users, a few products, the files your app expects. This page shows how Firenook imports a seed, how a seed works with `--keep-data`, and what happens when you change the seed. ## Import an export `--import` takes a folder written by `firenook emulators:export`, by `--export-on-exit`, or by the official emulator: ```sh npx firenook emulators:start --import ./data ``` The folder holds `firebase-export-metadata.json` and one folder per service. A seed with only some services, for example only `firestore_export`, imports those and starts the others empty. The start counts what it imported: ```text firenook imported 5 Firestore documents firenook imported 1 Auth user firenook imported 1 Storage object ``` Without `--keep-data`, every start imports the seed into a fresh run, and your changes are gone at the next start unless you export them ([export and snapshots](https://firenook.dev/docs/data/export)). A folder with no `firebase-export-metadata.json` is skipped with a warning, as the official emulator skips it, and the start goes on with no data: ```text firenook: Could not find import/export metadata file, skipping data import! (--import ~/demo-shop/data) ``` In a terminal the warning is one of the start-up warnings, which `w` shows. A folder that does not exist stops the start, unless `--export-on-exit` will write it: ```text suite failed: Directory "~/demo-shop/does-not-exist" does not exist. ``` ## A seed with --keep-data Add `--keep-data` and the seed is imported once: ```sh npx firenook emulators:start --import ./data --keep-data ``` The first start imports it into `.firenook/state`: ```text firenook 0.2.0 demo-shop · first start, importing the seed "data" Node 24.20.0 · data in .firenook/state Firestore ✓ 104 ms 5 documents imported Auth ✓ 104 ms 1 user imported Storage ✓ 104 ms 1 object imported ``` Every later start with the same command picks up the data where you left it: `picking up the data saved 5 minutes ago`. Firenook never writes to the seed folder, so the seed stays the same however much the data changes. Kept data remembers how it began. Data started from a seed needs the same `--import` at every start, and a start without it is refused: ```text Problem it was imported from ~/demo-shop/data: pass --import ~/demo-shop/data to keep using it, so Firenook will not pick up .firenook/state Fix to keep using it, start with its seed again: --import data Data nothing was deleted: .firenook/state is untouched ``` Data started without a seed never takes one. Here, data kept in `.firenook/seedless` without a seed, started with `--import ./data`: ```text Problem this data was started without a seed, so it cannot take the seed data: start without --import to keep using it, so Firenook will not pick up .firenook/seedless Fix to keep using it, start without --import; to start from the seed, give it a new --state-dir Data nothing was deleted: .firenook/seedless is untouched ``` To try a seed next to data you already keep, import it into a new folder: ```sh npx firenook emulators:start --import ./data --state-dir ./.firenook/from-seed --resume-state ``` ## When the seed changes At every start, Firenook checks that the seed is the one it imported. It compares each file's size and times with a record it made at the import, `native-seed.json` beside the data, and reads only a file whose details changed. A file you only touched, with the same content, does not count. Neither do the files operating systems write into folders they show: `.DS_Store`, `._*`, `Thumbs.db` and `desktop.ini`. A seed whose content changed stops the start, names the files, and deletes nothing: ```text Saved data ✕ 0 ms refused Problem the seed data changed after it was imported, so Firenook will not pick up .firenook/state Changed auth_export/accounts.json changed Fix to import the changed seed, start with a new --state-dir; this one stays as it is to keep this data with the seed as it is now: firenook state:adopt-seed --state-dir .firenook/state --import data Data nothing was deleted: .firenook/state is untouched ``` You have two ways on. **Import the changed seed.** Start with a new `--state-dir`. The new folder gets the seed as it is now; the old one stays as it was. **Keep your data.** Stop Firenook, then record the seed as it is now: ```console $ npx firenook state:adopt-seed --state-dir .firenook/state --import data Recorded ~/demo-shop/data as the seed of ~/demo-shop/.firenook/state (9 files). The saved data is as it was; the previous receipt is at ~/demo-shop/.firenook/state/native-state.json.before-adopt-1792596731. ``` `state:adopt-seed` does not import the change. The kept data stays exactly as it was; only the record of the seed is replaced. Use it when the change does not matter to the data you have, such as a file you saved again or a tidied export. A seed edited while Firenook imports it is refused too, and the import does not count. Data saved by 0.2.0-next.4 or earlier has no file record yet: its seed is read in full once, at the next start, and then recorded. ## Exports next to a seed `--export-on-exit` with no folder writes back into the `--import` folder, as in the official CLI. With `--keep-data` that would change the seed, so Firenook refuses it: ```text Firenook: Resume exports must be separate from the immutable seed ``` Name another folder, such as `--export-on-exit ./snapshot`, or take a snapshot while the emulators run. [Export and snapshots](https://firenook.dev/docs/data/export) shows both. ## Next Turn your kept data back into a seed with [export and snapshots](https://firenook.dev/docs/data/export). # Export and snapshots > Export Firestore, Auth and Storage in the official format, while running or at the stop, take snapshots of kept data, and move data to the official emulator. An export is a folder of your data in the official emulator's format. Use one to make a seed, to keep a snapshot of data you work on, or to take your data to the official emulator and back. Firenook exports Firestore, Auth and Storage. ## Export while the emulators run `emulators:export` asks the running suite to write its data to a folder: ```console $ npx firenook emulators:export ./exports/before-migration Found running emulator hub for project demo-shop at http://127.0.0.1:4400; exporting firestore, auth, storage to ~/demo-shop/exports/before-migration Export complete: ~/demo-shop/exports/before-migration ``` It needs a running suite, as the official command does. With the emulators stopped, it says so: ```text Firenook: Did not find a running emulator hub for project demo-shop (tried http://127.0.0.1:4400): ECONNREFUSED. Start it with firenook emulators:start. ``` The command finds the suite by its project id, through the same hub file the official CLI uses. When two suites run with one project id, such as two Git worktrees, it reaches the one started from the folder you run it in. Elsewhere it asks for `--hub-port`; see [troubleshooting](https://firenook.dev/docs/help/troubleshooting#a-command-reaches-the-wrong-emulators). | Option | What it does | | --- | --- | | `--only firestore,auth,storage` | Exports only the services you name | | `--force` | Writes into a folder that holds other files | | `--json` | Prints the result as one JSON line | | `-P`, `--project ID` | The project, when you are not in its folder | | `--hub-port PORT` | The suite whose hub listens on this port, and no other | The destination may not be the project folder, the current folder or a parent of either. A folder that holds files but no `firebase-export-metadata.json` needs `--force`: ```text Firenook: ~/demo-shop/notempty is not empty and holds no firebase-export-metadata.json; re-run with --force to overwrite its contents ``` A folder that already holds an export is written over without `--force`. ## Export at the stop `--export-on-exit` writes an export when you stop Firenook: ```sh npx firenook emulators:start --import ./data --export-on-exit ./data ``` Without a folder, it writes back into the `--import` folder, as in the official CLI, so `--import ./data --export-on-exit` keeps `./data` up to date. Without `--import`, name the folder: ```text Firenook: --export-on-exit requires --import or an explicit destination ``` The stop takes as long as the export, and the terminal says where it went: ```text 19:47:00 ✓ stopped 90 ms exported to snapshot ``` ## Snapshots of kept data Data kept with `--keep-data` is a working store, not an export. To take a copy you can load anywhere, use any of these while the emulators run: - Press `e` in the terminal. The snapshot goes to `.firenook/exports/-