Ground Truth

Your Installed Web App Logs Users Out, and Nothing in Your Code Explains It

Mostafa DhouibMostafa Dhouib·10 min read
The short answer

On one major mobile platform, a web app installed to the home screen has storage isolated from the browser. The cookie jar, local storage and database are a separate container, so a session established in the browser does not carry into the installed app. Users appear logged out for reasons no server log, token lifetime or cookie attribute accounts for. This covers the storage isolation model, why it presents as an authentication bug, and what it forces in session design.

Your Installed Web App Logs Users Out, and Nothing in Your Code Explains It

The short answer. On one major mobile platform, a web app installed to the home screen runs in a storage container separate from the browser's. Same origin, different cookie jar, different local storage, different database, different service worker registration. So a session established in the browser does not exist in the installed app, and the user who signed in yesterday on the website opens the icon on their home screen and sees a sign-in form. Nothing in your code explains it, no server log shows an error, and every lever teams reach for first, token lifetimes, refresh tokens, cookie attributes, session stores, is pointed at a session that was never broken. It is in a different box.

Members report being logged out. Not all of them, not predictably, and not in a way support can reproduce.

Support tries it on their laptop and it works. The engineer tries it on their phone browser and it works. The server logs show a successful authentication for the member who complained, followed a few minutes later by unauthenticated requests from what looks like the same person.

Everybody's instinct is that something is expiring the session. Nothing is.

The storage model

An installed home-screen web app on this platform is not a bookmark into the browser. It is a separate web context with its own storage partition.

That partition covers everything you would use to remember a user. Cookies, local and session storage, the indexed database, service worker registrations and their caches. The origin is identical. The storage is not shared.

Three consequences follow, and the third is where the reports come from.

Installing does not carry the session. A user who is signed in on the website, and who then adds the site to their home screen, opens a container that has never seen a cookie. The install is not a copy.

Signing out in one does not sign out the other. Two independent sessions for the same account on the same device, each unaware of the other. If your product offers a "sign out everywhere" control, clearing cookies cannot implement it.

Any authentication flow that leaves the app completes somewhere else. This is the one that produces the support tickets. A member taps sign in inside the installed app, the flow sends an email link, the member opens their mail and taps the link, and that link opens in the browser, or in the mail client's own embedded web view, which is a third container again. The session is established correctly, in a place the member will never look. They go back to the home screen icon and it still shows a sign-in form.

The member did everything right. Your server recorded a successful login. The member is looking at a login screen. Every part of that is working as designed by someone.

  1. Member taps sign in inside the installed app
    a container that has never seen a cookie, because installing is not a copy of the browser's session
  2. The flow sends an email link
  3. The link opens in the browser, or in the mail client's own web view
    which is a third container again, with its own storage and its own lifetime
  4. The session is established correctly
    in a place the member will never look, and your server log records a successful authentication
  5. They go back to the home screen icon
    and it still shows a sign in form, with no failed request anywhere to explain it
Signing out in one container does not sign out the other either, so a sign out everywhere control cannot be implemented by deleting a cookie. Revocation has to be server side, keyed to the account or the credential.
FigureSame origin, different cookie jar, different local storage, different database, different service worker registration. The member did everything right, the server recorded a successful login, and the member is looking at a login screen. Every part of that is working as designed by somebody.

Why it presents as an authentication bug

The symptom set maps almost perfectly onto a token problem, which is why teams lose days to it.

Logouts are intermittent, because they depend on which surface a given member happens to use. They cannot be reproduced on desktop, because desktop has no second container. There is no server error, because no request failed. And the server-side evidence looks exactly like a session being invalidated early: a successful authentication followed by anonymous traffic from the same account.

So the team shortens the access token lifetime, then lengthens it. Adds a refresh token. Changes the cookie's SameSite attribute, then its domain, then its path. Suspects the edge cache. Suspects the session store. Moves from cookies to local storage, which makes it worse, because local storage is partitioned the same way and is additionally cleared under storage pressure.

The diagnostic that ends this is one field. Record, on every request, which surface it came from. The page can detect whether it is running in standalone display mode and send that as a header, or you can log it alongside the user agent. Once that field exists, a single query resolves the whole thing: the complaining accounts have a successful authentication tagged browser and subsequent unauthenticated requests tagged standalone.

Without that field the pattern is invisible, because every request looks like the same origin, the same account and the same device. Add the field before you start theorising. It costs an afternoon and it is the difference between a diagnosis and a week of guesses.

What it forces in session design

Six things, in roughly the order they matter.

Never rely on a session surviving a context switch. Treat this as a design constraint rather than a bug to be worked around. Any flow that hands the user off to another application, and expects them to come back signed in, is relying on something the platform does not provide.

Make link-based flows land in the right container. There are two shapes that work and you want both. A platform-routed deep link can open the installed app directly rather than the browser, which is the good experience and depends on association metadata being hosted and configured correctly. It also fails silently and falls back to the browser, which is exactly the failure you are trying to fix. So build the ugly mechanism as the fallback: a short-lived single-use code the user reads from the email and types into the app. It works in every container, it works when the deep link association is misconfigured, and it is the only version you can be certain of.

Treat every surface as needing its own sign-in, and make signing in cheap. If a second login is unavoidable, the goal is to make it a biometric prompt rather than a password entry. Platform credential autofill and passkeys turn the second sign-in from a support ticket into two seconds. This is the highest-leverage change in the list, because it converts the problem from "fix the isolation" to "make the consequence trivial".

