HTTP Strict Transport Security sounds simple: send one response header and browsers stop using HTTP for your site.

That’s true, but HSTS is one of those headers that can quietly break things if you turn it on too aggressively. I’ve seen teams copy a preload-ready header from a blog post, deploy it, and then discover some forgotten subdomain is still serving plain HTTP. Now users can’t reach it at all.

If you’re hosting on Firebase Hosting, the good news is the setup is straightforward. The tricky part is choosing the right policy and rolling it out without locking yourself into a bad configuration.

What HSTS actually does

HSTS tells browsers:

  • always use HTTPS for this host
  • optionally apply the rule to all subdomains
  • optionally allow the domain to be included in browser preload lists

A typical header looks like this:

Strict-Transport-Security: max-age=31536000; includeSubDomains

That means:

  • max-age=31536000: remember the rule for 1 year
  • includeSubDomains: apply it to www, app, api, and every other subdomain too

Once a browser sees that header over HTTPS, future HTTP requests get upgraded to HTTPS before the request is even sent.

That matters because redirects from HTTP to HTTPS are not enough on their own. Without HSTS, the first request can still be intercepted or downgraded on hostile networks.

What Firebase Hosting gives you by default

Firebase Hosting already serves your hosted site over HTTPS and provisions certificates for your connected domains. That solves transport encryption.

HSTS is separate. Firebase does not automatically force a strict browser-side HSTS policy for your domain. You need to add the header yourself in firebase.json.

Official docs for custom headers live here:

https://firebase.google.com/docs/hosting/full-config#headers

Basic HSTS setup in Firebase Hosting

Firebase Hosting lets you set response headers by path pattern. The simplest starting point is to add HSTS for all responses.

Here’s a practical firebase.json example:

{
  "hosting": {
    "public": "public",
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=86400"
          }
        ]
      }
    ]
  }
}

This sets HSTS for every hosted response with a max-age of 1 day.

Deploy it with:

firebase deploy --only hosting

I like starting with 86400 seconds during rollout. One day is long enough to verify browsers are learning the policy, but short enough that a mistake won’t haunt you for months.

Why you should not start with preload

A lot of examples jump straight to this:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

That header is valid, but it’s not a beginner setting.

preload is basically you telling browser vendors: “hardcode my domain as HTTPS-only.” That can be a great end state, but only if:

  • your apex domain works perfectly on HTTPS
  • www works perfectly on HTTPS
  • every subdomain is HTTPS-ready if you use includeSubDomains
  • you understand this is hard to undo

If you have old DNS entries, forgotten staging subdomains, email-related subdomains, or legacy services outside Firebase, preload can turn into a cleanup project you didn’t plan for.

My advice: treat preload as a final step, not a checkbox.

A safer rollout plan

Here’s the rollout I usually recommend.

Phase 1: low max-age

Start with 1 day:

{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=86400"
          }
        ]
      }
    ]
  }
}

Watch for problems:

  • mixed content issues
  • hardcoded http:// links
  • subdomains that still depend on HTTP
  • backend redirects doing weird protocol handling

Phase 2: raise max-age

If everything looks good, move to something like 6 months or 1 year:

{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=31536000"
          }
        ]
      }
    ]
  }
}

Phase 3: add includeSubDomains

Only do this when you’re sure every subdomain is HTTPS-safe.

{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=31536000; includeSubDomains"
          }
        ]
      }
    ]
  }
}

Phase 4: consider preload

Only after you’ve run with includeSubDomains successfully and verified your entire domain space is ready.

{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=31536000; includeSubDomains; preload"
          }
        ]
      }
    ]
  }
}

Full Firebase Hosting config example

Here’s a more realistic config with HSTS and a few other useful security headers:

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=31536000; includeSubDomains"
          },
          {
            "key": "X-Content-Type-Options",
            "value": "nosniff"
          },
          {
            "key": "Referrer-Policy",
            "value": "strict-origin-when-cross-origin"
          },
          {
            "key": "X-Frame-Options",
            "value": "DENY"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

If you want to scan your deployed headers quickly, run your site through Headertest.

How to verify HSTS is actually working

After deployment, check the response headers directly.

Using curl:

curl -I https://your-domain.com

You should see something like:

HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains

You can also inspect the Network tab in browser devtools and look at the response headers for the main document.

If you don’t see the header:

  • make sure you deployed the correct Firebase project
  • make sure your headers rule matches the requested path
  • make sure you’re checking an HTTPS response, not HTTP

HSTS headers sent over plain HTTP are ignored by browsers. That’s intentional.

Common mistakes on Firebase Hosting

1. Enabling includeSubDomains too early

This is the classic mistake.

Your main site may be on Firebase Hosting, but your DNS probably points other subdomains to other systems. HSTS doesn’t care where they’re hosted. If the browser learns includeSubDomains, every subdomain must behave over HTTPS.

Inventory your subdomains first.

2. Using preload without understanding the commitment

Preload is not “extra secure mode.” It’s a long-term browser-level commitment. If you’re not fully sure, skip it.

3. Setting HSTS only on one route

If you only apply the header to /, but users land on /pricing or /app, the browser may never learn the policy.

Use a broad source pattern:

{
  "source": "**"
}

4. Forgetting custom domains

If you serve both:

  • example.com
  • www.example.com

then both need correct HTTPS behavior and should send HSTS if they’re both user-facing. HSTS is host-specific unless you use includeSubDomains, and even then you need the hierarchy to make sense.

5. Thinking HSTS replaces redirects

It doesn’t.

You still want normal HTTP-to-HTTPS redirect behavior for first-time visitors and non-HSTS clients. HSTS strengthens that behavior after the browser has learned the rule.

Firebase Hosting already handles HTTPS delivery nicely, but you should still test the HTTP experience for your connected domains.

How to back out of HSTS

If you need to disable HSTS, the proper way is to send:

Strict-Transport-Security: max-age=0

In Firebase Hosting:

{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Strict-Transport-Security",
            "value": "max-age=0"
          }
        ]
      }
    ]
  }
}

Then deploy again:

firebase deploy --only hosting

That tells browsers to forget the policy after receiving the new HTTPS response.

Two catches:

  • users must revisit over HTTPS to receive the clearing header
  • if your domain is preloaded, max-age=0 does not magically remove it from browser preload lists

That’s another reason I’m conservative about preload.

For a typical production app on Firebase Hosting, I’d usually aim for this once the domain setup is mature:

Strict-Transport-Security: max-age=31536000; includeSubDomains

And I’d use this during rollout:

Strict-Transport-Security: max-age=86400

That gets you strong protection without rushing into preload.

If your team is disciplined, your subdomains are clean, and you’ve verified HTTPS everywhere, then preload can make sense. But don’t copy it blindly just because it looks more “enterprise.”

Final deployment checklist

Before you call HSTS done, check these:

  • site works on HTTPS for every user-facing hostname
  • no critical subdomain still relies on HTTP
  • HSTS header appears on real HTTPS responses
  • max-age started low and was increased deliberately
  • includeSubDomains was added only after checking all subdomains
  • preload was considered carefully, not pasted from a snippet

Firebase Hosting makes the mechanics easy. The hard part is being honest about your domain sprawl. If you keep the rollout boring and incremental, HSTS is one of the highest-value headers you can add with almost no runtime cost.

For Firebase Hosting header configuration details, use the official docs:

https://firebase.google.com/docs/hosting/full-config#headers