Setting Up Persistence
Persistence saves your database to browser storage so data survives page reloads. This guide walks through configuration options and common patterns.
Prerequisites
Before starting, you should:
Overview
We'll configure persistence by:
- Choosing a storage backend
- Enabling automatic persistence
- Controlling save timing
- Handling errors gracefully
Step 1: Choose a Storage Backend
Two built-in options are available:
IndexedDB (recommended for most cases):
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' }
})
localStorage (simpler but limited):
const db = await createDatabase({
persist: { key: 'my-app', storage: 'localstorage' }
})
| Feature | IndexedDB | localStorage |
|---|---|---|
| Storage limit | ~50MB+ | ~5MB |
| Performance | Better for large data | Simple and fast for small data |
| Async-friendly | Yes | Blocking but works |
| Browser support | Modern browsers | All browsers |
Use IndexedDB unless you have a specific reason for localStorage.
Step 2: Enable Automatic Persistence
With persist configured, auto-save is enabled by default. Changes save automatically after a debounce period:
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' }
// autoSave: true (default when persist is set)
// autoSaveDebounce: 1000 (default: 1 second)
})
// Make changes - they auto-save after 1 second of inactivity
db.run('INSERT INTO notes (text) VALUES (?)', ['Hello'])
db.run('INSERT INTO notes (text) VALUES (?)', ['World'])
// Both changes save in a single write
Step 3: Control Save Timing
Adjust the debounce for your use case:
// Save quickly (100ms) for real-time feel
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' },
autoSaveDebounce: 100
})
// Save slowly (5s) to reduce writes
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' },
autoSaveDebounce: 5000
})
// Disable auto-save for manual control
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' },
autoSave: false
})
// Manual save when you decide
await db.save()
Step 4: Handle Errors Gracefully
Storage can fail (quota exceeded, private browsing, etc.). The library logs errors but doesn't crash:
const db = await createDatabase({
persist: { key: 'my-app', storage: 'indexeddb' }
})
// Auto-save failures are logged to console.error
// Manual save throws so you can handle it:
try {
await db.save()
} catch (error) {
console.error('Save failed:', error.message)
// Show user a warning, suggest data export, etc.
}
Complete Example
A typical app initialization with persistence:
import { createDatabase } from '@motioneffector/sql'
import type { Database, Migration } from '@motioneffector/sql'
const migrations: Migration[] = [
{
version: 1,
up: 'CREATE TABLE notes (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, created_at TEXT)'
}
]
async function initApp(): Promise<Database> {
// Create persistent database
const db = await createDatabase({
persist: { key: 'notes-app', storage: 'indexeddb' },
autoSaveDebounce: 500
})
// Run migrations
await db.migrate(migrations)
// Data loaded automatically from storage
const noteCount = db.get<{ count: number }>('SELECT COUNT(*) as count FROM notes')
console.log(`Loaded ${noteCount?.count ?? 0} notes from storage`)
return db
}
// Usage
const db = await initApp()
// Add a note - auto-saves after 500ms
db.run('INSERT INTO notes (text, created_at) VALUES (?, ?)', [
'My first note',
new Date().toISOString()
])
Variations
Custom Storage Adapter
Implement StorageAdapter to store anywhere:
import type { StorageAdapter } from '@motioneffector/sql'
const remoteStorage: StorageAdapter = {
async getItem(key: string): Promise<Uint8Array | null> {
const response = await fetch(`/api/storage/${key}`)
if (!response.ok) return null
return new Uint8Array(await response.arrayBuffer())
},
async setItem(key: string, value: Uint8Array): Promise<void> {
await fetch(`/api/storage/${key}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/octet-stream' },
body: value
})
},
async removeItem(key: string): Promise<void> {
await fetch(`/api/storage/${key}`, { method: 'DELETE' })
}
}
const db = await createDatabase({
persist: { key: 'user-123-db', storage: remoteStorage }
})
Discarding Changes
Reload from storage to undo uncommitted changes:
// User makes some edits
db.run('DELETE FROM important_data WHERE id = 1')
// User clicks "Discard Changes"
await db.load()
// Database reverts to last saved state
Multiple Databases
Each database needs a unique key:
const userDb = await createDatabase({
persist: { key: 'user-data', storage: 'indexeddb' }
})
const cacheDb = await createDatabase({
persist: { key: 'cache-data', storage: 'indexeddb' }
})
Forcing Immediate Save
When auto-save isn't fast enough:
// User clicks "Save Now"
await db.save()
console.log('Saved!')
// Before closing tab
window.addEventListener('beforeunload', () => {
db.save() // Fire and forget
})
Troubleshooting
Data Not Persisting
Symptom: Data gone after page reload.
Cause: persist option not configured, or storage failed.
Solution: Check that persist is passed to createDatabase(). Check console for storage errors.
Quota Exceeded
Symptom: QuotaExceededError in console.
Cause: Storage limit reached (~5MB for localStorage).
Solution: Switch to IndexedDB, or implement data cleanup logic. Export and clear old data.
Private Browsing Issues
Symptom: Errors about storage being unavailable.
Cause: Safari and some browsers restrict storage in private mode.
Solution: Catch the error and warn users that data won't persist. Consider offering export as alternative.
See Also
- Persistence Concept - How persistence works internally
- Export and Import - File-based backup alternative
- Persistence API - save() and load() reference