Supabase

Fix "Assertion failed: resolving hosting target" (Firebase)

firebase deploy says this "should have caused an error earlier". With a single hosting object and a dead login it did, and the CLI threw it away. Both causes, reproduced on firebase-tools 15.32.1, and how to tell them apart.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

TL;DR#

The message has two different causes, and firebase login --reauth, the most upvoted answer, fixes only one of them:

  • hosting in firebase.json is an array and one entry has neither "site" nor "target". The CLI uses array entries exactly as written, so this fails with any login. Give every entry a site or a target, or delete the duplicate entry. This is the case in the original question.
  • hosting is a single object without "site", and the CLI could not look up the project's default Hosting site, for example because the stored login has expired. Since firebase-tools 12.9.0, firebase deploy drops most failures of that lookup (on 15.32.1 it reacts only to an HTTP 403 and to a project with no default site) and stops on this assertion instead. Re-authenticate.

To tell them apart, check the shape of hosting first: the CLI runs the default-site lookup for every Hosting deploy, so with a dead login firebase-debug.log shows the same 401 for an array as for a single object. With a single object, an HTTP 401 on the firebase.googleapis.com project lookup in that file means the login.

text
Error: Assertion failed: resolving hosting target of a site with no site name or target name. This should have caused an error earlier

Two configs, one message#

The Stack Overflow question (24 votes, more than 15,000 views) is an Angular app deploying Hosting together with Realtime Database, Firestore and Storage rules to a live project, nur-beta. Its most upvoted answer is firebase login --reauth; the accepted one says the JSON is fine and that the app must be built before deploying. Neither matches the asker's file.

The runs quoted below were reproduced on 6 October 2026 with firebase-tools 15.32.1 on Node 22.23.3 (official node:22-alpine Docker image), plus firebase-tools 12.8.1 and 12.9.0 for the version history; the messages that only appear on paths the reproduction could not reach come from the CLI source and the upstream issues, linked where they are cited. No Google account was involved. The login used is a stored session whose refresh token Google's token endpoint rejects with invalid_grant, which is what an expired or revoked firebase login looks like to the CLI; every 400 and 401 quoted here is Google's real answer to it. A successful re-login could not be part of the reproduction.

Why it happens#

The message is raised in resolveTargets() in src/hosting/config.ts. Before deploying, the CLI turns every hosting config into a concrete site: a site written in firebase.json is used as it is, a target is translated through the targets block of .firebaserc, and a config with neither reaches the line that throws this assertion, with exit code 2. How a config can get there depends on the shape of hosting.

An array entry with no site and no target#

For the array form, extract() passes the entries on untouched (return cloneDeep(config.hosting)), so nothing can fill in a missing site. The question's file has two entries that both publish dist/nur, and only the second names a target (its cache headers are left out here):

json
{
  "hosting": [
    {
      "public": "dist/nur",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [{ "source": "**", "destination": "/index.html" }]
    },
    {
      "target": "nur",
      "public": "dist/nur",
      "ignore": ["**/.*"],
      "rewrites": [{ "source": "**", "destination": "/index.html" }]
    }
  ]
}

The first entry fails on every run. In the reproduction, the file still failed after the nur target had been applied, and deleting only that first entry got the deploy past the assertion with the same broken login, so the login played no part. With the dead login, this file's firebase-debug.log carried the same 401 on the project lookup as the single-object runs below, which is why the log alone cannot tell the two causes apart. The asker's own output agrees: before failing, their deploy checked the Realtime Database, Storage and Firestore rules, and those checks are authenticated API calls, so their session worked. (The project uses both Firebase databases; how the two compare is covered in Cloud Firestore vs Realtime Database in 2026.)

A single object and a lookup that failed in silence#

With a single object, the CLI copies options.site into a config that names neither key (res.site = options.site). firebase deploy fills options.site before every Hosting deploy, whatever the shape of hosting, by asking the Firebase API for the project's default Hosting site (requireHostingSite(), then getDefaultHostingSite()); only the single-object form uses the answer. If that request fails, the value stays empty and the assertion fires. Whether you see the real failure depends on the CLI version:

firebase-toolsWhen the default-site lookup failsWhat you see
12.8.1the error stops the deployFailed to get Firebase project …
from 12.9.0 to the last release before 13.15.0firebase deploy catches it and only reacts to "no default site"this assertion
13.15.0 and later, including 15.32.1an HTTP 403 (no access) is also thrown again; any other failure is still droppedFailed to get Firebase project … on a 403, or this assertion

