conf vs configstore vs electron-store: Persistent Config Storage in Node.js (2026)
TL;DR
conf is the modern config store for Node.js apps — JSON file storage with JSON Schema validation, migrations, dot-notation access, encryption support, and TypeScript generics. configstore is the simple, mature config store — stores JSON in ~/.config/, used by Yeoman and many CLI tools, minimal API. electron-store is conf adapted for Electron — same API as conf but works correctly in Electron's main and renderer processes, handles Electron-specific paths. In 2026: conf for Node.js CLI tools, electron-store for Electron apps, configstore for simple legacy needs.
Key Takeaways
- conf: ~5M weekly downloads — schema validation, migrations, encryption, TypeScript
- configstore: ~5M weekly downloads — simple, mature,
~/.config/storage, minimal API - electron-store: ~3M weekly downloads — conf for Electron, handles app userData path
- All three store config as JSON files on disk — persists across CLI/app restarts
- conf and electron-store share the same API — electron-store just adds Electron integration
- configstore is simpler but lacks validation, migrations, and encryption
conf
conf — modern config store:
Basic usage
import Conf from "conf"
const config = new Conf({
projectName: "pkgpulse-cli",
})
// Set values:
config.set("apiKey", "pk_live_abc123")
config.set("defaults.format", "table")
config.set("defaults.limit", 10)
// Get values:
config.get("apiKey") // "pk_live_abc123"
config.get("defaults.format") // "table"
config.get("defaults.limit") // 10
// Get with default:
config.get("theme", "dark") // "dark" (not set yet)
// Check existence:
config.has("apiKey") // true
// Delete:
config.delete("apiKey")
// Clear all:
config.clear()
// Get all config:
console.log(config.store)
// → { defaults: { format: "table", limit: 10 } }
// Config file path:
console.log(config.path)
// → ~/.config/pkgpulse-cli/config.json
TypeScript with schema
import Conf from "conf"
interface AppConfig {
apiKey?: string
defaults: {
format: "table" | "json" | "csv"
limit: number
verbose: boolean
}
lastSync?: string
}
const config = new Conf<AppConfig>({
projectName: "pkgpulse-cli",
schema: {
apiKey: {
type: "string",
pattern: "^pk_(live|test)_",
},
defaults: {
type: "object",
properties: {
format: { type: "string", enum: ["table", "json", "csv"], default: "table" },
limit: { type: "number", minimum: 1, maximum: 100, default: 10 },
verbose: { type: "boolean", default: false },
},
default: {},
},
lastSync: {
type: "string",
format: "date-time",
},
},
defaults: {
defaults: { format: "table", limit: 10, verbose: false },
},
})
// Type-safe access:
const format = config.get("defaults.format") // "table" | "json" | "csv"
config.set("defaults.limit", 50) // ✅
config.set("defaults.limit", 200) // ❌ Throws — maximum is 100
Migrations
import Conf from "conf"
const config = new Conf({
projectName: "pkgpulse-cli",
migrations: {
// Migrate from v1 → v2 schema:
"1.0.0": (store) => {
// Rename key:
if (store.has("api_key")) {
store.set("apiKey", store.get("api_key"))
store.delete("api_key")
}
},
"2.0.0": (store) => {
// Restructure:
const format = store.get("format")
const limit = store.get("limit")
if (format || limit) {
store.set("defaults", {
format: format ?? "table",
limit: limit ?? 10,
verbose: false,
})
store.delete("format")
store.delete("limit")
}
},
},
})
Encryption
import Conf from "conf"
const config = new Conf({
projectName: "pkgpulse-cli",
encryptionKey: "my-secret-key", // Encrypt the config file
})
// Config file is encrypted at rest:
config.set("apiKey", "pk_live_secret")
// ~/.config/pkgpulse-cli/config.json is encrypted (unreadable)
// Still accessed normally in code:
config.get("apiKey") // "pk_live_secret"
configstore
configstore — simple config store:
Basic usage
import Configstore from "configstore"
const config = new Configstore("pkgpulse-cli", {
// Default values:
format: "table",
limit: 10,
})
// Set:
config.set("apiKey", "pk_live_abc123")
// Get:
config.get("apiKey") // "pk_live_abc123"
// Dot-notation:
config.set("defaults.format", "json")
config.get("defaults.format") // "json"
// Delete:
config.delete("apiKey")
// Check:
config.has("apiKey") // false
// All config:
console.log(config.all)
// → { format: "table", limit: 10, defaults: { format: "json" } }
// File path:
console.log(config.path)
// → ~/.config/configstore/pkgpulse-cli.json
configstore vs conf
configstore:
✅ Simple API (get/set/has/delete)
✅ Mature — used since ~2013
✅ Minimal — small dependency footprint
❌ No schema validation
❌ No migrations
❌ No encryption
❌ No TypeScript generics
❌ No onDidChange listener
conf:
✅ Schema validation (JSON Schema)
✅ Migrations between versions
✅ Encryption at rest
✅ TypeScript generics
✅ onDidChange callback
✅ Same simple API (get/set/has/delete)
electron-store
electron-store — conf for Electron:
Setup
import Store from "electron-store"
const store = new Store({
schema: {
windowBounds: {
type: "object",
properties: {
width: { type: "number", default: 800 },
height: { type: "number", default: 600 },
x: { type: "number" },
y: { type: "number" },
},
default: { width: 800, height: 600 },
},
theme: {
type: "string",
enum: ["light", "dark", "system"],
default: "system",
},
recentFiles: {
type: "array",
items: { type: "string" },
default: [],
},
},
})
Electron-specific features
import Store from "electron-store"
const store = new Store()
// Saves to Electron's app.getPath("userData"):
// macOS: ~/Library/Application Support/MyApp/config.json
// Windows: %APPDATA%/MyApp/config.json
// Linux: ~/.config/MyApp/config.json
// Save window state:
mainWindow.on("close", () => {
store.set("windowBounds", mainWindow.getBounds())
})
// Restore window state:
const bounds = store.get("windowBounds")
const mainWindow = new BrowserWindow({
...bounds,
webPreferences: { preload: path.join(__dirname, "preload.js") },
})
// Watch for changes (useful for renderer ↔ main sync):
store.onDidChange("theme", (newValue, oldValue) => {
mainWindow.webContents.send("theme-changed", newValue)
})
// IPC bridge for renderer access:
import { ipcMain } from "electron"
ipcMain.handle("store:get", (_, key) => store.get(key))
ipcMain.handle("store:set", (_, key, value) => store.set(key, value))
Why not just use conf in Electron?
electron-store vs conf in Electron:
✅ Correct userData path (app.getPath("userData"))
✅ Works in both main and renderer processes
✅ Handles Electron's security model
✅ Proper app name detection from package.json
conf in Electron:
❌ May write to wrong directory
❌ Path detection issues in packaged apps
❌ Renderer process may not have fs access
Feature Comparison
| Feature | conf | configstore | electron-store |
|---|---|---|---|
| JSON Schema validation | ✅ | ❌ | ✅ |
| Migrations | ✅ | ❌ | ✅ |
| Encryption | ✅ | ❌ | ✅ |
| TypeScript generics | ✅ | ❌ | ✅ |
| onDidChange | ✅ | ❌ | ✅ |
| Dot notation | ✅ | ✅ | ✅ |
| Default values | ✅ | ✅ | ✅ |
| Electron support | ❌ | ❌ | ✅ (native) |
| Config file path | ~/.config/ | ~/.config/configstore/ | userData/ |
| Dependencies | Few | Few | Few |
| Weekly downloads | ~5M | ~5M | ~3M |
When to Use Each
Use conf if:
- Building a Node.js CLI tool that needs persistent configuration
- Want schema validation to prevent invalid config values
- Need migrations between config versions
- Want encryption for sensitive values (API keys, tokens)
Use configstore if:
- Need the simplest possible config persistence
- Existing project already using configstore
- Don't need validation, migrations, or encryption
- Building a quick CLI prototype
Use electron-store if:
- Building an Electron desktop application
- Need config storage that works across main/renderer processes
- Want correct platform-specific userData paths in packaged apps
- Same API as conf — easy migration if you already know conf
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on conf v13.x, configstore v7.x, and electron-store v10.x.