Persistent Key-Value Storage in Ionic with Capacitor Storage
Save user preferences across app restarts using Capacitor Storage in Ionic, with setup, service wrapper, platform limits, and verification steps.
14 Jun 2026, 20:30 UTC

Store small values across app restarts in Ionic
Problem: An Ionic app needs to remember a user’s theme choice, auth token, or form draft after the app closes. Browsers offer localStorage, but it is synchronous, web-only, and not available to native iOS or Android builds. Capacitor Storage is a cross-platform plugin that reads and writes key-value pairs to the native store on each platform: iOS Keychain, Android SharedPreferences, or IndexedDB on the web.
Takeaway: Install @capacitor/storage, wrap its asynchronous methods in an Angular service, handle rejections explicitly, and verify persistence on a real device or emulator before shipping.
Prerequisites
- Ionic Angular project with Capacitor initialized (npx cap init).
- Node.js and npm installed.
- For native testing: Android Studio or Xcode, and the corresponding Capacitor platform added.
Install the plugin
Run this in the project root. No elevated permissions are required for the npm install itself.
npm install @capacitor/storage @capacitor/core
If you are targeting native platforms, sync the Capacitor native projects afterward:
npx cap sync
Risk: npx cap sync writes to the android/ and ios/ directories. Review the changes in version control before committing.
Create a storage service
Generate a service so components do not call the plugin directly:
ng generate service services/storage
Open src/app/services/storage.service.ts and import the plugin:
import { Injectable } from '@angular/core';
import { Storage } from '@capacitor/storage';
@Injectable({
providedIn: 'root',
})
export class StorageService {
async setItem(key: string, value: string): Promise {
await Storage.set({ key, value });
}
async getItem(key: string): Promise {
const result = await Storage.get({ key });
return result.value ?? null;
}
async removeItem(key: string): Promise {
await Storage.remove({ key });
}
async clearAll(): Promise {
await Storage.clear();
}
}
Storage.get returns an object with a value property. When the key does not exist, value is null, so the service normalizes that to null.
Read and write from a component
import { Component } from '@angular/core';
import { StorageService } from '../services/storage.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.page.html',
})
export class SettingsPage {
constructor(private storage: StorageService) {}
async saveTheme(theme: string) {
try {
await this.storage.setItem('theme', theme);
console.log('Theme saved');
} catch (error) {
console.error('Failed to save theme', error);
// Show user feedback here
}
}
async loadTheme() {
try {
const theme = await this.storage.getItem('theme');
console.log('Stored theme:', theme);
} catch (error) {
console.error('Failed to read theme', error);
}
}
}
All plugin methods return Promises. Do not await them in tight loops or synchronous event handlers; unhandled rejections can leave the UI in an inconsistent state.
Understand limits and platform behavior
| Platform | Underlying store | Typical limit | Notes |
|---|---|---|---|
| iOS | Keychain | Not fixed by plugin; system-managed | Requires Keychain entitlement; denied access causes failures. |
| Android | SharedPreferences | Approximately 5 MB | Large values may throw. |
| Web | IndexedDB | Browser-dependent, often hundreds of MB | Unavailable in some private modes or older browsers. |
Keys are case-sensitive. Theme and theme are different keys. Values must be strings; encode binary data as base64 before storing.
Web fallback
On the web, Capacitor Storage uses IndexedDB. If IndexedDB is unavailable, you can add a last-resort fallback to window.localStorage. This is optional and should only be used for non-sensitive data because localStorage is accessible to any script on the page.
async function safeSet(key: string, value: string) {
try {
await Storage.set({ key, value });
} catch {
localStorage.setItem(key, value);
}
}
Risk: localStorage is synchronous and limited to roughly 5 MB in many browsers. Do not store secrets there.
Expected checks
- Build and run the app on an Android emulator or device.
- Call saveTheme('dark'), close the app completely, then reopen it and call loadTheme(). The value should return 'dark'.
- On iOS, confirm the value survives a restart. If the user denies Keychain access, expect storage operations to reject.
- In a browser, open Developer Tools > Application > IndexedDB and confirm a Capacitor Storage entry exists after a set call. Reload the page and read the value again.
- To test error handling, attempt to store a value larger than the platform limit and verify the catch block runs.
Recovery and rollback
Installing the plugin changes package.json and, after cap sync, native project files. To remove it:
npm uninstall @capacitor/storage
npx cap sync
Any values already written to native storage remain on the device until the app is uninstalled or Storage.clear() is called. There is no automatic migration back to localStorage.
Practical verification
After deploying a build, open the browser console on a web preview and run:
// Runs in browser context; not in native WebView unless exposed
import { Storage } from '@capacitor/storage';
await Storage.set({ key: 'smoke_test', value: '1' });
const check = await Storage.get({ key: 'smoke_test' });
console.log(check.value);
await Storage.remove({ key: 'smoke_test' });
If check.value is null, the plugin is not initialized or the web view lacks IndexedDB support.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.