Using Capacitor Storage for Small Key‑Value Persistence: An Architecture Note
Guidance on requirements, minimal design, trust boundaries, operational checks, failure modes, and when to revisit the design for Capacitor’s Storage plugin in Ionic/Capacitor apps.
29 Dec 2025, 17:09 UTC

Requirements
Before adding Capacitor Storage, clarify what the app needs to persist:
- Small key‑value pairs (typically < 100 KB total).
- Data that does not require strong confidentiality (e.g., UI preferences, feature flags, cached non‑personal identifiers).
- Availability across app restarts and, optionally, across web, Android, and iOS targets.
- No need for complex querying or relational semantics.
If any of the above is violated (e.g., large blobs, strict encryption, or background sync), consider a different storage mechanism.
Smallest Suitable Design
The minimal implementation consists of three steps:
- Install the plugin:
npm install @capacitor/storageand sync native projects. - Import and initialize once (usually in
app.component.tsor a service constructor):import { Storage } from '@capacitor/storage'; async function initStorage() { await Storage.create(); // ensures the underlying store exists } initStorage(); - Wrap get/set/remove in a service that returns promises, handling JSON serialization and errors:
import { Storage } from '@capacitor/storage'; export class KvService { async set(key: string, value: unknown): Promise { try { await Storage.set({ key, value: JSON.stringify(value) }); } catch (err) { // handle quota‑exceeded or JSON errors throw new Error(`Storage set failed for ${key}: ${err}`); } } async get(key: string): Promise { try { const ret = await Storage.get({ key }); if (!ret.value) return null; return JSON.parse(ret.value) as T; } catch (err) { // corrupted JSON or missing key return null; } } async remove(key: string): Promise { await Storage.remove({ key }); } }
This design uses the plugin’s default storage choice (LocalStorage on web, EncryptedSharedPreferences on Android ≥ 23, Keychain on iOS) and adds only a thin JSON wrapper.
Trust and Data Boundaries
Capacitor Storage isolates data per‑app:
- On the web, data lives in the origin’s LocalStorage, inaccessible to other origins.
- On Android, the plugin writes to the app‑private SharedPreferences directory (
/data/data//shared_prefs/). When EncryptedSharedPreferences is available, the file is encrypted with a key stored in the Android Keystore. - On iOS, values are placed in the app’s Keychain under the default access group, which other apps cannot read without explicit entitlements.
Because the plugin does not expose a migration API, data is not shared between different app installations unless you deliberately reuse the same key prefix across builds (e.g., by hard‑coding a suite identifier). Treat the storage as app‑private; do not rely on it for cross‑app communication.
Operational Checks
Integrate the following verification points into startup or feature flags:
- Plugin readiness – await
Storage.create()and catch any error; if it fails, fall back to an in‑memory store or display a degraded‑mode notice. - Storage availability – after a
setoperation, immediatelygetthe same key to confirm the write succeeded. - Quota handling – catch
QuotaExceededError(or the generic error the plugin throws) and implement a cleanup strategy (e.g., remove oldest keys). - JSON integrity – wrap
JSON.parsein a try/catch; on failure, treat the value as corrupt and optionally delete the key. - Platform‑specific encryption check (Android) – on devices running API < 23, the plugin falls back to unencrypted SharedPreferences. You can detect this by attempting to read the file via
adb shell run-as cat shared_prefs/.xmland verifying whether the values appear as plain text.
These checks add minimal overhead but surface the most common failure modes early.
Failure Modes
Typical ways Capacitor Storage can fail:
- Quota exceeded – especially on web browsers where LocalStorage is limited to ~5 MB. The plugin throws an error that you must catch.
- Corrupted JSON – if a non‑JSON string is stored (e.g., via a bug or manual edit), subsequent
getcalls will throw onJSON.parse. - iOS Keychain denial – after a device reset or if the app’s Keychain access group changes, reads may return
null. - Android encryption fallback – on API < 23, data is stored in plain‑text SharedPreferences, readable by any process with root or by the user via backup tools.
- Permission‑related silent failures** – on Android, no runtime permission is required for private storage, but if the app’s data directory is corrupted (e.g., due to a failed update), storage operations will fail.
Each failure mode should be logged (without exposing raw data) and presented to the user as a non‑critical UI issue unless the data is essential for app functionality.
When to Reconsider the Design
Revisit the storage choice if any of the following conditions arise:
- The app needs to store > 1 MB of structured data or binary blobs.
- Regulatory or security requirements demand end‑to‑end encryption with key rotation.
- Data must survive app uninstall/reinstall or be available to other apps (e.g., via a shared container).
- You observe frequent quota‑exceeded errors on web targets that cannot be mitigated by pruning.
- Your target Android user base includes a significant portion of devices running API < 23 and you cannot accept unencrypted storage.
In those cases, evaluate alternatives such as:
- Capacitor Secure Storage (or a custom crypto layer) for encrypted key‑value.
- SQLite via
@capacitor-community/sqlitefor larger or relational datasets. - Platform‑specific APIs (e.g., Android DataStore, iOS UserDefaults with encryption) when fine‑grained control is required.
Practical Verification Steps
To confirm that the design works as expected on each platform:
- Build and run the app on iOS simulator, Android emulator (API 23+ and API < 23), and a desktop browser.
- Use the service to set a known value (e.g.,
{foo: 'bar'}) under a test key. - Restart the app (or reload the page) and retrieve the value; assert deep equality.
- On Android:
- For API ≥ 23, locate the encrypted prefs file via
adb shell run-as cat shared_prefs/.xmland verify that the values are not readable as plain text. - For API < 23, confirm the file contains the key/value in clear text (this demonstrates the fallback).
- On iOS, open Xcode → Debug → View Debugging → Keychain Access (or use
security find-generic-password -s) to ensure the entry exists under the app’s identifier. - On the web, open the browser’s developer tools → Application → LocalStorage and check the key/value.
- Finally, simulate a quota‑exceeded condition by repeatedly setting large values until the operation fails, and verify that your error‑handling path logs appropriately and does not crash the app.
These steps give confidence that the smallest suitable design behaves predictably across the supported environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.