← Back to Blog

Fix stuck Capacitor storage on iOS

Developer NotePublished August 28, 2025Updated October 22, 20264 min read
iOSCapacitorSecurityMobileKeychain

When building mobile apps with Capacitor, storing tokens and secrets securely is critical. One option is Capacitor Secure Storage, which uses the iOS Keychain underneath.

If your app suddenly breaks because of stuck secure storage data, here are ways to fix it on a development or QA device and prevent it from happening after an app reinstall.

The short version: clear the affected Keychain data after a reinstall, but do not mistake the first app update containing this fix for a reinstall.

Updated: I later reused this solution in a stable app with existing users. That exposed a missing rollout guard and startup order requirement. This revision adds both while keeping the original recovery options.

An open safe beside an eraser, representing cleared secure storage

The Problem: Why iOS Is Different

Capacitor Preferences and secure storage have different lifecycles on iOS:

  • @capacitor/preferences uses UserDefaults, and its values are removed when the app is uninstalled.
  • Secure storage uses the iOS Keychain. In practice, Keychain items can remain after uninstall and become accessible again after reinstall.

Apple describes Keychain persistence after uninstall as an implementation detail rather than a documented contract. Your app should therefore work whether an old item is present or not.

On real devices, reinstalling the app may bring back an access token, refresh token, selected account, or other state from the previous installation. The user expects a clean start but gets a stale or inconsistent session instead.

How This Solution Evolved

I first ran into this problem in an app that was still under development. The mobile authentication flow was ready before the backend, so I created a hacky login workaround. It left tokens in Keychain that were difficult to remove because reinstalling the app did not clear them.

To improve the developer and QA experience, I added cleanup after reinstall. The solution was slightly improved and shipped with the app’s initial release. It had worked this way from the beginning, so there were no existing users to migrate and no need for an update guard.

The reset was convenient enough that I later added it to a second, stable app. Users and support no longer needed to think about which data might be left behind. They could reinstall the app and check whether leftover app state was causing the problem.

That app already had users. A simple first launch flag would also be missing after they updated to the version containing the fix, so the update could clear their valid sessions. I added the explicit existing state check and treated the rollout like other potentially breaking changes.

Fixing It: Options for Developers

Add a QA action

The quickest option is a repeatable internal QA or debug action that removes only the affected session keys and reloads the app.

Use Safari Web Inspector

If the app has no such action but its WebView is inspectable, open Safari Web Inspector on macOS and run:

await window.Capacitor.Plugins.SecureStoragePlugin.clear();
window.location.reload();

clear() removes all values stored by the plugin, including any data unrelated to the session.

How to Prevent It After Reinstall

Use @capacitor/preferences to create an installation marker such as hasLaunchedBefore. Preferences is cleared when the app is uninstalled, while old Keychain data may remain.

The tempting implementation is to clear secure storage whenever the marker is missing. That works for a new app, but it is unsafe when added to an app that people already have installed. Existing installations do not have the new marker either, so the first update containing the fix could log everyone out.

If you know every existing installation already contains at least one other Preferences value, you can use that existing state to distinguish an update from a reinstall. When the marker is missing, the following code clears secure storage only if no Preferences values are saved:

import { Capacitor } from "@capacitor/core";
import { Preferences } from "@capacitor/preferences";
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";

const INSTALL_MARKER = "hasLaunchedBefore";

const onAppLaunch = async () => {
  if (Capacitor.getPlatform() !== "ios") return;

  const { value } = await Preferences.get({ key: INSTALL_MARKER });
  if (value === "true") return;

  const { keys } = await Preferences.keys();
  if (keys.length === 0) {
    await SecureStoragePlugin.clear();
  }

  await Preferences.set({ key: INSTALL_MARKER, value: "true" });
};

Call and await onAppLaunch() before any code reads the stored session:

await onAppLaunch();
// Only now read the session from secure storage.

It must also run before any other startup code writes to Preferences. If another service writes a value first, keys.length is no longer zero, cleanup is skipped, and the old Keychain session remains.

When the Preferences check is not safe

The keys.length > 0 check only works when every existing installation is guaranteed to contain another Preferences value. If some installations may have none, use a staged rollout:

  1. Ship a version that writes the installation marker without clearing secure storage.
  2. Enable cleanup on first launch in a later version.

The previous version of this article did not mention that some users may skip the release that only writes the marker. Treat enabling cleanup like any other potentially breaking change: roll it out gradually and, where possible, monitor unexpected logouts or authentication failures. High adoption of that release reduces the risk but does not remove it.

In a perfect world, the backend also validates tokens restored from storage and rejects stale or revoked ones.

Verify the Fix

Test both lifecycle paths and the startup order on an iOS binary:

  1. Install the new build, sign in, delete the app, reinstall it, and confirm that the login screen appears.
  2. Install the previous production version, sign in, update to the new build, and confirm that the user remains signed in.
  3. Confirm that cleanup finishes before the first token or account state read.
Browse all writing →