Overview
CAFH Platform uses localStorage for data persistence. This provides a simple, serverless architecture perfect for MVPs and prototypes.
All data is stored in the browser. Each user’s browser has its own isolated dataset.
Storage Architecture
Implemented in storage.ts with 35+ versioned keys:
const KEYS = {
// Content
BLOG: 'cafh_blog_v1' ,
BLOG_CONFIG: 'cafh_blog_config_v1' ,
CONTENT: 'cafh_content_v1' ,
CUSTOM_PAGES: 'cafh_pages_v1' ,
MEDIA: 'cafh_media_v1' ,
HOME_CONFIG: 'cafh_home_config_v1' ,
MEGA_MENU: 'cafh_menu_v1' ,
// CRM & Communications
CONTACTS: 'cafh_contacts_v1' ,
CRM_LISTS: 'cafh_crm_lists_v1' ,
CAMPAIGNS: 'cafh_campaigns_v1' ,
AUTOMATIONS: 'cafh_automations_v1' ,
AUTOMATION_EXECUTIONS: 'cafh_automation_executions_v1' ,
EMAIL_LOGS: 'cafh_email_logs_v1' ,
EMAIL_METRICS: 'cafh_email_metrics_v1' ,
SMTP_CONFIG: 'cafh_smtp_config_v1' ,
// Events
EVENTS: 'cafh_events_v1' ,
ACTIVITY_EVENTS: 'cafh_activity_events_v1' ,
ACTIVITY_CATS: 'cafh_activity_cats_v1' ,
// Meetings Module
FEEDBACK_QUESTIONS: 'cafh_feedback_q_v1' ,
FEEDBACK_RESPONSES: 'cafh_feedback_r_v1' ,
MEMBER_BADGES: 'cafh_badges_v1' ,
PARTICIPATION: 'cafh_participation_v1' ,
ZOOM_WIDGET: 'cafh_zoom_widget_v1' ,
// User & Session
SESSION: 'cafh_user_session_v1' ,
USER_PREFS: 'cafh_user_prefs_v1' ,
HISTORY: 'cafh_user_history_v1' ,
CONTENT_INTERACTIONS: 'cafh_content_interactions_v1' ,
// System
CHANGE_LOG: 'cafh_changelog_v1' ,
};
Initialization
Data is initialized on app mount:
const initStorage = < T >( key : string , initialData : T ) : T => {
try {
const stored = localStorage . getItem ( key );
if ( ! stored ) {
// First run: seed with mock data
localStorage . setItem ( key , JSON . stringify ( initialData ));
return initialData ;
}
// Subsequent runs: load from storage
return JSON . parse ( stored );
} catch ( e ) {
console . error ( `Error accessing storage for ${ key } ` , e );
return initialData ;
}
};
export const db = {
init : () => {
// Initialize all storage keys with defaults from constants.ts
blogs = initStorage ( KEYS . BLOG , MOCK_BLOG_POSTS );
contacts = initStorage ( KEYS . CONTACTS , MOCK_CONTACTS );
events = initStorage ( KEYS . EVENTS , MOCK_EVENTS );
// ... 30+ more initializations
},
// ... API methods
};
Called in App.tsx:
const App : React . FC = () => {
useEffect (() => {
db . init (); // Initialize on mount
}, []);
// ...
};
CRUD Operations
The db object provides CRUD methods for each entity:
Blog Example
let blogs : BlogPost [] = [];
export const db = {
blog: {
getAll : () : BlogPost [] => blogs ,
getById : ( id : string ) : BlogPost | undefined =>
blogs . find ( b => b . id === id ),
create : ( post : BlogPost ) : void => {
blogs . push ( post );
localStorage . setItem ( KEYS . BLOG , JSON . stringify ( blogs ));
},
update : ( id : string , updates : Partial < BlogPost >) : void => {
const index = blogs . findIndex ( b => b . id === id );
if ( index !== - 1 ) {
blogs [ index ] = { ... blogs [ index ], ... updates };
localStorage . setItem ( KEYS . BLOG , JSON . stringify ( blogs ));
}
},
delete : ( id : string ) : void => {
blogs = blogs . filter ( b => b . id !== id );
localStorage . setItem ( KEYS . BLOG , JSON . stringify ( blogs ));
},
},
};
CRM Example
let contacts : Contact [] = [];
export const db = {
crm: {
contacts: {
getAll : () : Contact [] => contacts ,
create : ( contact : Contact ) : void => {
contacts . push ( contact );
localStorage . setItem ( KEYS . CONTACTS , JSON . stringify ( contacts ));
},
update : ( id : string , updates : Partial < Contact >) : void => {
const index = contacts . findIndex ( c => c . id === id );
if ( index !== - 1 ) {
contacts [ index ] = { ... contacts [ index ], ... updates };
localStorage . setItem ( KEYS . CONTACTS , JSON . stringify ( contacts ));
}
},
search : ( query : string ) : Contact [] =>
contacts . filter ( c =>
c . name . toLowerCase (). includes ( query . toLowerCase ()) ||
c . email . toLowerCase (). includes ( query . toLowerCase ())
),
},
},
};
Storage Limits
localStorage has a 5-10MB limit per origin. Monitor usage as your dataset grows.
Check Current Usage
function getStorageSize () {
let total = 0 ;
for ( let key in localStorage ) {
if ( localStorage . hasOwnProperty ( key )) {
total += localStorage [ key ]. length + key . length ;
}
}
return ( total / 1024 / 1024 ). toFixed ( 2 ) + ' MB' ;
}
console . log ( 'Storage used:' , getStorageSize ());
Optimization Strategies
Compress large fields
Truncate long text fields or move to external storage: // Store full content separately if needed
content : blogPost . content . substring ( 0 , 1000 ) + '...'
Limit history
Keep only recent records: // Keep last 100 emails only
emailLogs = emailLogs . slice ( - 100 );
Remove unused data
Clean up old records periodically: // Remove bounced contacts older than 30 days
contacts = contacts . filter ( c =>
c . status !== 'Bounced' ||
( Date . now () - new Date ( c . createdAt ). getTime ()) < 30 * 24 * 60 * 60 * 1000
);
Data Migration
To migrate to a backend database:
Export current data
function exportAllData () {
const data = {};
for ( let key in localStorage ) {
if ( key . startsWith ( 'cafh_' )) {
data [ key ] = JSON . parse ( localStorage [ key ]);
}
}
console . log ( JSON . stringify ( data , null , 2 ));
}
Set up backend
Replace storage.ts with API calls: export const db = {
blog: {
getAll : async () => {
const res = await fetch ( '/api/blog' );
return res . json ();
},
create : async ( post : BlogPost ) => {
await fetch ( '/api/blog' , {
method: 'POST' ,
body: JSON . stringify ( post ),
});
},
},
};
Import data
Seed your database with exported data
Versioning
Keys are versioned (_v1) to allow schema migrations:
// Old schema
const KEYS_V1 = {
CONTACTS: 'cafh_contacts_v1' ,
};
// New schema with engagementScore
const KEYS_V2 = {
CONTACTS: 'cafh_contacts_v2' ,
};
// Migration
function migrateV1toV2 () {
const oldContacts = JSON . parse ( localStorage . getItem ( 'cafh_contacts_v1' ) || '[]' );
const newContacts = oldContacts . map ( c => ({
... c ,
engagementScore: 0 , // Add new field
}));
localStorage . setItem ( 'cafh_contacts_v2' , JSON . stringify ( newContacts ));
localStorage . removeItem ( 'cafh_contacts_v1' );
}
Best Practices
Never access localStorage directly. Use the db API to ensure consistency and error handling.
localStorage can fail (quota exceeded, private browsing). Always wrap in try-catch.
Ensure data matches TypeScript interfaces before persisting.
Clear on logout (if sensitive)
For sensitive data, clear localStorage on logout: db . auth . logout = () => {
localStorage . clear ();
};
Next Steps
Data Model Explore all data types
API Reference Complete API documentation
Extending Types Add new data structures
Deployment Deploy to production