5. Adding Persistence
Store and retrieve data across app restarts using lightshell.store.
Most apps need to remember things between sessions. User preferences, saved data, recent files — all of this requires persistence. LightShell provides lightshell.store, a key-value storage API backed by bbolt, so you can save and load data without touching the file system directly.
Why Not Just Use Files?
Section titled “Why Not Just Use Files?”You could use lightshell.fs.readFile and lightshell.fs.writeFile to persist data. It works, but it is tedious:
// The hard way — manual file-based persistenceconst dataDir = await lightshell.app.dataDir()const path = dataDir + '/todos.json'
// Saveawait lightshell.fs.writeFile(path, JSON.stringify(todos))
// Loadconst raw = await lightshell.fs.readFile(path)const todos = JSON.parse(raw)You have to manage file paths, serialize to JSON, handle missing files on first launch, and deal with write errors. lightshell.store eliminates all of that.
The Store API
Section titled “The Store API”lightshell.store is a persistent key-value store. Values are automatically serialized and deserialized — you pass in objects and get objects back. The data is saved to a database file in your app’s data directory and survives restarts.
// Save a valueawait lightshell.store.set('username', 'Alice')
// Read it backconst name = await lightshell.store.get('username')console.log(name) // "Alice"
// Works with objects, arrays, numbers — anything JSON-serializableawait lightshell.store.set('settings', { theme: 'dark', fontSize: 14 })const settings = await lightshell.store.get('settings')console.log(settings.theme) // "dark"No file paths. No JSON.stringify. Just get and set.
Building a Persistent Todo List
Section titled “Building a Persistent Todo List”Let’s build a todo list that remembers your items across app restarts. Start with the HTML structure in src/index.html:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todo List</title> <link rel="stylesheet" href="style.css"></head><body> <h1>Todos</h1> <div class="input-row"> <input type="text" id="todo-input" placeholder="What needs to be done?"> <button id="add-btn">Add</button> </div> <ul id="todo-list"></ul> <div class="footer"> <span id="count">0 items</span> <button id="clear-btn">Clear All</button> </div> <script src="app.js"></script></body></html>Loading Todos on Startup
Section titled “Loading Todos on Startup”In src/app.js, start by loading any saved todos when the app launches:
let todos = []
async function loadTodos() { const saved = await lightshell.store.get('todos') if (saved) { todos = saved } render()}lightshell.store.get returns null if the key does not exist, so on first launch saved will be null and we start with an empty array.
Saving Todos
Section titled “Saving Todos”Every time the list changes, save it:
async function saveTodos() { await lightshell.store.set('todos', todos)}That is the entire persistence layer. One line.
Adding and Deleting Items
Section titled “Adding and Deleting Items”Wire up the input and buttons:
async function addTodo() { const input = document.getElementById('todo-input') const text = input.value.trim() if (!text) return
todos.push({ text, done: false }) input.value = '' await saveTodos() render()}
async function deleteTodo(index) { todos.splice(index, 1) await saveTodos() render()}
async function toggleTodo(index) { todos[index].done = !todos[index].done await saveTodos() render()}Rendering the List
Section titled “Rendering the List”function render() { const list = document.getElementById('todo-list') list.innerHTML = todos.map((todo, i) => ` <li class="${todo.done ? 'done' : ''}"> <input type="checkbox" ${todo.done ? 'checked' : ''} onchange="toggleTodo(${i})"> <span>${todo.text}</span> <button onclick="deleteTodo(${i})">Delete</button> </li> `).join('')
document.getElementById('count').textContent = `${todos.filter(t => !t.done).length} items remaining`}Clearing All Todos
Section titled “Clearing All Todos”The clear button deletes the stored data entirely:
async function clearAll() { const confirmed = await lightshell.dialog.confirm( 'Clear Todos', 'Are you sure you want to delete all todos?' ) if (!confirmed) return
todos = [] await lightshell.store.delete('todos') render()}Here we use lightshell.store.delete to remove the key entirely rather than saving an empty array. Both approaches work — delete is cleaner when you want to reset to a “never been set” state.
Wiring It Up
Section titled “Wiring It Up”Connect the event listeners and load on startup:
document.getElementById('add-btn').addEventListener('click', addTodo)document.getElementById('clear-btn').addEventListener('click', clearAll)document.getElementById('todo-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') addTodo()})
loadTodos()Run lightshell dev, add a few todos, then quit and relaunch. Your todos are still there.
Other Store Methods
Section titled “Other Store Methods”Checking If a Key Exists
Section titled “Checking If a Key Exists”Use has to check for existence without reading the value:
const hasTodos = await lightshell.store.has('todos')if (hasTodos) { console.log('Found saved todos')}Listing Keys
Section titled “Listing Keys”Use keys to list all stored keys, optionally filtered by prefix:
// Store some settingsawait lightshell.store.set('settings.theme', 'dark')await lightshell.store.set('settings.fontSize', 14)await lightshell.store.set('user.name', 'Alice')
// List all keys starting with "settings."const settingKeys = await lightshell.store.keys('settings.*')console.log(settingKeys) // ["settings.theme", "settings.fontSize"]
// List all keysconst allKeys = await lightshell.store.keys()console.log(allKeys) // ["settings.theme", "settings.fontSize", "user.name"]Clearing Everything
Section titled “Clearing Everything”clear removes all stored data. Use with caution:
await lightshell.store.clear()Common Patterns
Section titled “Common Patterns”Persisting App Settings
Section titled “Persisting App Settings”const defaults = { theme: 'light', fontSize: 14, sidebarOpen: true }
async function loadSettings() { const saved = await lightshell.store.get('settings') return { ...defaults, ...saved }}
async function saveSetting(key, value) { const settings = await loadSettings() settings[key] = value await lightshell.store.set('settings', settings)}Recently Opened Files
Section titled “Recently Opened Files”async function addRecentFile(path) { let recent = (await lightshell.store.get('recentFiles')) || [] recent = recent.filter(f => f !== path) // remove duplicates recent.unshift(path) // add to front recent = recent.slice(0, 10) // keep last 10 await lightshell.store.set('recentFiles', recent)}You have learned how to:
- Save data with
lightshell.store.set— no file paths or serialization needed - Load data with
lightshell.store.get— returnsnullif the key does not exist - Delete data with
lightshell.store.deleteandlightshell.store.clear - Check and list keys with
lightshell.store.hasandlightshell.store.keys - Build persistent apps that remember state across restarts
The store handles all the complexity of file paths, serialization, and atomicity behind a simple key-value interface.
Next, let’s connect to external APIs.