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

1

First Session Join

When joining a session with a never-seen-before sessionId:

  1. Multisynq looks up persistent data by persistentId
  2. If found, passes it to your model’s init() method
  3. 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();
        }
    }
}
2

Saving Persistent Data

Your application decides when to save using persistSession():

save() {
    this.persistSession(() => {
        return {
            version: 1,
            data: this.collectImportantData(),
            timestamp: Date.now()
        };
    });
}
3

Code Update

When you update your code:

  1. New sessionId is generated (code changed)
  2. Same persistentId is used (appId + name unchanged)
  3. Persistent data is loaded into new code version

Basic Implementation

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

When to Save Data

Unlike automatic snapshots, you control when to save persistent data. Balance data safety with performance.

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.

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

Debugging Tools

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

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.