Multisynq automatically handles session state through snapshots, but what happens when you update your code? This tutorial covers explicit persistence - how to save and restore application data across different versions of your application.
Automatic vs. Explicit Persistence
Built-in session persistence
Works automatically without any code
Preserves state when users leave and return
Lost when code changes (new session created)
Perfect for temporary game sessions
// This data persists automatically between sessions
class GameModel extends Multisynq . Model {
init () {
this . players = new Map ();
this . score = 0 ;
}
}
Any code change creates a new session, losing all previous data.
Session Identity System
Understanding how Multisynq identifies sessions is crucial for persistence:
๐ sessionId Code-dependent session identifier
Combination of appId, name, and code hash
Changes with any code modification
Used for snapshots and live sessions
// These create the same sessionId:
Session . join ({ appId: "myapp" , name: "room1" });
// With identical code
// This creates a different sessionId:
Session . join ({ appId: "myapp" , name: "room1" });
// With modified code (even tiny changes)
๐ persistentId Code-independent persistent identifier
Combination of appId and name only
Survives code changes
Used for persistence lookup
// These share the same persistentId:
Session . join ({ appId: "myapp" , name: "room1" });
// Regardless of code changes
// Different persistentId:
Session . join ({ appId: "myapp" , name: "room2" });
How Persistence Works
First Session Join
When joining a session with a never-seen-before sessionId :
Multisynq looks up persistent data by persistentId
If found, passes it to your modelโs init() method
Your model restores state from persistent data
class MyModel extends Multisynq . Model {
init ( options , persisted ) {
if ( persisted ) {
console . log ( "Restoring from persistent data:" , persisted );
this . restoreState ( persisted );
} else {
console . log ( "Fresh session - no persistent data" );
this . initializeDefaults ();
}
}
}
Saving Persistent Data
Your application decides when to save using persistSession(): save () {
this . persistSession (() => {
return {
version: 1 ,
data: this . collectImportantData (),
timestamp: Date . now ()
};
});
}
Code Update
When you update your code:
New sessionId is generated (code changed)
Same persistentId is used (appId + name unchanged)
Persistent data is loaded into new code version
Basic Implementation
Simple Pattern
Advanced Pattern
Most applications can use this straightforward approach: class SimpleAppModel extends Multisynq . Model {
init ( options , persisted ) {
// Regular setup
this . users = new Map ();
this . content = "" ;
this . lastModified = null ;
if ( persisted ) {
// Restore from persistent data
this . content = persisted . content || "" ;
this . lastModified = persisted . lastModified ;
// Restore users (handle Map serialization)
if ( persisted . users ) {
this . users = new Map ( persisted . users );
}
}
}
save () {
this . persistSession (() => {
return {
content: this . content ,
lastModified: this . lastModified ,
users: Array . from ( this . users . entries ()) // Convert Map to Array
};
});
}
updateContent ( newContent ) {
this . content = newContent ;
this . lastModified = Date . now ();
// Save after important changes
this . save ();
}
}
This pattern works well for simple applications with straightforward data structures.
Data Serialization Requirements
Critical : persistSession() uses stable JSON stringification. Non-JSON types require special handling.
These work automatically: const safeData = {
strings: "text" ,
numbers: 42 ,
booleans: true ,
arrays: [ 1 , 2 , 3 ],
objects: { nested: "value" },
nulls: null
};
this . persistSession (() => safeData );
When to Use Persistence
Applications that benefit from persistence:
Collaborative editors : Documents must survive code updates
Creative tools : User creations are valuable
Configuration apps : Settings should persist
Score tracking : High scores across game updates
Chat applications : Message history preservation
// Example: Collaborative whiteboard
class WhiteboardModel extends Multisynq . Model {
init ( options , persisted ) {
this . shapes = persisted ?. shapes || [];
this . users = new Map ( persisted ?. users || []);
}
addShape ( shape ) {
this . shapes . push ( shape );
this . save (); // Save after important changes
}
save () {
this . persistSession (() => ({
shapes: this . shapes ,
users: Array . from ( this . users . entries ())
}));
}
}
Applications that donโt need persistence:
Simple games : No long-term state to preserve
Temporary demonstrations : Short-lived sessions
Real-time only : No data worth preserving
Prototype applications : Data structure still changing
// Example: Simple multiplayer pong game
class PongModel extends Multisynq . Model {
init () {
// Game state that resets each session
this . ball = { x: 50 , y: 50 , vx: 1 , vy: 1 };
this . players = new Map ();
this . score = { left: 0 , right: 0 };
// No persistence needed - games start fresh
}
}
If you donโt need to preserve data across code changes, snapshots are sufficient.
When to Save Data
Unlike automatic snapshots, you control when to save persistent data . Balance data safety with performance.
๐ฅ Major Changes
โฐ Timer-Based
Save after significant data modifications: class DocumentEditor extends Multisynq . Model {
addDocument ( doc ) {
this . documents . set ( doc . id , doc );
this . save (); // Major change - save immediately
}
deleteDocument ( id ) {
this . documents . delete ( id );
this . save (); // Major change - save immediately
}
updateCursor ( position ) {
this . cursors . set ( this . viewId , position );
// Minor change - don't save for every cursor move
}
}
Error Handling and Recovery
Critical : Implement error handling to prevent data corruption during development.
Safe Loading Pattern
Development Workflow
Protect against corrupted persistent data: class SafeModel extends Multisynq . Model {
init ( options , persisted ) {
if ( persisted ) {
delete this . loadingPersistentDataErrored ;
this . loadingPersistentData = true ;
try {
this . fromSavedData ( persisted );
console . log ( "Successfully loaded persistent data" );
} catch ( error ) {
console . error ( "Error loading persistent data:" , error );
this . loadingPersistentDataErrored = true ;
// Fallback to defaults
this . initializeDefaults ();
} finally {
delete this . loadingPersistentData ;
}
} else {
this . initializeDefaults ();
}
}
save () {
// Don't save while loading (prevents corruption)
if ( this . loadingPersistentData ) return ;
// Don't save if loading failed (prevents overwriting good data)
if ( this . loadingPersistentDataErrored ) return ;
this . persistSession (() => this . toSaveData ());
}
fromSavedData ( persisted ) {
// Validate data structure
if ( ! persisted . version ) {
throw new Error ( "Missing version in persistent data" );
}
if ( persisted . version > 2 ) {
throw new Error ( `Unsupported version: ${ persisted . version } ` );
}
// Restore data
this . documents = new Map ( persisted . documents || []);
this . metadata = persisted . metadata || {};
}
toSaveData () {
return {
version: 2 ,
documents: Array . from ( this . documents . entries ()),
metadata: this . metadata
};
}
}
Version Management
๐ Data Format Evolution
๐ง Migration Strategies
Handle breaking changes safely: class MigrationModel extends Multisynq . Model {
fromSavedData ( persisted ) {
if ( persisted . version === 1 ) {
// Major breaking change: documents now have structure
const oldDocuments = persisted . documents || [];
this . documents = new Map ();
// Migrate old format to new format
oldDocuments . forEach (( oldDoc , index ) => {
const newDoc = {
id: `doc- ${ index } ` ,
title: oldDoc . name || "Untitled" ,
content: oldDoc . text || "" ,
created: oldDoc . timestamp || Date . now (),
modified: oldDoc . timestamp || Date . now ()
};
this . documents . set ( newDoc . id , newDoc );
});
console . log ( `Migrated ${ oldDocuments . length } documents from v1 to v2` );
} else {
// Handle current versions normally
this . documents = new Map ( persisted . documents || []);
}
}
}
Debug Options
Manual Inspection
Enable detailed logging: // In Session.join()
Session . join ({
appId: "myapp" ,
name: "session1" ,
model: MyModel ,
view: MyView ,
debug: "session" // Enables persistence logging
});
// Or via URL parameter
// https://myapp.com/#session1&debug=session
Console output: [Multisynq] Session loaded from persistent data (persistentId: myapp-session1)
[Multisynq] Restoring 1.2KB of persistent data
[Multisynq] Persistent data saved (2.3KB)
Security and Encryption
End-to-End Encryption : Persistent data inherits Multisynqโs security model.
๐ Secure by Default Your data is protected:
All persistent data is encrypted
Only clients with session password can decrypt
Server cannot read your data
Suitable for sensitive information
// This data is automatically encrypted
this . persistSession (() => ({
confidentialDocument: "sensitive content" ,
userSecrets: privateData
}));
โ ๏ธ Password Management Important considerations:
Lost password = lost data (unrecoverable)
Consider password storage strategy
Balance security vs. convenience
// Option 1: User-managed passwords (most secure)
Session . join ({
name: userProvidedSessionName ,
password: userProvidedPassword // User remembers
});
// Option 2: Server-stored passwords (convenient but less secure)
const sessionInfo = await getSessionFromServer ( userId );
Session . join ({
name: sessionInfo . name ,
password: sessionInfo . password // Server provides
});
Best Practices Summary
๐ Planning Think ahead about persistence:
Plan persistence from the start
Canโt add persistence to existing sessions
Consider what data needs to survive updates
Design for data format evolution
// โ
Good: Plan persistence early
class MyModel extends Multisynq . Model {
init ( options , persisted ) {
// Handle both cases from the start
if ( persisted ) {
this . restoreData ( persisted );
} else {
this . initDefaults ();
}
}
}
โก Performance Optimize save frequency:
Save after major changes only
Use timers for burst activity
Keep persistent data minimal
Clean up unnecessary data
// โ
Good: Strategic saving
save () {
this . persistSession (() => ({
// Only essential data
documents: this . getEssentialDocs (),
metadata: this . coreMetadata ,
// Skip: temporary UI state, caches, etc.
}));
}
๐ก๏ธ Safety Handle errors gracefully:
Validate persistent data structure
Handle version mismatches
Donโt save during loading
Test with corrupted data
// โ
Good: Safe error handling
try {
this . fromSavedData ( persisted );
} catch ( error ) {
console . error ( "Load failed:" , error );
this . initDefaults (); // Fallback
this . loadingErrored = true ;
}
๐ Testing Test thoroughly:
Test fresh sessions
Test loading from persistence
Test data migrations
Use separate deployments for testing
// Test both paths
console . log ( persisted ? "Loading saved data" : "Fresh start" );
Real-World Examples
class DocumentEditorModel extends Multisynq . Model {
init ( options , persisted ) {
if ( persisted ) {
this . documents = new Map ( persisted . documents || []);
this . users = new Map ( persisted . users || []);
this . settings = persisted . settings || {};
} else {
this . documents = new Map ();
this . users = new Map ();
this . settings = { theme: "light" , autosave: true };
}
this . setupAutosave ();
}
setupAutosave () {
if ( this . settings . autosave ) {
this . future ( 30000 ). autosave ();
}
}
autosave () {
if ( this . hasUnsavedChanges ) {
this . save ();
this . hasUnsavedChanges = false ;
}
this . setupAutosave (); // Schedule next autosave
}
createDocument ( title , content ) {
const doc = {
id: this . generateId (),
title ,
content ,
created: Date . now (),
modified: Date . now ()
};
this . documents . set ( doc . id , doc );
this . hasUnsavedChanges = true ;
this . save (); // Save immediately for major changes
}
updateDocument ( id , changes ) {
const doc = this . documents . get ( id );
if ( doc ) {
Object . assign ( doc , changes , { modified: Date . now () });
this . hasUnsavedChanges = true ;
// Don't save immediately - let autosave handle it
}
}
save () {
this . persistSession (() => ({
version: 1 ,
documents: Array . from ( this . documents . entries ()),
users: Array . from ( this . users . entries ()),
settings: this . settings
}));
}
}
class CanvasModel extends Multisynq . Model {
init ( options , persisted ) {
this . loadingPersistentData = !! persisted ;
try {
if ( persisted ) {
this . fromSavedData ( persisted );
} else {
this . shapes = [];
this . layers = [{ id: 'default' , name: 'Layer 1' , visible: true }];
this . canvasSize = { width: 800 , height: 600 };
}
} catch ( error ) {
console . error ( "Failed to load canvas data:" , error );
this . shapes = [];
this . layers = [{ id: 'default' , name: 'Layer 1' , visible: true }];
this . canvasSize = { width: 800 , height: 600 };
this . loadingErrored = true ;
} finally {
this . loadingPersistentData = false ;
}
}
addShape ( shape ) {
shape . id = this . generateId ();
shape . created = Date . now ();
this . shapes . push ( shape );
this . save (); // Save after adding shapes
}
deleteShape ( shapeId ) {
const index = this . shapes . findIndex ( s => s . id === shapeId );
if ( index >= 0 ) {
this . shapes . splice ( index , 1 );
this . save (); // Save after deleting shapes
}
}
save () {
if ( this . loadingPersistentData || this . loadingErrored ) return ;
this . persistSession (() => ({
version: 1 ,
shapes: this . shapes ,
layers: this . layers ,
canvasSize: this . canvasSize ,
metadata: {
shapeCount: this . shapes . length ,
lastModified: Date . now ()
}
}));
}
fromSavedData ( persisted ) {
if ( persisted . version !== 1 ) {
throw new Error ( `Unsupported version: ${ persisted . version } ` );
}
this . shapes = persisted . shapes || [];
this . layers = persisted . layers || [];
this . canvasSize = persisted . canvasSize || { width: 800 , height: 600 };
console . log ( `Restored ${ this . shapes . length } shapes` );
}
}
Next Steps
Persistence is essential for applications where user data has long-term value. Plan for it early, implement it safely, and test thoroughly to ensure your users never lose their important work.