Skip to content

Cloud Storage

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.

Storage starts when firebase.json has a storage section or an emulators.storage entry:

firebase.json
{
"storage": { "rules": "storage.rules" },
"emulators": {
"storage": { "port": 9199 }
}
}

Connect the client SDK and the Admin SDK the usual way:

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 <project>.appspot.com
[{ "target": "uploads", "rules": "uploads.rules" }, …] A rules file per target, for every bucket the target maps to (see buckets and targets)
No section A demo- project gets open default rules; a real project id does not start, as on the official emulator
  • 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.

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/:

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;
}
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:

Terminal window
$ 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:

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.

Each rules file is watched and reloaded when you save it, whether or not Firestore runs:

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 covers testing them.

To give buckets their own rules, list targets in firebase.json and map each target to its buckets in .firebaserc:

firebase.json
{
"storage": [
{ "target": "uploads", "rules": "uploads.rules" },
{ "target": "public", "rules": "public.rules" }
]
}
Terminal window
$ 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:

{"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:

Terminal window
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:

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.

  • 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.