# 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/<project>: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.
