src/skills/desktop-storage-electron/SKILL.md
Persistent storage, SQLite databases, and credential management in Electron apps
npx skillsauth add agents-inc/skills desktop-storage-electronInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
electron-storefor typed JSON preferences (small key-value config with schema validation, migrations, and file watching). Usebetter-sqlite3for structured/relational data or anything beyond simple key-value (synchronous, WAL mode, transactions). UsesafeStoragefor encrypting secrets like tokens and API keys via the OS keychain -- it replaces the deprecatedkeytar. All persistent data belongs underapp.getPath("userData"). Never store secrets in plain JSON files.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use safeStorage.encryptString() / safeStorage.decryptString() for secrets -- never store tokens, API keys, or passwords in plain text or in electron-store without OS-level encryption)
(You MUST store all persistent data under app.getPath("userData") -- never write to the app installation directory, which is replaced on updates)
(You MUST enable WAL mode (PRAGMA journal_mode = WAL) when using better-sqlite3 -- it prevents readers from blocking writers and avoids SQLITE_BUSY errors in multi-window apps)
(You MUST call safeStorage.isEncryptionAvailable() before encrypting -- it returns false before the app ready event and on some Linux configurations)
(You MUST rebuild better-sqlite3 for Electron's Node.js version using @electron/rebuild -- mismatched native bindings crash the app)
</critical_requirements>
Auto-detection: electron-store, better-sqlite3, safeStorage, app.getPath, userData, encryptString, decryptString, isEncryptionAvailable, lowdb, JSONFilePreset, persistent storage, credential storage, keytar replacement, electron config, electron preferences, electron database
When to use:
app.getPath() standard directoriesWhen NOT to use:
Key patterns covered:
app.getPath()Electron apps have access to the full filesystem but should store data in OS-designated locations. The right storage solution depends on data shape and sensitivity:
Preferences and small config (theme, window bounds, feature flags): electron-store writes a single JSON file atomically. It is read and written in full on every change, so it is only appropriate for small data (under ~1MB).
Structured or queryable data (chat history, project metadata, analytics): better-sqlite3 provides a synchronous SQLite database with ACID transactions. It handles concurrent reads via WAL mode and scales to gigabytes.
Secrets (OAuth tokens, API keys, passwords): safeStorage uses the OS keychain (macOS Keychain, Windows DPAPI, Linux secret service) to encrypt strings. The encrypted buffer can be stored in electron-store or a file -- only your app can decrypt it on the same machine and user account.
Medium-complexity JSON data (todo lists, small document stores): lowdb provides a file-backed JavaScript object with native array methods. Simpler than SQLite for JSON-shaped data that does not need relational queries.
Key principle: Storage runs in the main process. Renderers request data via IPC. Never give renderers direct filesystem or database access.
</philosophy>Use for small key-value configuration that persists across sessions. Supports schema validation, defaults, and migrations.
import Store from "electron-store";
interface AppSettings {
theme: "light" | "dark" | "system";
windowBounds: { width: number; height: number; x?: number; y?: number };
recentFiles: string[];
fontSize: number;
}
const DEFAULT_WIDTH = 1200;
const DEFAULT_HEIGHT = 800;
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 72;
const DEFAULT_FONT_SIZE = 14;
const store = new Store<AppSettings>({
defaults: {
theme: "system",
windowBounds: { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT },
recentFiles: [],
fontSize: DEFAULT_FONT_SIZE,
},
schema: {
fontSize: {
type: "number",
minimum: MIN_FONT_SIZE,
maximum: MAX_FONT_SIZE,
},
},
});
Why good: Type-safe generic parameter ensures .get() and .set() are checked at compile time, named constants for all limits, schema rejects invalid values at write time
See examples/core.md for migrations, file watching, dot-notation access, and renderer integration via IPC.
Use for structured data that benefits from queries, indexes, or transactions. Always enable WAL mode.
import Database from "better-sqlite3";
import { app } from "electron";
import path from "node:path";
const DB_FILE = "app-data.db";
const db = new Database(path.join(app.getPath("userData"), DB_FILE));
// Performance pragmas -- set once at connection open
db.pragma("journal_mode = WAL");
db.pragma("synchronous = NORMAL");
db.pragma("foreign_keys = ON");
Why good: WAL mode allows concurrent reads during writes (essential for multi-window apps), synchronous = NORMAL balances safety and speed, foreign keys enforce referential integrity
See examples/sqlite.md for prepared statements, transactions, bulk inserts, and schema migrations.
Use for secrets (tokens, API keys, passwords). The encrypted buffer is opaque -- only your app on the same machine and user account can decrypt it.
import { safeStorage, app } from "electron";
import Store from "electron-store";
const credentialStore = new Store<Record<string, string>>({
name: "credentials",
});
function saveSecret(key: string, plainText: string): void {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error("OS encryption is not available");
}
const encrypted = safeStorage.encryptString(plainText);
credentialStore.set(key, encrypted.toString("base64"));
}
function loadSecret(key: string): string | null {
const stored = credentialStore.get(key);
if (!stored) return null;
const buffer = Buffer.from(stored, "base64");
return safeStorage.decryptString(buffer);
}
Why good: Secrets are encrypted via the OS keychain before being persisted, base64 encoding allows storing the buffer in JSON, explicit availability check prevents crashes on unsupported systems
See examples/core.md for the full credential manager pattern and async API usage.
All persistent data belongs under app.getPath("userData"). Use other paths for specific purposes.
import { app } from "electron";
// User-specific persistent data (config, databases, credentials)
const userDataDir = app.getPath("userData");
// macOS: ~/Library/Application Support/<AppName>
// Windows: %APPDATA%/<AppName>
// Linux: ~/.config/<AppName>
// Temporary files (cache, downloads in progress)
const tempDir = app.getPath("temp");
// Log files
const logsDir = app.getPath("logs");
// User's documents, downloads, desktop (for file save dialogs)
const documentsDir = app.getPath("documents");
const downloadsDir = app.getPath("downloads");
Key point: The userData directory survives app updates. The app installation directory does not -- writing data there causes data loss on update.
Use when data is JSON-shaped but too complex for flat key-value (nested arrays, document collections) and does not need relational queries.
import { JSONFilePreset } from "lowdb/node";
import { app } from "electron";
import path from "node:path";
interface ProjectData {
projects: Array<{ id: string; name: string; lastOpened: string }>;
settings: { sortBy: "name" | "lastOpened" };
}
const DB_FILE = "projects.json";
const defaultData: ProjectData = {
projects: [],
settings: { sortBy: "lastOpened" },
};
const db = await JSONFilePreset<ProjectData>(
path.join(app.getPath("userData"), DB_FILE),
defaultData,
);
// Read
const recent = db.data.projects.toSorted((a, b) =>
b.lastOpened.localeCompare(a.lastOpened),
);
// Write (mutate then persist)
db.data.projects.push({
id: "abc",
name: "New Project",
lastOpened: new Date().toISOString(),
});
await db.write();
Why good: Plain JavaScript data access (no query language), type-safe with generics, file I/O only on explicit .write() call
When to prefer SQLite instead: Data exceeds ~10MB, you need indexes or joins, you need concurrent write safety, or you need partial reads (lowdb loads the entire file into memory).
</patterns><decision_framework>
What kind of data?
|
+-- User preferences / small config (theme, window size, feature flags)?
| +-- electron-store (JSON file, schema validation, migrations)
|
+-- Secrets (tokens, API keys, passwords)?
| +-- safeStorage + electron-store or file
| +-- Never plain text, never unencrypted electron-store
|
+-- Structured / relational data (records, queries, indexes)?
| +-- better-sqlite3 (WAL mode, transactions, scales to GB)
|
+-- JSON document collections (nested objects, no joins needed)?
| +-- Small (<10MB) -> lowdb
| +-- Large or concurrent writes -> better-sqlite3 with JSON columns
|
+-- Temporary / cache data?
| +-- app.getPath("temp") + regular file I/O
|
+-- Session-only state (lost on quit)?
+-- In-memory (no persistence needed)
| Criteria | electron-store | better-sqlite3 | | ----------------- | -------------------------- | ----------------------------------- | | Data shape | Flat key-value, small JSON | Relational, structured records | | Data size | < 1MB | Up to several GB | | Query capability | Get by key, dot-notation | Full SQL, indexes, joins | | Concurrent access | Single process only | WAL mode supports multi-window | | Schema evolution | Migrations by semver | SQL ALTER TABLE / migration scripts | | Setup complexity | Zero (pure JS) | Native module rebuild required | | Best for | Preferences, feature flags | Chat history, project data, logs |
| Feature | safeStorage | electron-store encryptionKey | | ------------------- | ------------------------------ | ----------------------------------- | | Security level | OS keychain (strong) | Obfuscation only (weak) | | Key management | OS manages keys | Key embedded in source code | | Use for secrets | Yes | No -- not actual encryption | | Use for obfuscation | Overkill | Yes -- prevents casual file reading | | Platform support | macOS, Windows, Linux (varies) | All platforms |
</decision_framework>
Detailed Resources:
<red_flags>
Critical Security Issues:
electron-store's encryptionKey option for actual secrets -- it is obfuscation, not encryption. The key is in your source code.Architecture Issues:
SQLITE_BUSY errors when reading and writing concurrently@electron/rebuild -- native module version mismatch crashes the app at startupsafeStorage.isEncryptionAvailable() before encrypting -- crashes on Linux without a secret serviceCommon Mistakes:
safeStorage methods before app.whenReady() -- encryption is unavailable until the app is readydb.close() on before-quit -- risks WAL file corruptionbetter-sqlite3 transactions -- the transaction commits at the first await, not at function endasarUnpack for better-sqlite3 in packaged builds -- the native binary fails to load from inside ASAR archivesBuffer objects directly in electron-store -- they serialize incorrectly. Convert to base64 strings.Gotchas & Edge Cases:
electron-store requires Electron 30+ and is ESM-only (no CommonJS)safeStorage on Windows (DPAPI) protects data per-user but not per-app -- another app running as the same user could theoretically decryptsafeStorage on Linux depends on the desktop environment's secret service (gnome-keyring, KWallet) -- falls back to plaintext if none is availableelectron-store's schema validation uses JSON Schema draft-2020-12 via ajv -- not ZodObject.groupBy on better-sqlite3 result rows works but rows are plain objects with a null prototype -- use Object.hasOwn() not hasOwnProperty</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use safeStorage.encryptString() / safeStorage.decryptString() for secrets -- never store tokens, API keys, or passwords in plain text or in electron-store without OS-level encryption)
(You MUST store all persistent data under app.getPath("userData") -- never write to the app installation directory, which is replaced on updates)
(You MUST enable WAL mode (PRAGMA journal_mode = WAL) when using better-sqlite3 -- it prevents readers from blocking writers and avoids SQLITE_BUSY errors in multi-window apps)
(You MUST call safeStorage.isEncryptionAvailable() before encrypting -- it returns false before the app ready event and on some Linux configurations)
(You MUST rebuild better-sqlite3 for Electron's Node.js version using @electron/rebuild -- mismatched native bindings crash the app)
Failure to follow these rules will cause data loss, security vulnerabilities, or application crashes.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events