@crossplatformai/storage
v0.2.25
Published
Shared storage plugin for CrossPlatform.ai projects
Readme
Storage Plugin
A cross-platform, zero-dependency storage abstraction for web, mobile, desktop, and CLI applications.
Features
- Platform-Agnostic: Single interface for web (localStorage), mobile (AsyncStorage), desktop (electron-store), and CLI (file-based JSON)
- Zero Dependencies: No external dependencies, only uses environment-specific APIs
- Type-Safe: Full TypeScript support with strict typing
- Utility Functions: Helpers for namespacing and temporary storage
- Well-Tested: 43+ comprehensive unit tests with 100% pass rate
Installation
pnpm add @repo/storageAdapters
Web Storage
Uses localStorage with SSR safety checks:
import { createWebStorage } from '@repo/storage';
const storage = createWebStorage();
await storage.setItem('key', 'value');
const value = await storage.getItem('key');Mobile Storage
Uses React Native's AsyncStorage:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMobileStorage } from '@repo/storage';
const storage = createMobileStorage(AsyncStorage);
await storage.setItem('key', 'value');Desktop Storage
Uses Electron's electron-store:
import ElectronStore from 'electron-store';
import { createDesktopStorage } from '@repo/storage';
const store = new ElectronStore();
const storage = createDesktopStorage(store);
await storage.setItem('key', 'value');CLI Storage
File-based JSON storage with lazy-loading and caching:
import path from 'path';
import { createCliStorage } from '@repo/storage';
const storage = createCliStorage({
name: 'my-app',
cwd: path.join(process.env.HOME || '.', '.config'),
});
await storage.setItem('key', 'value');Testing Adapters
For unit tests, use in-memory or no-op adapters:
// In-memory storage (data cleared on process exit)
import { createInMemoryStorage } from '@repo/storage';
const storage = createInMemoryStorage();
// No-op storage (all operations succeed but store nothing)
import { createNoOpStorage } from '@repo/storage';
const storage = createNoOpStorage();Utilities
Namespaced Storage
Prevent key collisions across features by prefixing keys:
import { createNamespacedStorage } from '@repo/storage';
const authStorage = createNamespacedStorage(storage, '@app_auth');
await authStorage.setItem('token', 'abc123');
// Stores as: @app_auth_tokenTemporary Storage
Manage session-scoped data that should be cleared after use:
import { createTemporaryStorage } from '@repo/storage';
const tempStorage = createTemporaryStorage(storage);
await tempStorage.setItem('email', '[email protected]');
// Later, clear all temp data
await tempStorage.clear();Building Domain-Specific Storage Wrappers
The storage plugin provides low-level key-value storage. For complex features, consider building domain-specific wrappers.
Example: Auth Storage Wrapper
The template includes AuthStorage as a reference pattern:
packages/ui/src/core/storage.ts
class AuthStorage {
private storage: StorageInterface | null = null;
setStorage(storage: StorageInterface): void {
this.storage = storage;
}
async getAccessToken(): Promise<string | null> {
return this.storage.getItem('@crossplatformai_auth_access_token');
}
async setTokens(
accessToken: string,
refreshToken: string
): Promise<void> {
await this.storage.setItem('@crossplatformai_auth_access_token', accessToken);
await this.storage.setItem('@crossplatformai_auth_refresh_token', refreshToken);
}
async setUser(user: unknown): Promise<void> {
return this.storage.setItem(
'@crossplatformai_auth_user',
JSON.stringify(user)
);
}
}Benefits of Domain Wrappers
- Type Safety - Domain-specific method signatures (e.g.,
getAccessToken()vsgetItem('key')) - Single Source of Truth - Storage keys defined once, not duplicated across files
- Automatic Serialization - Handle JSON.stringify/parse in one place
- Atomic Operations -
setTokens()ensures both tokens are set together - Self-Documenting -
clearAuth()is clearer than multipleremoveItem()calls
When to Use Domain Wrappers
- ✅ Use plugin directly for simple key-value storage
- ✅ Create domain wrappers for features with multiple related keys (auth, settings, cart, etc.)
- ✅ Use namespacing with wrappers to prevent key collisions:
createNamespacedStorage(storage, '@myapp_feature')
Interface
All adapters implement StorageInterface:
export interface StorageInterface {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
}Architecture
The plugin follows the Plugin Coordination Pattern for cross-platform consistency:
- Single Interface:
StorageInterfacedefines the contract all adapters must implement - Dependency Injection: Adapters accept platform-specific implementations (AsyncStorage, electron-store, etc.)
- Platform Abstraction: Apps import platform-specific adapters and wire them into their initialization
- React Integration: Apps wrap the storage in
StorageServiceProviderfor React component access
Plugin Independence
This plugin is standalone - it does NOT depend on other plugins, and other plugins MUST NOT depend on it at runtime:
- Plugin-to-Plugin Composition: Happens at the application level, never within plugins
- Type-Only Imports: Other plugins may import types from this plugin as
devDependenciesfor TypeScript support - Dependency Injection: Plugins receive storage instances injected from apps, they don't import adapters directly
Example: The repositories feature has storage-related functionality:
// ❌ WRONG: Features should not depend on storage plugin
// features/repositories/package.json
{ "dependencies": { "@repo/storage": "workspace:^" } }
// ✅ CORRECT: Type-only imports are in devDependencies
// features/repositories/package.json
{ "devDependencies": { "@repo/storage": "workspace:^" } }
// Type-only import (devDependency is OK)
import type { StorageInterface } from '@repo/storage';Why This Matters: This enforces clean plugin boundaries - each plugin is independently deployable and testable without requiring specific other plugins to be installed.
Usage in Apps
Web App
import { createWebStorage } from '@repo/storage';
const storage = createWebStorage();
<StorageServiceProvider value={storage}>
{/* App content */}
</StorageServiceProvider>Mobile App
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMobileStorage } from '@repo/storage';
const storage = createMobileStorage(AsyncStorage);
<StorageServiceProvider value={storage}>
{/* App content */}
</StorageServiceProvider>Desktop App
import ElectronStore from 'electron-store';
import { createDesktopStorage, createWebStorage } from '@repo/storage';
// Use web storage for renderer with sync to electron-store
const baseStorage = createWebStorage();
const electronStore = new ElectronStore();
const electronStorage = createDesktopStorage(electronStore);
// Hybrid: Use web storage but sync to electron-store for main process
const storage = {
...baseStorage,
setItem: async (key, value) => {
await baseStorage.setItem(key, value);
await electronStorage.setItem(key, value);
},
};Testing
Run tests with:
pnpm test --filter @repo/storageKey Design Decisions
- Async-First: All operations are async (
Promise<T>) to support both sync (localStorage) and async (AsyncStorage, file I/O) backends - String Values: Only stores strings (like localStorage) - callers handle serialization with JSON.stringify/parse
- Zero Dependencies: No external libraries, only platform APIs
- Dependency Injection: Adapters receive implementations rather than importing them, enabling flexibility and testing
Migration Guide
If migrating from custom storage implementations:
- Replace custom storage creation with plugin adapters
- Wrap in
StorageServiceProvider(if using React) - Use
createNamespacedStoragefor feature isolation - Update tests to use
createInMemoryStorageorcreateNoOpStorage
See individual app migrations:
- Web:
apps/web/utils/storage-service.ts - Mobile:
apps/mobile/utils/storage-service.ts - Desktop:
apps/desktop/utils/storage-service.ts