The first two rows were reproduced with the same dead login and the same single-object config. firebase-tools 12.8.1 printed Error: Failed to get Firebase project example-shop-eu. Please make sure the project exists and your account has permission to access it., and 12.9.0 printed the assertion. The change is PR #6493, "adds a check for a hosting site to exist in hosting init", which is in the 12.9.0 release of 14 November 2023: it wrapped the lookup in a try/catch that only handles the "no default site" case. PR #7490 then answered issue #7190, where a service account without access to the project got this assertion on 13.9.0, by re-throwing the lookup's error when it is an HTTP 403. The 13.15.0 release notes list it as "Improved error message thrown when missing permissions to deploy to hosting." The one failure the catch has handled since 12.9.0 is a project with no default site: firebase deploy then offers to create one, or, with --non-interactive, stops on Unable to deploy to Hosting as there is no Hosting site.

A dead login is a 401, not a 403, and 15.32.1 still drops it. When Google refuses to refresh the stored session with a plain invalid_grant, refreshTokens() in src/auth.ts does not stop the command: it uses the refresh token itself as the access token, and every API call then comes back 401. Run the deploy with --debug to watch it happen:

bash
firebase deploy --only hosting --non-interactive --debug 2>&1 | grep -E 'authorizing via|apiv2\]\[status\]|HTTP Error|Failed to get Firebase project|\[iam\]|Assertion failed'

Three lines of what that printed in the reproduction, verbatim:

