Notes
A practical guide to launching a small web app on Firebase Hosting
Firebase Hosting is a solid fit for a small web app: static files, a global content network, free HTTPS, and very little to maintain. It also has a few sharp edges that tend to appear only after deployment. This guide covers an approach that worked for several small apps run by one person: start on the web, use a preview link before production, set cache headers that actually match, and make sure a stylesheet cannot go stale. Opinions are marked as opinions.
Start on the web, and keep the backend out of it
Every app in this workshop starts as a web app. Mobile stores add a gatekeeper and a yearly toll: an Apple developer account costs $99 a year before a single build ships, and Google Play charges a one-time $25 fee. Neither fee is large, but paying it before you know whether an idea is worth pursuing is backwards. A web app answers that question cheaply: put it on a subdomain, see whether anyone comes back, and only then decide whether a store listing is worth the fee and review queue.
The same instinct applies to servers. BlockClock, a Bitcoin dashboard, has no backend and no accounts. It is a static site that reads public data sources (mempool.space, blockstream.info, blockchain.info and CoinGecko) directly from the browser. There is no database to back up and no process to patch. Opinion: before adding a backend, ask what happens to the app during the week you do not touch it. A static site answers that question by default. This is not an argument against backends, only a reminder that each one is an ongoing decision to carry more operational weight.
Preview first, always
Firebase Hosting can deploy to a preview channel, giving you a real, working URL for a build before anything reaches your live domain. The command looks like this:
firebase hosting:channel:deploy preview --only hosting:your-site
The output includes a temporary address. Open it on a phone and a laptop, click through the pages, and only then deploy to the live channel. The one exception I allow myself is moving content that has already been reviewed to its permanent home without changing it. A preview link adds a small amount of ceremony, but it means you discover a mistake through a link only you opened, rather than from a visitor or an ad reviewer. Channels can be given an expiry, so old previews do not linger.
One more habit is worth adopting: if the preview is a draft you do not want indexed, make sure it carries a noindex instruction. A preview URL is public to anyone who has the link.
Cache headers: the rule that skipped the home page
Caching bugs do not show up as errors. The site keeps returning 200 responses while serving the wrong version of itself to some visitors. A natural first attempt is to set a no-cache header on HTML files only, so edits appear immediately:
{
"hosting": {
"headers": [
{
"source": "**/*.html",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
}
]
}
}
That pattern looks as though it matches every page, and for most pages it does. It does not match
the request path / itself. The glob matches paths ending in .html, while the root
path the browser requests for the home page has no .html suffix. The rule therefore skips the
page that matters most and leaves it with the default caching behavior.
The fix is to reverse the approach. Set a catch-all no-cache rule on every path first, then add a more specific rule afterward to opt genuinely static assets back into long-term caching. Firebase applies the more specific matching rule when more than one rule matches, so the override works as long as the catch-all comes first:
{
"hosting": {
"headers": [
{
"source": "**",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
},
{
"source": "**/*.@(js|css)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}
}
Note that the long-lived rule is safe only for files whose names change when their content changes, which is covered in the next section.
Put a content hash in the stylesheet name
On FitCalc, the stylesheet was served with a 24-hour Cache-Control header under a fixed
filename, styles.css. A day of caching sounds mild, but anyone who had opened the
site in the previous 24 hours, including through a link shared that morning, continued seeing the
old design for up to a full day after a redesign was live for everyone else. The cache was not too
long in the abstract. The filename never changed, so the browser had no signal that the content
behind it had changed.
The fix was a content hash in the filename, generated from the file's own contents at build
time: styles.3f9a21c8.css instead of styles.css. Any edit produces a
new hash, a new filename, and an HTML reference to it. Because the name changes with the
content, the file can be marked immutable and cached for a year without a risk of staleness.
It is a one-line change in a build script, and it is invisible in your own browser because you
are the one person guaranteed to have cleared your cache recently.
Service workers can bring the bug back
If your app is installable as a progressive web app, the service worker sits between the browser and your headers. A cache-first service worker keeps responding from its local cache no matter what Cache-Control says, so a visitor who installed the app once can keep seeing an old build indefinitely. The same instinct fixes it one layer deeper: default to network-first, and treat the cache as an offline fallback rather than the source of truth. It costs a little latency, but it means a shipped fix reaches people who already installed the app.
Verify the live response, not the config
A rule that looks right on paper can still fail to match the path it was written for, as the home-page rule above did. After every deploy, inspect the headers the server actually returns:
curl -sI https://your-site.example/ | grep -i cache-control
curl -sI https://your-site.example/styles.3f9a21c8.css | grep -i cache-control
The first should say no-cache, and the second should show the long immutable value. If either is not what you expect, fix the configuration before telling anyone the release is done.
Do not start the next thing until this one runs itself
The hardest discipline is sequencing, not technology. An app that needs daily attention to stay up, such as a job that silently stops or a page that needs manual review before every change, becomes a liability as more apps are stacked on top. My rule, which is a personal preference and not a universal law, is that nothing new begins until the previous thing can run untouched for a month. It is slower than it could be, and it is the only way one person can keep several live products from becoming several part-time babysitting jobs.
Launch checklist
- Deploy to a preview channel first and check it on a real phone.
- Put a catch-all no-cache rule first in the headers list, with specific overrides after it.
- Give long caching only to files whose names contain a content hash.
- If you use a service worker, make it network-first.
- After deploying, run
curl -sIagainst the live URLs and read the headers. - Confirm the real Hosting site id before writing any DNS record for a custom domain.