npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/storage

Adapters

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_token

Temporary 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() vs getItem('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 multiple removeItem() 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:

  1. Single Interface: StorageInterface defines the contract all adapters must implement
  2. Dependency Injection: Adapters accept platform-specific implementations (AsyncStorage, electron-store, etc.)
  3. Platform Abstraction: Apps import platform-specific adapters and wire them into their initialization
  4. React Integration: Apps wrap the storage in StorageServiceProvider for 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 devDependencies for 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/storage

Key Design Decisions

  1. Async-First: All operations are async (Promise<T>) to support both sync (localStorage) and async (AsyncStorage, file I/O) backends
  2. String Values: Only stores strings (like localStorage) - callers handle serialization with JSON.stringify/parse
  3. Zero Dependencies: No external libraries, only platform APIs
  4. Dependency Injection: Adapters receive implementations rather than importing them, enabling flexibility and testing

Migration Guide

If migrating from custom storage implementations:

  1. Replace custom storage creation with plugin adapters
  2. Wrap in StorageServiceProvider (if using React)
  3. Use createNamespacedStorage for feature isolation
  4. Update tests to use createInMemoryStorage or createNoOpStorage

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