Caching
How long your changes take to reach users in the app, and what to do when they take too long
Caching
Your web app runs inside the app's embedded browser. It caches your site exactly as a browser does, using your own Cache-Control headers — the app does not override them.
So when a user sees an old version of your site, the lifetime holding it there is almost always one your own server set. This page tells you how long each layer holds a change, how to shorten it, and what to check when the headers look right and it still won't update.
The short version
| What | Who controls it | How long | How to make it faster |
|---|---|---|---|
Your responses with Cache-Control | You | Exactly what you set | Set no-cache, or ship a new filename |
Your responses without Cache-Control | Nobody — the browser guesses | Unpredictable | Set a header. There is no other lever |
| The starti.app brand bundle | starti.app | Up to 4 hours | Not possible — wait it out |
| The native app | App Store / Google Play | Until the user updates | Not possible |
Updating the app from the App Store or Google Play does not deliberately clear anything. The cache, cookies and local storage carry over to the new version.
Set your cache headers
Two rules, for two kinds of file. This pair is what makes deploys appear instantly and keeps caching effective — no single max-age does both.
Your HTML entry point — revalidate every time:
Cache-Control: no-cacheYour fingerprinted assets — cache indefinitely:
Cache-Control: public, max-age=31536000, immutableFingerprinting means the filename contains a hash of the contents, so app.4f2a91.js becomes app.8c7d03.js when the file changes. A new filename is a new cache entry, fetched immediately. Vite and Next.js do this by default; webpack and Rollup need [contenthash] in the output filename.
Only put immutable on filenames that actually contain a hash. On a fixed name like bundle.js it tells every browser to keep that exact file for a year, and you cannot take it back.
no-cache does not mean "do not cache". It means "cache it, but check with the server before reusing it" — which is what you want for HTML, since an unchanged file costs you a cheap 304 Not Modified. The directive that prevents storage entirely is no-store, and you rarely want it.
For Firebase Hosting, the two rules look like this:
"headers": [
{ "source": "**", "headers": [
{ "key": "Cache-Control", "value": "no-cache" } ] },
{ "source": "/assets/**", "headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } ] }
]The order matters: when two rules match, the last one wins, so the broad ** rule has to come first or your hashed assets end up no-cache.
Headers are only half of it. A tab or app session that was already open when you deploy still asks for the old asset filenames, which no longer exist — so also handle chunk-load failures in your code by reloading the page once. Getting the headers right prevents the next user from hitting it; it does nothing for the session already running.
Making a change appear now
Users have no way to force a fresh fetch themselves, and the only lever that scales is to change the URL, because a cached entry is keyed by URL. (Your page can also call clearWebData(), but that signs the user out, so it is a recovery tool rather than a deployment strategy.)
- Assets — change the filename (
app.4f2a91.js→app.8c7d03.js), or append a query string (?v=2026-08-18). - HTML — you cannot rename your entry point, so it has to already be
no-cache. If it isn't, users stay on the old page until their copy expires, and there is nothing you can do from your side. Fix the header now so the next incident is recoverable.
Check what you're sending
curl -sD - -o /dev/null https://your-site.example/ | grep -i cache-controlTo watch it on a real device, attach a remote inspector and use the Network tab — see Debug Your App. This requires a dev or test build; remote inspection is disabled in production builds.
How long each layer holds a change
| Layer | Controlled by | Updates when |
|---|---|---|
| Your origin | You | Immediately on deploy |
| Your CDN or host, if any | You | Per your headers and your host's purge tools |
| The device's cache, inside the app | Your Cache-Control | When the lifetime you set expires |
| The starti.app brand bundle | starti.app | Within about 4 hours of a deploy from the Manager |
| The native app | The app stores | When the user installs an update |
The third row is the one behind almost every "my change isn't showing" report.
The brand bundle
main.js and main.css are served from cdn.starti.app through a CDN and are currently sent with max-age=14400 — 4 hours. Deploying your brand from the starti.app Manager takes roughly 10–15 minutes to build and publish; after that, a user holding a cached copy picks up the new bundle within the 4-hour window. You can check the current value yourself:
curl -sIL https://cdn.starti.app/c/YOUR_BRAND/main.js | grep -i cache-controlThe native app
Only what is compiled into the app waits for a store update — the splash screen and the bundled offline page. The starting URL and the app icon can both be changed at runtime from your own page through the SDK, and those overrides are stored natively and survive app updates.
When it isn't the cache
If your headers are right and users still see old behaviour, one of these is usually why. None of them are HTTP caches and none respond to Cache-Control:
- A starting URL you set earlier. If your page has ever called the SDK to override the app's starting URL, that override is stored natively and persists across app updates — the app will keep opening the old address until you reset it. This looks exactly like a cached page and is the easiest cause to miss.
- Cookies. Persistent cookies survive restarts and app updates. Session cookies are normally dropped when the app process starts: WebKit does this on iOS, and the app does it explicitly on Android.
localStorage,sessionStorageandIndexedDB.localStorageandIndexedDBsurvive restarts and app updates; only your code,clearWebData(), Android's Clear storage, or an uninstall removes them.- App storage (
startiapp.Storage.app) — see Storage and data. Stored natively, and not touched byclearWebData(). Note that outside the app, and in older app versions, it falls back tolocalStorage; in that mode it is web storage and is cleared. - A common script. If your brand has one configured — a snippet the app re-runs on every page load — it is stored natively, survives app updates, and nothing expires it. You have to overwrite it, or set it to an empty string.
Clearing web data (debugging only)
Your page can ask the app to clear web data through the SDK. It clears everything, so the user is signed out of any web session — use it for "reset app" and "log out", not as a routine cache-buster.
await startiapp.Storage.clearWebData();| What it clears | iOS | Android |
|---|---|---|
| HTTP cache | Yes | Yes |
| Cookies | Yes | Yes |
Website storage (localStorage, IndexedDB) | Yes | Yes |
The promise resolves once the app reports the clearing finished, so you can reload straight after it:
await startiapp.Storage.clearWebData();
location.reload();Neither platform reloads the page for you — that is yours to do.
In app versions 4.1.224 and earlier, Android clears only website storage — the HTTP cache and cookies are left in place, so the call cannot evict a stale page there. Those versions also resolve the promise as soon as the request reaches the app rather than when clearing has finished, so a reload in the same tick can land back in the cache you just cleared.
If you support those versions, check with await startiapp.App.version() and delay the reload:
await startiapp.Storage.clearWebData();
setTimeout(() => location.reload(), 500);clearWebData() needs the Storage integration enabled for your brand. If the promise never resolves and the console shows that the Storage integration could not be found, that is why — contact starti.app to have it enabled.
What a user can do
There is no pull-to-refresh on your content. The offline screen's Try again button and the in-app browser's refresh button re-navigate, but they still honour the cache, so neither forces a fresh fetch.
- Android — Settings → Apps → your app → Storage → Clear cache. Note that Android's automatic backup can restore web data after a reinstall.
- iOS — there is no equivalent; deleting and reinstalling the app is the only route.