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

zustand-crypto-persist

v0.3.0

Published

Secure, encrypted persistence for Zustand stores with Web Crypto API

Downloads

2

Readme

zustand-crypto-persist

Secure, encrypted persistence for Zustand stores using Web Crypto API. Features include automatic salt generation, IndexedDB support, migration utilities, and React Suspense integration.

Features

  • 🔐 Secure Encryption: Uses Web Crypto API for browser-native encryption
  • 🧂 Automatic Salt Generation: Unique salts per browser/session for enhanced security
  • 💾 Multiple Storage Options: localStorage, sessionStorage, and IndexedDB
  • 🔄 Migration Support: Built-in versioning and migration system
  • ⚛️ React Suspense Ready: Hooks for seamless async storage integration
  • 📦 Zero Dependencies: Only requires Zustand as a peer dependency
  • 🌐 TypeScript First: Full type safety and IntelliSense support
  • 🚀 Performance Optimized: Minimal overhead with caching strategies
  • 🏗️ SSR Compatible: Built-in support for Next.js and other SSR frameworks (v0.3.0+)

Installation

npm install zustand-crypto-persist zustand
# or
yarn add zustand-crypto-persist zustand
# or
pnpm add zustand-crypto-persist zustand

Quick Start

Basic Usage

import { createSimpleCryptoStore } from 'zustand-crypto-persist';

// Create an encrypted store with minimal configuration
const useUserStore = createSimpleCryptoStore(
  'user-store',                    // Store name
  process.env.CRYPTO_SECRET_KEY!,  // Your secret key
  (set) => ({
    user: null,
    isAuthenticated: false,
    login: (user) => set({ user, isAuthenticated: true }),
    logout: () => set({ user: null, isAuthenticated: false }),
  })
);

// Use in your components
function Profile() {
  const user = useUserStore((state) => state.user);
  return <div>Welcome, {user?.name}!</div>;
}

Advanced Configuration

import { createCryptoStore } from 'zustand-crypto-persist';

interface StoreState {
  todos: Todo[];
  addTodo: (todo: Todo) => void;
  removeTodo: (id: string) => void;
}

const useTodoStore = createCryptoStore<StoreState>(
  {
    name: 'todo-store',
    storage: {
      secretKey: process.env.CRYPTO_SECRET_KEY!,
      storageType: 'indexedDB',  // Use IndexedDB for larger data
      encrypt: true,
    },
    version: 1,
    migrations: [
      {
        fromVersion: 0,
        toVersion: 1,
        migrate: (oldData) => ({
          ...oldData,
          todos: oldData.items || [],  // Rename 'items' to 'todos'
        }),
      },
    ],
  },
  (set) => ({
    todos: [],
    addTodo: (todo) => set((state) => ({ 
      todos: [...state.todos, todo] 
    })),
    removeTodo: (id) => set((state) => ({ 
      todos: state.todos.filter(t => t.id !== id) 
    })),
  })
);

Custom Persist Options (v0.2.0+)

You can now pass custom persist options to control hydration, merging, and other behaviors:

const useAuthStore = createCryptoStore<AuthState>(
  {
    name: 'auth-store',
    storage: {
      secretKey: process.env.CRYPTO_SECRET_KEY!,
      storageType: 'localStorage',
    },
    persistOptions: {
      // Skip automatic hydration (useful for SSR)
      skipHydration: true,
      
      // Custom merge strategy
      merge: (persistedState, currentState) => {
        // Only use persisted state if token is still valid
        if (persistedState?.tokenExpiry > Date.now()) {
          return { ...currentState, ...persistedState };
        }
        return currentState;
      },
      
      // Handle rehydration events
      onRehydrateStorage: () => (state, error) => {
        if (error) {
          console.error('Failed to rehydrate:', error);
        } else {
          console.log('Store rehydrated successfully');
          // Clean up expired data
          if (state && !state.isTokenValid()) {
            state.clearUserData();
          }
        }
      },
    },
  },
  (set, get) => ({
    user: null,
    tokenExpiry: 0,
    setUser: (user, tokenExpiry) => set({ user, tokenExpiry }),
    clearUserData: () => set({ user: null, tokenExpiry: 0 }),
    isTokenValid: () => get().tokenExpiry > Date.now(),
  })
);

React Suspense Integration

Using Suspense with Async Storage

import { createAsyncStore, useSuspenseStore } from 'zustand-crypto-persist/react';
import { createIndexedDBCryptoStore } from 'zustand-crypto-persist';

// Create a store with async support
const useAsyncStore = createIndexedDBCryptoStore(
  'async-store',
  process.env.CRYPTO_SECRET_KEY!,
  createAsyncStore((set) => ({
    data: [],
    isLoading: false,
    fetchData: async () => {
      set({ isLoading: true });
      const data = await api.fetchData();
      set({ data, isLoading: false });
    },
  }))
);

// Component using Suspense
function DataList() {
  // This will trigger Suspense during store initialization
  const data = useSuspenseStore(useAsyncStore, (state) => state.data);
  
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// Wrap with Suspense
function App() {
  return (
    <Suspense fallback={<div>Loading encrypted data...</div>}>
      <DataList />
    </Suspense>
  );
}

Crypto Initialization Hook

import { useCryptoInit, withCryptoInit } from 'zustand-crypto-persist/react';

// Hook approach
function MyComponent() {
  const { isInitialized, isInitializing, error } = useCryptoInit();
  
  if (isInitializing) return <div>Initializing encryption...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!isInitialized) return <div>Waiting for crypto system...</div>;
  
  return <YourApp />;
}

// HOC approach
const SecureApp = withCryptoInit(
  YourApp,
  () => <div>Loading...</div>,
  ({ error }) => <div>Error: {error.message}</div>
);

Storage Options

localStorage (Default)

const useStore = createCryptoStore({
  name: 'my-store',
  storage: {
    secretKey: 'your-secret-key',
    storageType: 'localStorage',
  },
}, initialState);

sessionStorage

const useStore = createCryptoStore({
  name: 'session-store',
  storage: {
    secretKey: 'your-secret-key',
    storageType: 'sessionStorage',
  },
}, initialState);

IndexedDB (Recommended for large data)

const useStore = createCryptoStore({
  name: 'indexed-store',
  storage: {
    secretKey: 'your-secret-key',
    storageType: 'indexedDB',
    dbName: 'MyAppDB',       // Optional custom DB name
    storeName: 'MyStore',    // Optional custom store name
  },
}, initialState);

Migration System

Handle data structure changes between versions:

import { createMigration, composeMigrations } from 'zustand-crypto-persist/migration';

const migrations = composeMigrations(
  createMigration(0, 1, (data) => ({
    ...data,
    // Add new field with default value
    settings: data.settings || { theme: 'light' },
  })),
  createMigration(1, 2, (data) => ({
    ...data,
    // Restructure data
    user: {
      profile: data.userProfile,
      preferences: data.settings,
    },
  })),
);

const useStore = createCryptoStore({
  name: 'app-store',
  storage: { secretKey: 'key' },
  version: 2,
  migrations,
}, initialState);

Manual Crypto Operations

For advanced use cases, you can use the crypto utilities directly:

import { encryptData, decryptData, SaltManager } from 'zustand-crypto-persist';

// Encrypt data
const encrypted = await encryptData('sensitive data', 'secret-key');

// Decrypt data
const decrypted = await decryptData(encrypted, 'secret-key');

// Manage salt
const salt = await SaltManager.getSalt();
await SaltManager.clearSalt(); // Clear on logout

API Reference

Core Functions

createCryptoStore(config, initialState)

Creates a Zustand store with encrypted persistence.

Parameters:

  • config: Configuration object
    • name: Store name (string)
    • storage: Storage options
      • secretKey: Encryption key (string)
      • storageType: 'localStorage' | 'sessionStorage' | 'indexedDB'
      • encrypt: Enable encryption (boolean, default: true)
    • version: Store version (number, default: 1)
    • migrations: Array of migration functions
    • persistOptions: Custom persist options (v0.2.0+)
      • skipHydration: Skip automatic hydration (boolean)
      • merge: Custom merge function
      • onRehydrateStorage: Rehydration callback
      • All other Zustand persist options
  • initialState: State creator function

createSimpleCryptoStore(name, secretKey, initialState, persistOptions?)

Creates a basic encrypted store with minimal configuration.

Parameters:

  • name: Store name (string)
  • secretKey: Encryption key (string)
  • initialState: State creator function
  • persistOptions: Optional persist options (v0.2.0+)

createIndexedDBCryptoStore(name, secretKey, initialState, persistOptions?)

Creates an encrypted store using IndexedDB.

Parameters:

  • name: Store name (string)
  • secretKey: Encryption key (string)
  • initialState: State creator function
  • persistOptions: Optional persist options (v0.2.0+)

Storage Functions

initializeCrypto()

Initializes the crypto system. Called automatically but can be called manually for eager initialization.

waitForCryptoInit()

Returns a promise that resolves when crypto is initialized.

isCryptoInitialized()

Check if the crypto system is ready.

React Hooks

useSuspenseStore(store, selector?)

Use a store with React Suspense support.

useCryptoInit()

Hook to check crypto initialization status.

withCryptoInit(Component, Loading?, Error?)

HOC that ensures crypto is initialized before rendering.

Migration Utilities

createMigration(fromVersion, toVersion, migrateFn)

Create a migration function.

composeMigrations(...migrations)

Combine multiple migrations in order.

Security Considerations

⚠️ IMPORTANT SECURITY WARNING ⚠️

The default implementation provides obfuscation, NOT true security:

  • The salt is stored in IndexedDB (accessible via browser DevTools)
  • The secret key is embedded in your JavaScript bundle (visible to anyone)
  • This is suitable ONLY for non-sensitive data that needs basic obfuscation
  • For true security, use password-based encryption (see below)

Default Mode Limitations

  1. Secret Key Exposure:

    • Your secret key is visible in the JavaScript bundle
    • Anyone can extract it using browser DevTools
    • Environment variables don't hide keys from the client
  2. Salt Accessibility:

    • Salt is stored in plain text in IndexedDB
    • Accessible via browser DevTools → Application → IndexedDB
    • Provides uniqueness, not security
  3. Suitable Use Cases:

    • User preferences that aren't sensitive
    • UI state that needs persistence
    • Data you want to obfuscate from casual inspection
    • NOT suitable for: passwords, API keys, personal data, financial info

Browser Storage Limitations

  • Data is only as secure as the browser's storage
  • XSS attacks can still access encrypted data if they have the key
  • Browser extensions can potentially access storage
  • Always use HTTPS to prevent man-in-the-middle attacks

True Security with Password-Based Encryption (WIP)

For sensitive data that requires actual security, use password-based encryption where the user provides the password at runtime.

Why Password-Based Encryption is Secure

  1. No Key in Code: The encryption key is derived from the user's password
  2. PBKDF2 Key Derivation: Makes brute-force attacks computationally expensive
  3. User-Controlled: Only the user knows the password
  4. Memory-Only: Password never needs to be stored

Implementation Example

import { createCryptoStore } from 'zustand-crypto-persist';
import { deriveKeyFromPassword } from 'zustand-crypto-persist/crypto';

interface SecureState {
  sensitiveData: string | null;
  isUnlocked: boolean;
  unlock: (password: string) => Promise<boolean>;
  lock: () => void;
  setSensitiveData: (data: string) => void;
}

// Store for managing the encrypted store instance
let secureStoreInstance: ReturnType<typeof createCryptoStore> | null = null;

export const useSecureStore = create<SecureState>((set, get) => ({
  sensitiveData: null,
  isUnlocked: false,
  
  unlock: async (password: string) => {
    try {
      // Derive key from password
      const { key, salt } = await deriveKeyFromPassword(password);
      
      // Create/recreate the encrypted store with the derived key
      secureStoreInstance = createCryptoStore(
        {
          name: 'secure-user-data',
          storage: {
            secretKey: key,
            salt: salt, // Use the same salt for consistent key derivation
            storageType: 'indexedDB',
          },
        },
        (innerSet) => ({
          data: get().sensitiveData || '',
          setData: (data: string) => {
            innerSet({ data });
            set({ sensitiveData: data });
          },
        })
      );
      
      // Load existing data
      const state = secureStoreInstance.getState();
      set({ sensitiveData: state.data, isUnlocked: true });
      
      return true;
    } catch (error) {
      console.error('Failed to unlock:', error);
      return false;
    }
  },
  
  lock: () => {
    secureStoreInstance = null;
    set({ sensitiveData: null, isUnlocked: false });
  },
  
  setSensitiveData: (data: string) => {
    if (secureStoreInstance) {
      secureStoreInstance.getState().setData(data);
      set({ sensitiveData: data });
    }
  },
}));

// Usage in component
function SecureDataManager() {
  const [password, setPassword] = useState('');
  const { isUnlocked, unlock, lock, sensitiveData } = useSecureStore();
  
  const handleUnlock = async () => {
    const success = await unlock(password);
    if (success) {
      setPassword(''); // Clear password from memory
    } else {
      alert('Invalid password');
    }
  };
  
  if (!isUnlocked) {
    return (
      <div>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Enter password to unlock"
        />
        <button onClick={handleUnlock}>Unlock</button>
      </div>
    );
  }
  
  return (
    <div>
      <h3>Secure Data: {sensitiveData}</h3>
      <button onClick={lock}>Lock</button>
    </div>
  );
}

Password-Based Encryption Utilities

// Example usage of password crypto utilities
import { 
  deriveKeyFromPassword, 
  encryptWithPassword, 
  decryptWithPassword 
} from 'zustand-crypto-persist/crypto';

// Derive a key from password (happens once per session)
const { key, salt } = await deriveKeyFromPassword('user-password');

// Or encrypt/decrypt directly with password
const encrypted = await encryptWithPassword('sensitive data', 'user-password');
const decrypted = await decryptWithPassword(encrypted, 'user-password');

Security Comparison

| Feature | Default Mode | Password-Based Mode | |---------|--------------|-------------------| | Key Storage | In JavaScript bundle | Derived from user password | | Salt Storage | IndexedDB (visible) | Generated per password | | Security Level | Obfuscation only | Actual encryption | | Use Cases | UI preferences, non-sensitive data | Personal data, credentials, sensitive info | | User Experience | Automatic | Requires password entry | | Key Accessibility | Anyone can extract | Only with correct password |

Best Practices for Password-Based Encryption

  1. Password Requirements:

    • Enforce minimum length (12+ characters recommended)
    • Encourage passphrases over complex passwords
    • Never store the password, only use it to derive keys
  2. User Experience:

    • Provide "Remember for this session" option (store derived key in memory)
    • Clear sensitive data on browser close or inactivity
    • Implement proper password reset flows
  3. Additional Security:

    • Use 2FA for password reset flows
    • Implement rate limiting for unlock attempts
    • Consider biometric authentication where available
  4. Implementation Tips:

    • Derive the key once per session and reuse
    • Clear derived keys from memory when locking
    • Use high iteration counts for PBKDF2 (100,000+)

Performance Tips

  1. Use IndexedDB for large datasets - It handles async operations better
  2. Initialize crypto early - Call initializeCrypto() in your app root
  3. Batch updates - Minimize the number of state updates
  4. Use selectors - Only subscribe to the data you need

SSR and Next.js Integration

Overview

Server-Side Rendering (SSR) presents unique challenges for encrypted storage:

  • Crypto APIs are browser-only (no Node.js support)
  • Hydration mismatches when server and client states differ
  • Async initialization of encrypted stores

zustand-crypto-persist provides several strategies to handle these challenges. As of v0.3.0, the library includes built-in SSR compatibility with automatic browser detection.

Basic Next.js Setup

1. Skip Hydration (Simplest Approach)

// app/stores/user-store.ts
import { createSimpleCryptoStore } from 'zustand-crypto-persist';

// Use skipHydration to prevent SSR issues
export const useUserStore = createSimpleCryptoStore(
  'user-store',
  process.env.NEXT_PUBLIC_CRYPTO_KEY!,
  (set) => ({
    user: null,
    isAuthenticated: false,
    login: (user) => set({ user, isAuthenticated: true }),
    logout: () => set({ user: null, isAuthenticated: false }),
  }),
  {
    skipHydration: true, // Prevents hydration mismatches
  }
);

2. Manual Rehydration in useEffect

// app/components/user-profile.tsx
'use client';

import { useEffect, useState } from 'react';
import { useUserStore } from '@/stores/user-store';

export function UserProfile() {
  const [isHydrated, setIsHydrated] = useState(false);
  const user = useUserStore((state) => state.user);

  useEffect(() => {
    // Manually trigger rehydration after mount
    useUserStore.persist.rehydrate();
    setIsHydrated(true);
  }, []);

  if (!isHydrated) {
    return <div>Loading...</div>;
  }

  return <div>Welcome, {user?.name || 'Guest'}!</div>;
}

Advanced Next.js Patterns

1. Layout Component with Crypto Initialization

// app/components/crypto-provider.tsx
'use client';

import { useEffect, useState } from 'react';
import { initializeCrypto, waitForCryptoInit } from 'zustand-crypto-persist';

export function CryptoProvider({ children }: { children: React.ReactNode }) {
  const [isReady, setIsReady] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    // Initialize crypto system on mount
    initializeCrypto()
      .then(() => waitForCryptoInit())
      .then(() => setIsReady(true))
      .catch((err) => setError(err as Error));
  }, []);

  if (error) {
    return (
      <div className="error-boundary">
        <h2>Encryption Error</h2>
        <p>{error.message}</p>
      </div>
    );
  }

  if (!isReady) {
    return (
      <div className="loading-spinner">
        Initializing secure storage...
      </div>
    );
  }

  return <>{children}</>;
}

// app/layout.tsx
import { CryptoProvider } from '@/components/crypto-provider';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <CryptoProvider>
          {children}
        </CryptoProvider>
      </body>
    </html>
  );
}

2. Middleware Pattern for Auth

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// Note: Middleware runs on Edge Runtime, crypto operations must happen client-side
export function middleware(request: NextRequest) {
  // Check for auth cookie (set by client after decryption)
  const isAuthenticated = request.cookies.get('auth-status')?.value === 'true';
  
  if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};

// app/hooks/use-auth-sync.ts
'use client';

import { useEffect } from 'react';
import { useAuthStore } from '@/stores/auth-store';

export function useAuthSync() {
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  useEffect(() => {
    // Sync auth state to cookie for middleware
    document.cookie = `auth-status=${isAuthenticated}; path=/; SameSite=Lax`;
  }, [isAuthenticated]);
}

3. Async Store Pattern with Suspense

// app/stores/async-user-store.ts
import { createAsyncStore } from 'zustand-crypto-persist/react';
import { createIndexedDBCryptoStore } from 'zustand-crypto-persist';

interface UserState {
  user: User | null;
  isLoading: boolean;
  error: Error | null;
  fetchUser: () => Promise<void>;
}

export const useAsyncUserStore = createIndexedDBCryptoStore<UserState>(
  'async-user-store',
  process.env.NEXT_PUBLIC_CRYPTO_KEY!,
  createAsyncStore((set, get) => ({
    user: null,
    isLoading: false,
    error: null,
    fetchUser: async () => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch('/api/user');
        const user = await response.json();
        set({ user, isLoading: false });
      } catch (error) {
        set({ error: error as Error, isLoading: false });
      }
    },
  }))
);

// app/components/user-dashboard.tsx
'use client';

import { Suspense } from 'react';
import { useSuspenseStore } from 'zustand-crypto-persist/react';
import { useAsyncUserStore } from '@/stores/async-user-store';

function UserContent() {
  const user = useSuspenseStore(useAsyncUserStore, (state) => state.user);
  
  if (!user) {
    return <div>No user data available</div>;
  }

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      {/* User dashboard content */}
    </div>
  );
}

export function UserDashboard() {
  return (
    <Suspense fallback={<UserDashboardSkeleton />}>
      <UserContent />
    </Suspense>
  );
}

function UserDashboardSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
      <div className="h-4 bg-gray-200 rounded w-1/2"></div>
    </div>
  );
}

Handling Hydration Mismatches

1. Client-Only Wrapper

// app/components/client-only.tsx
'use client';

import { useEffect, useState } from 'react';

export function ClientOnly({ children }: { children: React.ReactNode }) {
  const [hasMounted, setHasMounted] = useState(false);

  useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) {
    return null;
  }

  return <>{children}</>;
}

// Usage
import { ClientOnly } from '@/components/client-only';
import { UserData } from '@/components/user-data';

export function Page() {
  return (
    <ClientOnly>
      <UserData /> {/* Component that uses encrypted store */}
    </ClientOnly>
  );
}

2. Progressive Enhancement Pattern

// app/stores/settings-store.ts
import { createCryptoStore } from 'zustand-crypto-persist';

interface SettingsState {
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (language: string) => void;
  toggleNotifications: () => void;
}

// Default values that match server expectations
const defaultSettings: Partial<SettingsState> = {
  theme: 'light',
  language: 'en',
  notifications: true,
};

export const useSettingsStore = createCryptoStore<SettingsState>(
  {
    name: 'settings-store',
    storage: {
      secretKey: process.env.NEXT_PUBLIC_CRYPTO_KEY!,
      storageType: 'localStorage',
    },
  },
  (set) => ({
    ...defaultSettings,
    setTheme: (theme) => set({ theme }),
    setLanguage: (language) => set({ language }),
    toggleNotifications: () => set((state) => ({ 
      notifications: !state.notifications 
    })),
  }),
  {
    skipHydration: true,
    onRehydrateStorage: () => (state, error) => {
      if (error) {
        console.error('Failed to rehydrate settings:', error);
        // Fall back to defaults on error
        return defaultSettings;
      }
    },
  }
);

// app/components/theme-provider.tsx
'use client';

import { useEffect, useState } from 'react';
import { useSettingsStore } from '@/stores/settings-store';

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [mounted, setMounted] = useState(false);
  const theme = useSettingsStore((state) => state.theme);

  useEffect(() => {
    setMounted(true);
    // Trigger rehydration after mount
    useSettingsStore.persist.rehydrate();
  }, []);

  // Use default theme during SSR and initial mount
  const currentTheme = mounted ? theme : 'light';

  return (
    <div data-theme={currentTheme}>
      {children}
    </div>
  );
}

Best Practices for SSR

  1. Always use skipHydration: true for encrypted stores to prevent hydration mismatches
  2. Initialize crypto early in your app lifecycle (layout or _app component)
  3. Use client components for components that read from encrypted stores
  4. Provide fallback UI during the initialization phase
  5. Handle errors gracefully - crypto operations can fail in various scenarios
  6. Use progressive enhancement - start with default values, enhance after hydration
  7. Consider using cookies for critical auth state needed during SSR
  8. Test thoroughly in both development and production modes

Common Pitfalls and Solutions

Problem: "Hydration mismatch" errors

// ❌ Bad: Reading encrypted store during SSR
export default function Page() {
  const user = useUserStore((state) => state.user);
  return <div>{user?.name}</div>;
}

// ✅ Good: Client-only component with loading state
'use client';

export default function Page() {
  const [isClient, setIsClient] = useState(false);
  const user = useUserStore((state) => state.user);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  if (!isClient) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

Problem: "Crypto is not defined" on server

// ❌ Bad: Top-level crypto operations
const encrypted = await encryptData('data', 'key'); // Fails on server

// ✅ Good: Crypto operations only in browser
export function EncryptButton() {
  const handleEncrypt = async () => {
    if (typeof window !== 'undefined') {
      const encrypted = await encryptData('data', 'key');
      // ... handle encrypted data
    }
  };
  
  return <button onClick={handleEncrypt}>Encrypt</button>;
}

Problem: Race conditions during initialization

// ✅ Good: Proper initialization sequence
export function useInitializedStore() {
  const [isReady, setIsReady] = useState(false);
  
  useEffect(() => {
    const init = async () => {
      await initializeCrypto();
      await useUserStore.persist.rehydrate();
      setIsReady(true);
    };
    
    init().catch(console.error);
  }, []);
  
  return isReady;
}

Browser Support

  • Chrome/Edge: Full support
  • Firefox: Full support
  • Safari: Full support (14+)
  • Mobile browsers: Full support for modern versions

Requires browser support for:

  • Web Crypto API
  • IndexedDB (for IndexedDB storage)
  • TextEncoder/TextDecoder

Examples

Check out the examples directory for:

  • Next.js integration
  • React Native (with polyfills)
  • Migration examples
  • Advanced patterns

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting PRs.

License

MIT © Vorshim92


Built with ❤️ using TypeScript and Web Crypto API