firebase.json Explained: Hosting, Functions, Firestore Rules, and Emulators

Last updated:

firebase.json is the Firebase CLI's project configuration file. It is generated by firebase init and lives at the project root alongside .firebaserc. Its top-level keys map directly to Firebase services — hosting, functions, firestore, storage, and emulators — and each block tells the CLI how to build, package, and deploy that service. The split with .firebaserc is simple but load-bearing: firebase.json sets policy (rewrites, headers, function source dir, rules-file paths); .firebaserc says which Firebase project to deploy to.

Need to validate a firebase.json file? Paste it into Jsonic's JSON Validator — it pinpoints syntax errors with line and column numbers before firebase deploy fails.

Validate firebase.json

firebase.json vs .firebaserc: policy vs project mapping

Both files are generated by firebase init at the project root, both are committed to git, and both are read on every CLI invocation. They split cleanly: firebase.json is policy, .firebaserc is addressing.

FileAnswersContentsChanges per environment?
firebase.jsonHow to deployhosting rewrites, function source, rules paths, emulator portsRarely — same policy across staging/prod
.firebasercWhere to deployProject ID aliases (default, staging, production) and target mappingsYes — each alias points at a different project ID
// .firebaserc
{
  "projects": {
    "default": "my-app-dev",
    "staging": "my-app-staging",
    "production": "my-app-prod"
  },
  "targets": {
    "my-app-prod": {
      "hosting": {
        "marketing": ["my-marketing-site"],
        "app": ["my-app-site"]
      }
    }
  }
}

Switch the active project with firebase use staging; the same firebase.json is reused. Secrets never go in either file — use firebase functions:secrets:set or environment variables instead.

hosting: public dir, rewrites, redirects, headers, cleanUrls

The hosting block is the most-edited section of firebase.json. It declares which directory ships to the Firebase Hosting CDN, how URLs map to files or functions, what headers wrap each response, and which paths to ignore on upload.

{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "cleanUrls": true,
    "trailingSlash": false,
    "rewrites": [
      { "source": "/api/**", "function": "api" },
      { "source": "**", "destination": "/index.html" }
    ],
    "redirects": [
      { "source": "/old-path", "destination": "/new-path", "type": 301 }
    ],
    "headers": [
      {
        "source": "**/*.@(js|css|woff2|png|jpg|svg)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "**/*.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache" }
        ]
      }
    ]
  }
}

Order matters. rewrites, redirects, and headers are all evaluated in array order; the first match wins. Static files in public/ are always served before any rewrite triggers, so a real /robots.txt file beats a catch-all to index.html.

  • public — the build output directory uploaded to the CDN
  • cleanUrls: true — strips .html from URLs (so /about.html serves at /about)
  • trailingSlash: false — canonicalizes URLs without trailing slashes
  • rewrites[].function — proxies a path to a Cloud Function
  • rewrites[].run — proxies a path to a Cloud Run service
  • redirects[].type — HTTP status, usually 301 (permanent) or 302 (temporary)

functions: source, ignore, predeploy, codebase (for monorepos)

The functions block tells the CLI where your Cloud Functions source lives, how to build it before upload, and which files to skip. For monorepos with multiple function bundles, switch from an object to an array and set a codebase key on each entry.

// Single-codebase functions
{
  "functions": {
    "source": "functions",
    "runtime": "nodejs20",
    "ignore": ["node_modules", ".git", "*.log"],
    "predeploy": [
      "npm --prefix functions run lint",
      "npm --prefix functions run build"
    ]
  }
}

// Multi-codebase (monorepo)
{
  "functions": [
    {
      "source": "apps/api",
      "codebase": "api",
      "runtime": "nodejs20",
      "predeploy": ["npm --prefix apps/api run build"]
    },
    {
      "source": "apps/jobs",
      "codebase": "jobs",
      "runtime": "nodejs20",
      "predeploy": ["npm --prefix apps/jobs run build"]
    }
  ]
}

source is the directory containing the package.json for your functions. predeploy commands run locally before upload — if any command exits non-zero the deploy aborts. Use codebase to keep multiple function bundles in one project; deploy individual codebases with firebase deploy --only functions:api.

firestore: rules file, indexes file

The firestore block is small — it just points at two sibling files that hold the actual content. rules points at your security rules in the Firebase rules DSL; indexes points at a JSON file describing composite indexes.

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  }
}

firestore.rules is NOT JSON — it is the Firebase Security Rules language with service cloud.firestore at the top and match /databases/{database}/documents blocks inside. firestore.indexes.json IS JSON and lists composite indexes the CLI creates on deploy. Generate index entries automatically by running queries against the emulator — the emulator logs the index JSON to paste into this file.

For projects with multiple Firestore databases (Firestore Multi-database, GA in 2024), change firestore to an array and add a database key to each entry.

{
  "firestore": [
    { "database": "(default)", "rules": "firestore.rules", "indexes": "firestore.indexes.json" },
    { "database": "analytics", "rules": "analytics.rules", "indexes": "analytics.indexes.json" }
  ]
}

storage: rules file, single vs multiple buckets

The storage block points at a rules file for Cloud Storage. Like Firestore rules, the rules file uses the Firebase Security Rules language, not JSON.

// Single bucket (default)
{
  "storage": {
    "rules": "storage.rules"
  }
}

// Multiple buckets
{
  "storage": [
    { "bucket": "my-app.appspot.com", "rules": "storage.rules" },
    { "bucket": "my-app-uploads", "rules": "uploads.rules" }
  ]
}

Buckets must already exist in Google Cloud Storage; the CLI does not create them. The default bucket created with a Firebase project is <project-id>.appspot.com and is used when storage is an object without a bucket key.

emulators: local development with auth, firestore, functions, hosting

The emulators block is a single top-level object that configures every emulator at once. There is one block — not one per service — and each service gets a sub-key with its port.

{
  "emulators": {
    "auth":      { "port": 9099 },
    "functions": { "port": 5001 },
    "firestore": { "port": 8080 },
    "database":  { "port": 9000 },
    "hosting":   { "port": 5000 },
    "storage":   { "port": 9199 },
    "pubsub":    { "port": 8085 },
    "ui":        { "enabled": true, "port": 4000 },
    "singleProjectMode": true
  }
}
EmulatorDefault portConnect from client SDK
auth9099connectAuthEmulator(auth, "http://127.0.0.1:9099")
firestore8080connectFirestoreEmulator(db, "127.0.0.1", 8080)
functions5001connectFunctionsEmulator(fn, "127.0.0.1", 5001)
database (RTDB)9000connectDatabaseEmulator(db, "127.0.0.1", 9000)
hosting5000open http://127.0.0.1:5000
storage9199connectStorageEmulator(storage, "127.0.0.1", 9199)
pubsub8085set PUBSUB_EMULATOR_HOST=127.0.0.1:8085
UI4000open http://127.0.0.1:4000

Start all configured emulators with firebase emulators:start. Persist Firestore and Auth state across runs with firebase emulators:start --import=./seed --export-on-exit=./seed. When your client SDK is not pointed at the emulator host it hits production — gate the emulator connect calls behind process.env.NODE_ENV !== "production".

Multi-site hosting: deploying multiple targets from one project

A single Firebase project can host multiple sites — useful for splitting a marketing site, a docs site, and the app under the same Auth and Firestore backend. The Firebase Console creates sites; the CLI binds them to local targets.

# 1. Create sites in the Firebase Console under Hosting
# 2. Apply local target names
firebase target:apply hosting marketing my-marketing-site
firebase target:apply hosting app my-app-site

# 3. firebase.json becomes an array of hosting configs
{
  "hosting": [
    {
      "target": "marketing",
      "public": "marketing/dist",
      "cleanUrls": true,
      "rewrites": [{ "source": "**", "destination": "/index.html" }]
    },
    {
      "target": "app",
      "public": "app/dist",
      "rewrites": [
        { "source": "/api/**", "function": "api" },
        { "source": "**", "destination": "/index.html" }
      ]
    }
  ]
}

firebase target:apply writes the target mapping into .firebaserc, NOT firebase.json. That is why the same firebase.json works across projects — each project applies its own site IDs. Deploy a single target with firebase deploy --only hosting:marketing.

Key terms

firebase.json
The Firebase CLI's project configuration file. Strict JSON, lives at the project root, generated by firebase init. Defines policy for every Firebase service you deploy.
.firebaserc
The Firebase CLI's project alias file. Maps local names (default, staging, production) to Firebase project IDs, and stores hosting target mappings created by firebase target:apply.
Hosting rewrite
A rule in hosting.rewrites that maps an incoming URL pattern to a destination — either a file (typically /index.html for SPAs), a Cloud Function, or a Cloud Run service. Evaluated in array order; first match wins.
Security rules
The access-control language used by Firestore, Realtime Database, and Cloud Storage. Written in the Firebase Rules DSL (not JSON) in files like firestore.rules; firebase.json only points at the file paths.
Emulator
A local process that mimics a Firebase service for development and testing. The Firebase Local Emulator Suite ships emulators for Auth, Firestore, Functions, RTDB, Hosting, Storage, and Pub/Sub. Configured under the single emulators object in firebase.json.
Deploy target
A local alias for a hosting site or other resource, applied with firebase target:apply. Lets firebase.json reference resources by stable names while .firebaserc maps the names to per-project IDs.

Frequently asked questions

What is the difference between firebase.json and .firebaserc?

firebase.json holds deploy policy: which directory to serve from hosting, where your Cloud Functions source lives, which file contains your Firestore security rules, which ports the emulators bind to, and so on. .firebaserc holds project mapping: which Firebase project ID to deploy to under aliases like default, staging, and production. The CLI reads both — firebase.json answers "how to deploy" and .firebaserc answers "where to deploy". Both live at the project root. You commit both to git, but secrets never go in either file. Switching projects is firebase use staging; switching what gets deployed is editing firebase.json. Treat firebase.json as the contract for your build/deploy pipeline and .firebaserc as the address book.

How do I configure Firebase Hosting rewrites for a SPA?

A single-page app needs every unknown path to fall through to index.html so the client-side router can handle it. In firebase.json under hosting, set rewrites to [{"source": "**", "destination": "/index.html"}]. The double-asterisk glob matches every path that did not resolve to a real file in your public directory. Put more specific rewrites first (for example /api/** to a Cloud Function) and the catch-all last — Firebase Hosting evaluates rewrites in array order and uses the first match. Combine with "cleanUrls": true to strip .html extensions from your asset paths, and "trailingSlash": false to canonicalize URLs. Static files in public/ always win over rewrites, so a real /robots.txt is served before the SPA catch-all triggers.

How do I run Firebase locally with emulators?

Run firebase init emulators once to pick which services to emulate (auth, firestore, functions, hosting, storage, pubsub, database). The CLI writes an emulators block into firebase.json with default ports — auth on 9099, firestore on 8080, functions on 5001, hosting on 5000, storage on 9199, plus a UI on 4000. Start them all with firebase emulators:start. Add --import=./seed --export-on-exit=./seed to persist Firestore and Auth state across runs. The single emulators object in firebase.json controls all per-service ports and the bundled UI. Your client SDKs need to be pointed at the emulator hosts with connectFirestoreEmulator and friends, otherwise they will hit production. The emulators do not require a network connection once installed.

Can firebase.json have comments?

No — firebase.json is strict JSON (RFC 8259), so // and /* */ comments are syntax errors that break the Firebase CLI parser. There is no officially-recognized comment key like npm’s "//". The standard workarounds are: keep explanatory notes in a README.md next to firebase.json, add a top-level "comment" field (the CLI ignores unknown top-level keys but warns on some), or split configuration into multiple referenced files (firestore.rules, storage.rules, .firebaserc) where the formats that allow comments do so. If you find yourself wanting comments inside firebase.json, that is usually a signal to factor logic out into rules files, predeploy scripts, or named hosting targets. See our JSON Comments guide for the full set of workarounds across config formats.

How do I add cache headers in firebase.json?

Inside the hosting block, add a headers array. Each entry has a source glob and a list of header objects with key + value. To cache hashed asset files aggressively but keep HTML fresh, use two entries — one matching **/*.@(js|css|png|jpg|svg|woff2) with Cache-Control: public, max-age=31536000, immutable, and one matching **/*.html with Cache-Control: no-cache. Firebase Hosting applies the first matching rule per request, so order matters. Headers are evaluated against the request path after rewrites are resolved. You can also set security headers here: Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, Referrer-Policy. Changes take effect on the next firebase deploy --only hosting; the CDN reads the response headers when serving from origin.

What does the predeploy hook do?

predeploy is an array of shell commands the Firebase CLI runs locally before uploading anything. It exists on hosting, functions, firestore, and storage blocks. The most common use is "predeploy": ["npm --prefix functions run build"] inside the functions block to compile TypeScript before the CLI zips and uploads the dist directory. For hosting, predeploy typically runs your framework build: ["npm run build"]. If any command exits non-zero the deploy aborts before any change reaches Firebase, which makes it the right place to put type-checks, lint, and tests that must pass to ship. There is also a postdeploy hook with the same shape that runs after a successful deploy — useful for cache purges or release notifications. Both are run with the project root as the working directory.

How do I deploy only the functions and not hosting?

Use firebase deploy --only functions. The --only flag accepts a comma-separated list of services and matches the top-level keys in firebase.json — hosting, functions, firestore, storage, database, remoteconfig, extensions. You can narrow further: --only functions:api,functions:webhooks deploys just two named Cloud Functions, and --only hosting:marketing deploys only one named hosting target. The inverse flag --except hosting deploys everything except hosting. Combining these is how CI pipelines deploy backend changes independently of the static site, and how teams stage rollouts. The CLI still reads the entire firebase.json on every deploy, but only the matched sections are uploaded. Predeploy hooks of unaffected sections do not run.

Can I have multiple hosting sites in one Firebase project?

Yes. Create additional sites in the Firebase Console under Hosting, then map each to a local target name with firebase target:apply hosting marketing my-marketing-site. In firebase.json change the hosting key from an object to an array of objects, each with its own "target" property plus per-site public, rewrites, and headers. The CLI writes the target mapping to .firebaserc, not firebase.json, so the same firebase.json works across projects as long as each project applies its own targets. Deploy a single site with firebase deploy --only hosting:marketing. Multi-site is how teams keep a marketing site, an app, and a docs site under one project with shared Auth and Firestore — three deploys, three URLs, one Firebase project ID.

Further reading and primary sources