Skip to content

Storage Engine (@repo/core-storage) ​

Architectural Foundation: PouchDB Β· CouchDB Β· IndexedDB API (MDN)

Description: Enterprise storage engine providing AES-encrypted LocalStorage, strict-gatekeeper IndexedDB, and offline-first PouchDB with bi-directional CouchDB cloud synchronization.

@repo/core-storage is the Enterprise-grade, multi-tool storage engine for the Eigen Monorepo.

It provides a unified set of strictly-typed, secure, and fault-tolerant storage mechanisms tailored for React applications. It enforces strict Inversion of Control (IoC)β€”the core engine knows absolutely nothing about your application's business domains; instead, the consuming apps inject their own configurations and types.

This package provides three primary storage solutions:

  1. Secure Local Storage (Strict Key-Gatekeeping & AES encryption)
  2. Secure IndexedDB (For larger key-value payloads)
  3. Offline-First PouchDB (For document-oriented, bi-directional sync data)

πŸ”’ Secure Key-Value Storage (LocalStorage & IndexedDB) ​

Browser storage is notoriously vulnerable to XSS attacks and pollution. The LocalStorageService and IndexedDBService implement a strict Gatekeeper pattern to solve this.

By forcing developers to register every key explicitly into either plainTextKeys or encryptedKeys, the engine guarantees:

  1. No unapproved or rogue keys can ever be written or read (throws a Security Exception).
  2. Highly sensitive tokens (e.g., JWTs) are automatically routed through the @repo/utils AES Encryption pipeline before touching the disk.

Architecture ​

Usage & Implementation ​

typescript
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';

// 1. Define allowed keys (Strict Type Safety)
export type AppStorageKey = 'THEME' | 'ACCESS_TOKEN' | 'OFFLINE_CACHE';

// 2. Instantiate Local Storage
export const appStorage = createLocalStorage<AppStorageKey>({
  plainTextKeys: new Set(['THEME']),
  encryptedKeys: new Set(['ACCESS_TOKEN']), // Auto AES encrypted
});

// 3. Usage
await appStorage.setItem('ACCESS_TOKEN', 'ey...'); // Encrypted on disk
const theme = await appStorage.getItem('THEME'); // Plaintext on disk

βœ… Do's and ❌ Don'ts ​

  • βœ… DO use TypeScript Literal Types for your storage keys (type Keys = 'A' | 'B') to get full IntelliSense.
  • βœ… DO place Session/Auth tokens exclusively inside the encryptedKeys Set.
  • ❌ DON'T use native window.localStorage directly anywhere in your React components. It bypasses our encryption and gatekeeper logic.
  • ❌ DON'T mix domain data. Keep UI preferences (Theme, Sidebar state) in LocalStorage, and large datasets (Offline Caches) in IndexedDB.

πŸ”„ Offline-First Document Storage (PouchDB & CouchDB) ​

For complex, document-oriented data that requires fault-tolerance, offline support, and bi-directional cloud synchronization, we use the PouchDBManager. This layer is powered by PouchDB syncing to CouchDB.

Architecture ​

1. Initialization (IoC Factory) ​

The PouchDBManager acts as a central singleton. It registers and manages all database instances. If a remote URL is provided, it automatically handles background synchronization.

typescript
import { PouchDBManager } from '@repo/core-storage';
import type { Item } from './types';

export const dbManager = new PouchDBManager();

export const itemDB = dbManager.register<Item>({
  localName: 'items_db',
  remoteUrl: 'http://admin:password@localhost:5984/items_db',
});

2. CRUD & MongoDB-style Queries ​

The registered database returns a PouchService instance. This wrapper fully abstracts the raw PouchDB API into clean, Promise-based helpers, auto-handling _rev conflicts.

