How to Manage Project Persistence with SQLite and Auto-Save Middleware in Clypra
Clypra implements project persistence through a Tauri-backed SQLite database accessed via a platform API abstraction, with the Project Store (src/store/projectStore.ts) orchestrating auto-save middleware that debounces changes every 500ms and prevents cross-project corruption by capturing project IDs at schedule time.
Clypra is a video editing application built with Tauri that stores project metadata, timeline data, and media assets in a local SQLite database. The frontend TypeScript code never interacts directly with SQLite; instead, it delegates all persistence operations to a thin platform layer that forwards requests to Rust code using the rusqlite crate. This architecture ensures ACID compliance while keeping the frontend framework-agnostic.
Architecture Overview
The Platform API Abstraction
All database interactions flow through src/core/platform/*, which exposes methods like platform.saveProject, platform.renameProject, and platform.deleteProject. These functions act as a thin wrapper that forwards requests to the Rust backend, where actual SQLite queries execute against JSON-encoded project representations. This separation keeps the frontend persistence-agnostic while leveraging SQLite’s transactional guarantees.
The Project Store Facade
The Project Store (src/store/projectStore.ts) serves as the single entry point for persistence logic. It maintains the runtime session and ensures the timeline store remains the source of truth for editable state. When loading projects, the store calls useTimelineStore.hydrateFromProject to populate the timeline without direct mutation, preserving clean separation between the facade and domain layers.
Auto-Save Middleware Implementation
Debouncing with scheduleAutoSave
The auto-save middleware uses scheduleAutoSave to implement a 500ms debounce (AUTO_SAVE_DELAY). When mutating actions like addMediaAsset, updateProject, or removeMediaAsset execute, they trigger this scheduler, which serializes the current in-memory state and invokes platform.saveProject to write back to SQLite.
This debouncing prevents excessive disk I/O during rapid edits while ensuring data durability.
Preventing Cross-Project Corruption
The middleware captures the current project ID at schedule time to prevent race conditions when users switch projects during pending saves. If a user closes Project A and opens Project B while a save for Project A is still debouncing, the captured ID ensures the SQLite write targets the correct project, preventing cross-project corruption.
SQLite Persistence Patterns
Project Lifecycle Operations
The loadProject method reads saved projects from SQLite, deserializes the data, and creates a new runtime session via ProjectSession.ts. The closeProject method clears pending auto-save timers, performs a final save, and disposes the runtime session.
All SQLite interaction happens inside the platform layer, ensuring the frontend deals only with JavaScript objects.
Crash Recovery via IndexedDB
After successful auto-saves, the store creates lightweight snapshots in IndexedDB through saveSnapshot (handled by src/core/runtime/CrashRecoveryService.ts). These snapshots enable crash recovery dialogs to restore user work if the application terminates unexpectedly, providing a secondary safety net beyond SQLite persistence.
Practical Implementation Examples
Create a new project and let auto-save handle persistence automatically:
// Create a new project – auto-save schedules automatically after creation
useProjectStore.getState().createProject('My Vacation', '16:9', 30);
Add media assets and trigger the debounced save:
// Add a media asset – triggers scheduleAutoSave with 500ms debounce
useProjectStore.getState().addMediaAsset({
id: generateId('media'),
name: 'beach.mp4',
type: 'video',
path: '/Users/me/Videos/beach.mp4',
});
Manually trigger auto-save for testing or critical operations:
// Force immediate auto-save consideration
useProjectStore.getState().scheduleAutoSave();
Load an existing project with full state hydration:
// Load project – handles disposal, reset, hydration, and runtime session creation
await useProjectStore.getState().loadProject(savedProjectMeta, {
tracks: savedTracks,
clips: savedClips,
transitions: savedTransitions,
mediaAssets: savedMedia,
});
Gracefully close a project with final persistence:
// Close project – clears timers, final save, disposes runtime
await useProjectStore.getState().closeProject();
Summary
- SQLite persistence in Clypra is accessed exclusively through the platform API (
src/core/platform/*), which forwards requests to Rust code using therusqlitecrate. - The Project Store (
src/store/projectStore.ts) acts as a facade that orchestrates saves viascheduleAutoSave, implementing a 500ms debounce to prevent excessive I/O. - Race condition protection is achieved by capturing project IDs at schedule time, ensuring saves never corrupt the wrong project during rapid switching.
- Crash recovery relies on IndexedDB snapshots created after successful auto-saves, providing a backup restoration mechanism independent of SQLite.
- The timeline store remains the single source of truth for editable state, with hydration occurring only through
hydrateFromProjectduring load operations.
Frequently Asked Questions
How does Clypra prevent data loss during rapid edits?
Clypra uses debounced auto-save middleware with a 500ms delay (AUTO_SAVE_DELAY) that batches rapid changes into a single SQLite write. This prevents excessive disk operations while ensuring the database reflects the latest state shortly after the user stops editing.
Where does Clypra actually store project data on disk?
Project data is stored in a SQLite database managed by the Tauri Rust backend, accessed through the platform API. The JavaScript frontend never directly accesses the database file; all reads and writes flow through platform.saveProject, platform.loadProject, and related methods in src/core/platform/*.
How does the auto-save middleware handle project switching?
The middleware captures the project ID at the moment scheduleAutoSave is invoked. If the user switches projects while a save is pending, the captured ID ensures the write operation targets the original project, preventing cross-contamination between different project files in SQLite.
Can I manually trigger a project save outside the auto-save cycle?
Yes. While mutating actions like addMediaAsset automatically trigger scheduleAutoSave, you can manually invoke useProjectStore.getState().scheduleAutoSave() to force immediate consideration of pending changes, or use closeProject() to ensure a final save before application exit.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →