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:
- Secure Local Storage (Strict Key-Gatekeeping & AES encryption)
- Secure IndexedDB (For larger key-value payloads)
- 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:
- No unapproved or rogue keys can ever be written or read (throws a
Security Exception). - Highly sensitive tokens (e.g., JWTs) are automatically routed through the
@repo/utilsAES Encryption pipeline before touching the disk.
Architecture β
Usage & Implementation β
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
encryptedKeysSet. - β DON'T use native
window.localStoragedirectly 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.
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.
| Method | Description |
|---|---|
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). |
// 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.
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: { ... } }.
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
unsubscribefunction in youruseEffectcleanup 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
_revproperty manually when updating or deleting. The wrapper'supdate()anddelete()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.inior via its dashboard) to alloworigins,credentials, andheaders.