MethodDescription
create(data)Inserts a new document. Auto-generates _id if omitted.
update(id, data)Auto-fetches the latest _rev to merge payloads cleanly.
delete(id)Auto-fetches the latest _rev to safely remove the document.
getAll()Retrieves all documents (filters out internal _design/ docs).
find(options)Queries using MongoDB-style selectors (via pouchdb-find).
typescript
// Example: Querying data using selectors
const expensiveItems = await itemDB.find({
  selector: { price: { $gt: 100 }, category: 'electronics' },
});

3. Real-Time Reactivity (onChange Pub/Sub) ​

We implemented a Publisher-Subscriber (Pub/Sub) pattern inside the wrapper to handle real-time data changes efficiently. The wrapper maintains a single background connection to the changes feed and broadcasts events to all React subscribers.

tsx
import { useEffect, useCallback, useState } from 'react';
import { itemDB } from '../core/db';

export function InventoryList() {
  const [items, setItems] = useState([]);

  const loadData = useCallback(async () => {
    const data = await itemDB.getAll();
    setItems(data);
  }, []);

  useEffect(() => {
    loadData();

    // Subscribe to background sync mutations
    const unsubscribe = itemDB.onChange(() => {
      loadData();
    });

    // CRITICAL: Prevent memory leaks
    return () => unsubscribe();
  }, [loadData]);
}

4. Envelope Pattern (PouchEnvelopeDBManager) ​

If you want to store multiple types of entities (e.g. items, bookings, activities) in a single CouchDB/PouchDB database to simplify sync setup, use the Envelope Pattern.

Instead of PouchDBManager, instantiate a PouchEnvelopeDBManager. It provides the exact same PouchService API (CRUD + Find), but automatically wraps documents into an envelope format internally: { _id: "entityName:businessId", entity: "entityName", data: { ... } }.

typescript
import { PouchEnvelopeDBManager } from '@repo/core-storage';
import type { ItemEntity, BookingEntity } from './types';

export const envelopeDbManager = new PouchEnvelopeDBManager();

// Registers to the SAME database 'master_db', but scoped to 'item'
export const itemDB = envelopeDbManager.register<ItemEntity>(
  {
    localName: 'master_db',
    remoteUrl: 'http://admin:pass@localhost:5984/master_db',
  },
  'item',
);

// Registers to the SAME database 'master_db', but scoped to 'booking'
export const bookingDB = envelopeDbManager.register<BookingEntity>(
  {
    localName: 'master_db',
    remoteUrl: 'http://admin:pass@localhost:5984/master_db',
  },
  'booking',
);

// API usage remains identical!
await itemDB.create({ _id: '123', name: 'Widget' }); // Stored as "item:123"
const items = await itemDB.getAll(); // Only returns documents where entity === 'item'

// Unique to PouchEnvelopeService: Cross-field keyword searching
const results = await itemDB.search('widget keyword', ['data.name', 'data.sku']);

βœ… Do's and ❌ Don'ts for PouchDB ​

  • βœ… DO use .onChange() to make your UI reactive to background cloud syncs.
  • βœ… DO return the unsubscribe function in your useEffect cleanup block to prevent severe memory leaks.
  • ❌ DON'T use db.raw.changes() inside your React components. It creates zombie WebSocket connections and tightly couples your UI to PouchDB's specific API.
  • ❌ DON'T pass the _rev property manually when updating or deleting. The wrapper's update() and delete() methods handle revision fetching automatically.

⚠️ Troubleshooting ​

CouchDB CORS Infinite Retries ​

By providing a remoteUrl, the engine runs bi-directional sync in the background (live: true, retry: true). Fault tolerance is guaranteed: if CouchDB crashes, local reads/writes continue uninterrupted.

However, if your browser blocks CouchDB sync with a CORS error, PouchDB will misinterpret this as a network failure and enter an infinite retry loop, flooding your Network tab.

DO NOT try to fix this in the frontend Vite config or proxy! This is strictly a CouchDB server policy issue. You must enable CORS directly on the CouchDB cluster (editing its local.ini or via its dashboard) to allow origins, credentials, and headers.