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.
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.
| File | Answers | Contents | Changes per environment? |
|---|---|---|---|
firebase.json | How to deploy | hosting rewrites, function source, rules paths, emulator ports | Rarely — same policy across staging/prod |
.firebaserc | Where to deploy | Project ID aliases (default, staging, production) and target mappings | Yes — 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 CDNcleanUrls: true— strips.htmlfrom URLs (so/about.htmlserves at/about)trailingSlash: false— canonicalizes URLs without trailing slashesrewrites[].function— proxies a path to a Cloud Functionrewrites[].run— proxies a path to a Cloud Run serviceredirects[].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
}
}| Emulator | Default port | Connect from client SDK |
|---|---|---|
| auth | 9099 | connectAuthEmulator(auth, "http://127.0.0.1:9099") |
| firestore | 8080 | connectFirestoreEmulator(db, "127.0.0.1", 8080) |
| functions | 5001 | connectFunctionsEmulator(fn, "127.0.0.1", 5001) |
| database (RTDB) | 9000 | connectDatabaseEmulator(db, "127.0.0.1", 9000) |
| hosting | 5000 | open http://127.0.0.1:5000 |
| storage | 9199 | connectStorageEmulator(storage, "127.0.0.1", 9199) |
| pubsub | 8085 | set PUBSUB_EMULATOR_HOST=127.0.0.1:8085 |
| UI | 4000 | open 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 byfirebase target:apply. - Hosting rewrite
- A rule in
hosting.rewritesthat maps an incoming URL pattern to a destination — either a file (typically/index.htmlfor 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.jsononly 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
emulatorsobject infirebase.json. - Deploy target
- A local alias for a hosting site or other resource, applied with
firebase target:apply. Letsfirebase.jsonreference resources by stable names while.firebasercmaps 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
- Firebase Hosting — Configure hosting behavior — Authoritative reference for the hosting block: rewrites, redirects, headers, cleanUrls
- Firebase CLI Reference — firebase init, firebase deploy --only, firebase use, and every CLI flag
- Firebase Local Emulator Suite — Installing, configuring, and connecting clients to the emulator suite
- Firestore Security Rules — The rules DSL referenced by firestore.rules
- Firebase Hosting deploy targets — Multi-site hosting and firebase target:apply