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.
Real-World CLI Patterns with conf
conf is the go-to choice for CLIs that evolve over multiple versions, and its migration system directly addresses one of the most painful aspects of shipping CLI tools: handling users who have old config files. When you ship version 2.0 of a CLI that restructures its configuration schema, users upgrading from v1 will have a ~/.config/your-cli/config.json with the old shape. Without migrations, your CLI either crashes on invalid config or silently ignores existing user preferences. conf's migration callbacks run once per version bump and are stored alongside the config file so they never re-execute.
A pattern common in API tooling CLIs is using conf for both user-facing settings and internal state. For example, a CLI that calls an authenticated API stores the API key (encrypted with encryptionKey) in one Conf instance scoped to projectName: "my-cli", and stores internal state like lastFetchedAt or cached responses in a separate Conf instance with a different project name. Separating these allows the user-facing config to be cleared with --reset-config without wiping internal cache state.
The onDidChange callback is underused but valuable for CLIs with long-running daemon modes. When a user edits the config file directly (rather than through the CLI), conf detects the file change via fs.watch and fires the callback, allowing the daemon to reload settings without a restart. This makes conf a reasonable foundation for tools like development servers or background sync processes that read config dynamically.
Electron-Store IPC and Renderer Process Access
The most common Electron pitfall with config storage is accessing the store from the renderer process (where your React/Vue UI code runs). Electron's security model means the renderer does not have Node.js fs access by default — it cannot instantiate electron-store directly. The canonical pattern is to expose store operations through the contextBridge in your preload script, creating a typed API that the renderer can call via IPC.
This requires three pieces: an IPC handler in main that delegates to the electron-store instance, a preload that exposes the handler through contextBridge, and a typed interface in the renderer that matches the exposed API. electron-store provides a helper method store.openInEditor() that opens the config file in the system's default text editor — useful for power-user "edit config directly" escape hatches. The store.onDidAnyChange() callback (which fires for any key change) can be used in main to broadcast config updates to all renderer windows via webContents.send(), keeping multiple windows in sync without manual polling.
For Electron apps that support multiple user profiles or workspaces, a common pattern is instantiating separate electron-store instances with different name options (e.g., new Store({ name: "workspace-abc123" })), which writes to separate files in userData. This gives each workspace isolated config without needing a database.
Compare configuration and developer tooling on PkgPulse →
Config File Security and Sensitive Value Handling
CLI tools that store authentication credentials (API keys, tokens, OAuth refresh tokens) in config files face a security challenge that deserves explicit attention. JSON config files in ~/.config/ are world-readable on most systems unless file permissions are set explicitly — any process running as the same user can read them, and accidental inclusion in a directory listing or cat command in a terminal recording exposes the credentials.
conf's encryptionKey option addresses this for the common case of protecting against casual exposure. The encryption uses AES-256-CBC with the provided key, making the stored file unreadable without the key. The tradeoff is that the encryption key itself must come from somewhere — hardcoding it in your CLI source code means anyone who decompiles or reads your source has the key. A more robust approach is deriving the key from a machine-specific source: conf in Electron uses safeStorage (which ties encryption to the OS keychain), but in Node.js CLI tools you need to supply the key yourself.
A pragmatic pattern for CLI tools that need to store API keys is using the operating system's credential store directly rather than conf's encryption: the keytar package (or its successor @computer-crafts/keytar) reads and writes to macOS Keychain, Windows Credential Manager, and Linux Secret Service. Store only the API key in the OS credential store; store everything else (non-sensitive preferences, last-used settings, feature flags) in conf without encryption. This gives you OS-level key protection for sensitive values and simple JSON storage for everything else.
configstore has no encryption support — for any CLI that stores credentials, either upgrade to conf or add OS keychain integration as a separate layer. Never store raw API keys in configstore's unencrypted JSON files and instruct users of your CLI not to commit the ~/.config/ directory in their dotfiles repositories, as it will expose credentials stored there.
When to Use Each
Use conf if:
- You are building a Node.js CLI tool or desktop application (non-Electron) that needs persistent user settings
- You want automatic schema validation of config values via JSON Schema
- You want encryption support for sensitive config values
- You are building with TypeScript and want typed config access
Use configstore if:
- You are building a simple Node.js CLI tool that needs basic key-value persistence
- You are already in the Sindre Sorhus ecosystem and want consistent package style
- You want a straightforward API without JSON Schema validation overhead
- You need an older Node.js version compatibility that
confmay not support
Use electron-store if:
- You are building an Electron application — it is the obvious choice as it uses the correct OS-specific data paths for Electron apps
- You want the same interface as
confbut designed for Electron's main and renderer process model - You need automatic migration support between app versions
In 2026, for Node.js CLI tools, conf is the modern recommendation — it actively maintained, has TypeScript types, schema validation, and encryption. configstore is a legacy choice that works but gets less active development. electron-store is the standard for Electron applications.
All three packages store config in JSON files in the OS user data directory. On macOS: ~/Library/Application Support/<app-name>/. On Linux: ~/.config/<app-name>/. On Windows: %APPDATA%/<app-name>/. Be aware that these locations are user-writable and readable — never store secrets like API keys without encryption.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on conf v13.x, configstore v6.x, and electron-store v10.x. All three use JSON files for persistence. conf uses env-paths under the hood for cross-platform data directory resolution. electron-store uses app.getPath('userData') from Electron's app API to determine the correct data directory — this is why it must be used instead of conf in Electron apps, as conf's path resolution is not aware of Electron's directory structure. The encryption feature in conf and electron-store uses the safeStorage API in Electron and a hardware-bound key on other platforms.
See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).