Make the installed app's session long-lived. Re-authentication inside the installed container is the expensive event, so avoid needing it. A long-lived refresh credential bound to the device, with short-lived access tokens derived from it, and server-side revocation so you keep the ability to end the session centrally.

Do not keep the only copy of anything in a container. A draft, a part-completed form, an onboarding step, items selected before installing: all of it is invisible to the installed app. If it matters, it lives server-side keyed to the account. If it does not matter, say so explicitly in the design rather than discovering it in a complaint.

Explain it to the user. A screen that says you are signed in on the website and need to sign in here as well is dramatically better than an unexplained login form, because it converts a defect into an expected step. Users forgive a step they understand and churn on one they do not.

And revisit your logout semantics while you are there. Ending a session has to be a server-side revocation keyed to the account or the credential, not a cookie deletion, because a cookie deletion only reaches the container it ran in.

The acceptance test nobody has

This will not be caught by your automated browser tests. The test harness drives a browser, and the installed container is not a browser the harness can create. There is no amount of integration testing that closes this hole.

It has to be a manual device check on the release list, and it is small. Four states and two transitions.

The states: signed in on the browser, signed in on the installed app, signed out on the browser, signed out on the installed app, each verified independently on the same device for the same account.

The transitions, which are where the failures live: install the app while signed in on the browser and confirm what the installed app shows, and start a sign-in inside the installed app, complete the emailed link, and confirm where you end up signed in.

Write those six checks down with the expected result for each. The expected result may legitimately be "the installed app shows the sign-in screen", as long as somebody decided that and the screen explains itself. The failure is not the behaviour, it is the behaviour being unknown.

Signed in on the browsereach state verified independently, on the same device, for the same account
Signed in on the installed app
Signed out on the browser
Signed out on the installed app
Install while signed in on the browsera transitionconfirm what the installed app shows the first time it is opened
Sign in inside the app, then complete the emailed linka transitionconfirm which container you actually end up signed in to
The transitions are where the failures live. The failure is not the behaviour, it is the behaviour being unknown.
FigureAutomated browser tests cannot catch this, because the installed container is not a browser the harness can create, so no amount of integration testing closes the hole. It has to be a manual device check on the release list, and it is six lines long. The expected result may legitimately be that the installed app shows a sign in screen, as long as somebody decided that and the screen explains itself.

The honest limit

This is one platform's behaviour, and platform storage policies change between operating system versions. The partitioning described here is current, and it is a property of the platform rather than of your application, which means it can change under you in either direction. Re-verify it on real devices at each major release rather than treating this article as the current state of the world.

Other platforms partition differently, and some share storage between the browser and the installed app entirely, which means a single codebase will show this symptom on one platform and not another. That asymmetry is itself a diagnostic clue and also a reason your cross-platform test matrix cannot be collapsed.

Embedded web views inside messaging and social applications are a further set of containers, each with their own storage and their own lifetime, and they are a common entry point because that is where links get tapped. A session established in one of those may not survive the view being closed at all, which is a different failure with the same presentation.

And the symptom is not uniquely diagnostic. A user who cleared site data, or who is in a private browsing mode, produces the same evidence. That is precisely why the display-mode field matters. Without it you are inferring, and with it you are reading.

Finally, the fix for some products is not to ship an installable app. If the whole value of installing was a home screen icon, and the cost is a second authentication boundary nobody wants to own, that trade deserves to be made deliberately rather than inherited from a framework default.

FAQ

Why does my installed web app show a login screen when I am signed in on the website? Because on one major mobile platform the home-screen app has a storage container separate from the browser's, covering cookies, local storage, the indexed database and service worker registrations. Same origin, different storage. The session you established in the browser does not exist in the installed app, and installing does not copy it across.

Why can we not reproduce it on desktop? Because desktop has only one container for the origin, so there is no second storage partition for a session to be missing from. The bug requires the installed mobile surface to exist, which is also why it looks intermittent: whether a given member sees it depends entirely on which surface they happen to use next.

How do I confirm this is the cause rather than a token problem? Record which surface each request came from. Detect standalone display mode in the page and send it as a header or log it beside the user agent. Then query for accounts with a successful authentication tagged as browser followed by unauthenticated requests tagged as standalone. Without that field every request looks like the same origin, account and device.

How should email magic links work for an installed app? Use a platform-routed deep link so the link opens the installed app rather than the browser, and build a fallback, because deep link association fails silently. The reliable fallback is a short-lived single-use code the user reads from the email and types into the app. It works in every container, including embedded web views inside mail clients.

Does changing cookie attributes or token lifetimes help? No. The session is valid and the server never rejected anything, so SameSite changes, domain and path adjustments, longer or shorter token lifetimes and new refresh tokens all act on a mechanism that is not failing. Moving from cookies to local storage makes it worse, since local storage is partitioned identically and is additionally evicted under storage pressure.

How do I implement sign out everywhere across both surfaces? Server-side revocation keyed to the account or the credential, never cookie deletion. Clearing cookies only affects the container the code ran in, so a user signing out of the installed app remains signed in on the browser and vice versa. Issue short-lived access tokens against a revocable long-lived credential so revocation takes effect quickly on every surface.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program