DEV Community

Aaroophan Varatharajan
Aaroophan Varatharajan

Posted on Originally published at aaroophan.medium.com on

Staging Frontend Deployment Using a Firebase Hosting Preview Channel

A technical guide to staging frontend deployments with Firebase Hosting Preview Channels.

A technical guide to staging frontend deployments with Firebase Hosting Preview Channels, covering .firebaserc, firebase.json, build artifacts, channel URLs, authentication, deployment output, expiration, and backend frontend-origin configuration.
A technical guide to staging frontend deployments with Firebase Hosting Preview Channels, covering .firebaserc, firebase.json, build artifacts, channel URLs, authentication, deployment output, expiration, and backend frontend-origin configuration.

There is a very tempting way to think about staging frontend deployment.

The app is already hosted somewhere. There is already a live URL. There are previous releases. The hosting console has a neat deployment history. So the first instinct is simple:

“Can’t we just use one of those previous releases as staging?”

It sounds ✨reasonable✨

It is also where the mental model starts lying to us.


The live site is not staging

In Firebase Hosting, the live site has a job. It serves the current production version.

It is protective. Slightly boring. Very serious.

When the console shows domains like:

  • example.web.app
  • example.firebaseapp.com
  • custom-domain.com

Those are not staging URLs. Those are attached to the live Hosting channel.

The “Previous releases” section can also feel misleading at first. It looks like a version list. It has deploy hashes. It has timestamps. It has a calm little history of what happened.

But previous releases are not staging environments.

They are history.

That distinction matters because staging is not just “something that existed before.” Staging is a place where a new build can be viewed, tested, shared, and thrown away without touching the live channel.

The live channel says:

“I am what users see.”

A preview channel says:

“Come break things here first.”

That is the boundary.


The scary part: “deploy” does not always mean “production deploy”

This is the uncomfortable part.

The command contains the word deploy.

firebase hosting:channel:deploy stg --project <firebase-project-id>
Enter fullscreen mode Exit fullscreen mode

And when production is nearby, the word “deploy” has villain energy.

It sounds like it might overwrite the live site. It sounds like it might replace the current release. It sounds like one wrong Enter key could turn a staging experiment into a cursed production moment.

But Firebase Hosting has separate channel concepts.

A normal Hosting deploy targets the live channel.

A preview channel deploy targets a named preview channel.

So this command:

firebase hosting:channel:deploy stg --project <firebase-project-id>
Enter fullscreen mode Exit fullscreen mode

does not promote the build to the live site.

It creates or updates a preview channel called stg.

That distinction is the whole trick.

It is still a deployment, but it is not a live deployment.


Before deploying, check what Firebase thinks this project is

The next trap is assuming the CLI is looking at the right Firebase project.

That assumption is too confident.

Before running anything, check .firebaserc :

cat .firebaserc
Enter fullscreen mode Exit fullscreen mode

A simple config may look like this:

{
  "projects": {
    "default": "<firebase-project-id>"
  }
}
Enter fullscreen mode Exit fullscreen mode

That tells the Firebase CLI which project the local folder belongs to.

Then check firebase.json :

cat firebase.json
Enter fullscreen mode Exit fullscreen mode

For a frontend app, it may look like this:

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      " **/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This tiny file is the gatekeeper.

It says: “When you deploy Hosting, I am not deploying your source code. I am deploying whatever is inside dist.”

That means the preview channel is only as correct as the build output inside dist.

If dist is stale, Firebase will faithfully deploy stale vibes.

Firebase is not wrong. It is obedient.

That is worse.


Build first, then deploy the preview

So the safe sequence is not to rush into the preview deploy.

First, confirm the branch:

git status
git branch
Enter fullscreen mode Exit fullscreen mode

Then build the frontend:

npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

Then check that *dist * exists:

dir dist
Enter fullscreen mode Exit fullscreen mode

or on Linux/macOS:

ls -la dist
Enter fullscreen mode Exit fullscreen mode

Only after that does the preview channel command make sense:

firebase hosting:channel:deploy stg --project <firebase-project-id>
Enter fullscreen mode Exit fullscreen mode

If a longer-lived preview URL is needed:

firebase hosting:channel:deploy stg --project <firebase-project-id> --expires 30d
Enter fullscreen mode Exit fullscreen mode

The build step creates the artifact.

The preview deploy publishes the artifact.

Those are different responsibilities, and mixing them up is how staging starts gaslighting everyone.


The first failure is usually boring: authentication

A very normal first failure looks like this:

Error: Failed to authenticate, have you run firebase login?
Enter fullscreen mode Exit fullscreen mode

This is not a Firebase Hosting problem.

It is the CLI saying:

“I don’t know who you are.”

The fix is straightforward:

firebase login
Enter fullscreen mode Exit fullscreen mode

During login, Firebase may ask optional questions about Gemini features or CLI usage/error reporting. Those choices do not change the preview deployment itself.

After login succeeds, rerun:

firebase hosting:channel:deploy stg --project <firebase-project-id>
Enter fullscreen mode Exit fullscreen mode

This is one of those failures that feels annoying because it is not intellectually interesting.

But it is useful.

It proves the CLI is not silently deploying with some mystery identity. It forces an authenticated boundary before touching Hosting.

That is a good kind of boring.


Reading the deploy output without panicking

A successful preview deploy may show output like this:

hosting:channel: Channel stg has been created on site <site-id>.

=== Deploying to '<project-id>'...

deploying hosting
hosting[<site-id>]: beginning deploy...
hosting[<site-id>]: found 8 files in dist
hosting[<site-id>]: file upload complete
hosting[<site-id>]: finalizing version...
hosting[<site-id>]: version finalized
hosting[<site-id>]: releasing new version...
hosting[<site-id>]: release complete

Deploy complete!

Hosting URL: https://<live-site>.web.app
hosting:channel: Channel URL (<site-id>): https://<preview-url>.web.app
Enter fullscreen mode Exit fullscreen mode

This output can look suspicious at first because it still prints the live Hosting URL.

That does not mean the live site was replaced.

The important line is the channel URL:

Channel URL: https://<preview-url>.web.app
Enter fullscreen mode Exit fullscreen mode

That is the staging frontend URL.

That is the URL to share.

That is the URL to put into the staging backend config if the backend needs to know the frontend origin.

The live URL appearing in the output is just Firebase being Firebase. Helpful, but slightly dramatic.


The preview URL becomes part of the staging system

Once the preview channel exists, the frontend has a temporary staging address.

That URL can then be wired into backend configuration:

FRONTEND_URL="https://<preview-url>.web.app"
Enter fullscreen mode Exit fullscreen mode

This matters for flows where the backend needs to know the frontend origin, such as redirects, email links, CORS behavior, or environment-specific links.

The frontend preview channel is not only “a link to click”.

It becomes part of the staging environment’s identity.

That is where the preview channel stops being just a Firebase feature and starts becoming deployment architecture.

The frontend has its own temporary boundary.

The backend points at that boundary.

The staging environment begins to behave like an environment, not just a random build on the internet.


What this teaches

The seductive mistake is thinking staging is just another deployment.

It is not.

Staging is a boundary.

Firebase Hosting already has a live boundary. Preview channels give us a temporary one. The job is not to fight that model, or to reuse previous releases, or to squint at deployment history until it looks like staging.

The job is to respect the channel model:

The preview URL becomes part of the staging system

Each step has ownership.

The branch owns the code.

The build owns the artifact.

firebase.json owns what gets deployed.

The preview channel owns the temporary URL.

The backend config owns which frontend it trusts.

The Origami Software Engineer owns the mental model.

Once that model is clear, the command stops feeling dangerous:

firebase hosting:channel:deploy stg --project <firebase-project-id>
Enter fullscreen mode Exit fullscreen mode

It is not a risky shortcut around production.

It is the safe path around production.

And that is the broader lesson: good deployment workflows are not only about pushing code somewhere. They are about creating the right boundaries so code can be wrong in the correct place.

A staging frontend is allowed to be temporary.

It is allowed to be tested.

It is allowed to expire.

It is allowed to reveal that the mental model was incomplete.

That’s not failure.

That’s evolution.


The “I liked this” Starter Pack:

Don’t let your fingers get lazy now.

  • Like : It tells me this was worth writing.
  • A Comment: Tell me your thoughts, your favorite snack, or a better title for this blog.
  • Boost it: Especially with that one developer who definitely needs this.

Thanks for being here. It genuinely helps more than you know!

— Aaroophan Varatharajan

Find me elsewhere:

Top comments (0)