text
[2026-10-06T05:46:49.839Z] > authorizing via signed-in user (reader@example.co.uk)
[2026-10-06T05:46:51.123Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/example-shop-eu 401
Error: Assertion failed: resolving hosting target of a site with no site name or target name. This should have caused an error earlier

Between the first two, the token endpoint answered 400 three times and the CLI's own permission check got a 401, which it logged as [iam] error while checking permissions, command may fail and ignored. Right after the 401 on the project lookup, the debug output printed the lookup's own error, Request to https://firebase.googleapis.com/v1beta1/projects/example-shop-eu had HTTP Error: 401, Request had invalid authentication credentials., and then dropped it: in the filtered output, the next line is the assertion.

The answers on the question follow the same timeline. Those from February 2023 deal with the project itself (add the target name, re-run firebase init, build before deploying), while every "log in again" answer, the most upvoted firebase login --reauth included, was posted in December 2023 or later, after 12.9.0 shipped.

Fix#

1. Check which shape your hosting has#

Run this in the folder that holds firebase.json:

bash
node -e "const h = require('./firebase.json').hosting; console.log(Array.isArray(h) ? 'array' : 'single object'); for (const e of [].concat(h)) console.log(JSON.stringify({ site: e.site, target: e.target, public: e.public || e.source }))"

On the question's file it prints array, then {"public":"dist/nur"} for the first entry: no site, no target, so go to step 2. If it prints single object and the line has no site, go to step 3.

2. Array: give every entry a site or a target#

When the entries describe the same site, as the question's two dist/nur entries do, keep one and delete the other: the one with "target": "nur" and the cache headers. Then look up the exact site ID, because target:apply does not check it. Its source makes no API call, and in the reproduction it accepted the placeholder ID while the login was dead, so a wrong ID only fails later, when the deploy looks the site up:

bash
firebase hosting:sites:list

Use the value in the Site ID column. The multiple sites guide notes that "you cannot delete the default site, which has the same SITE_ID as your Firebase project ID", but a project can hold other sites, and the list is what target:apply and "site" expect. If the question's live site is the default one of project nur-beta, mapping the target once looks like this:

bash
firebase target:apply hosting nur nur-beta

target:apply stores the mapping in .firebaserc, so commit that file with the project. In the reproduction, with the placeholder project example-shop-eu, it became:

text
{
  "projects": {
    "default": "example-shop-eu"
  },
  "targets": {
    "example-shop-eu": {
      "hosting": {
        "nur": [
          "example-shop-eu"
        ]
      }
    }
  },
  "etags": {}
}

Two other shapes are accepted. You can write the site ID in the entry as "site" instead of a target, but not both: an entry with both stops the CLI with Hosting configs should only include either "site" or "target", not both. A literal site ID ties the entry to that one site whichever project is active, and the multiple sites guide recommends targets instead: "Using deploy targets is the recommended way to configure deploys for multiple Hosting sites." If you really serve two sites, give each entry its own site, or its own target with one target:apply per target. That is the pattern of the deploy targets page: "In the arrays, specify the target (using the TARGET_NAME) and define your settings for the associated Firebase resource or group of resources." Re-authenticating does nothing for this case.

3. Single object: repair the login the CLI is using#

bash
firebase login --reauth

The CLI's own help describes --reauth as "force reauthentication even if already logged in". This is the step the reproduction could not run, since it needs a real Google account; it is what the question's later answers report. Two checks go with it:

  • firebase login:list is not a health check. For the dead session in the reproduction it still printed Logged in as reader@example.co.uk.
  • With several Google accounts, make sure the CLI uses one that can see the project: firebase login:use <email> switches the account, and firebase projects:list lists the projects the current account can reach. On 13.15.0 and later, an account without access gets the explicit Failed to get Firebase project message rather than this assertion, as the test output in PR #7490 shows.

If CI authenticates with a service account key through GOOGLE_APPLICATION_CREDENTIALS, as in issue #7190, that account is the one that needs access to the project. If the same deploy runs in GitHub Actions and the entry uses source (framework-aware Hosting), the runner also needs the webframeworks experiment, which is covered in Firebase webframeworks not enabled in GitHub Actions.

4. Optional: name the site to see the real error#

Adding "site" to the single object, with the default site's ID (the project ID, per the guide quoted in step 2; firebase hosting:sites:list confirms it once the login works), makes the CLI independent of the default-site lookup, so whatever is wrong shows up as its own error. With the dead login, that one change replaced the assertion with the deploy banner:

text
=== Deploying to 'example-shop-eu'...
 
i  deploying hosting

followed by the real failure, Error: Request to https://firebasehosting.googleapis.com/v1beta1/projects/example-shop-eu/sites/example-shop-eu had HTTP Error: 401, Request had invalid authentication credentials., and Google's usual hint about OAuth credentials. It is a diagnosis, not a cure: the 401 stays until step 3 is done. A site key also pins every deploy from this folder to that site ID, so take it out again if the same folder deploys to several projects.

Verify the fix#

Run firebase deploy --only hosting again. The === Deploying to banner is the first sign that the hosting config resolved: on 15.32.1, deploy() resolves it through hasPinnedFunctions() before printing that banner. That is also why the assertion now appears with no banner and before any other target is prepared, unlike the 2023 output in the question, where the rules checks ran first. If the deploy still fails, look at firebase-debug.log in the project folder, which a failed deploy writes even without --debug (the reproduction checked this on a fresh folder): it should no longer show a 401 on the project lookup. For an array config, firebase target lists the targets applied in the directory.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Why --only hosting:nur gets through while firebase deploy fails#

--only hosting:nur selects entries before targets are resolved, so an entry without a target is skipped. With the question's file, that command got past target resolution in the reproduction while --only hosting still stopped on the assertion, as the plain firebase deploy did in the question. Until the target is applied, the selective command fails on a much more explicit error, the one to expect right after you add a target:

text
Error: Deploy target nur not configured for project example-shop-eu. Configure with:
 
  firebase target:apply hosting nur <resources...>

Re-auth every hour on a Google Workspace account#

In issue #7626, a user had to run firebase login --reauth every hour to get past this assertion. The maintainer's reply points at Google Workspace: "It’s possible that the session length set by your organization is causing the session to expire." The tracking issue, #6898 ("Firebase requires reauth for Google Workspace Accounts"), is still open, and the CLI source singles the case out with a Cloud Session Control expiry comment next to invalid_rapt in auth.ts. That session length belongs to your organisation's admin settings, not to firebase.json. Until it changes, re-authenticate when the session expires. If the folder only ever deploys to one project, keeping "site" in the config makes an expired session fail with an authentication error, as in step 4, rather than with this assertion.

Nothing from that deploy went out#

firebase deploy runs the prepare step of every target before any target deploys (src/deploy/index.ts), and on 15.32.1 the assertion comes even earlier, before the banner. Either way, rules that were part of the same command, firestore.rules included, were not deployed. If the app then answers PERMISSION_DENIED after a deploy you believed had gone out, rules that were never deployed are one of the causes in Firestore PERMISSION_DENIED: every real cause and fix.

A missing build is not the cause#

The accepted answer says the JSON is fine and to run ng build before firebase deploy. The assertion is thrown before the folder is checked: in the reproduction, the question's file failed identically with and without a built dist/nur/index.html. The build is still needed. Once the config is fixed, a deploy without it gets past the assertion and fails later, in the upload step of src/deploy/hosting/deploy.ts, with Directory 'dist/nur' for Hosting does not exist. So build first, but expect it to change nothing about this assertion.

The message claims the problem "should have caused an error earlier", and in the login case it did: the CLI caught the failed site lookup and moved on without a site. Start from the shape of hosting. An array needs a site or a target on every entry, and no login will repair a missing one; a single object depends on a lookup that only a working login can make, and firebase-debug.log shows which request failed. Once the === Deploying to banner appears, the next thing the CLI reports is either the upload or an error that names its real cause.

Further reading: Fix Firestore INTERNAL ASSERTION FAILED (ID: ca9), Fix npm ERESOLVE: Firebase prerelease peer conflict. Also see Fix "c(...) is not a function" on Firebase SSR deploy.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote