<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/conf-vs-configstore-vs-electron-store-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/conf-vs-configstore-vs-electron-store-2026/raw.md -->
<!-- Source path: content/guides/conf-vs-configstore-vs-electron-store-2026.mdx -->

---
og_image: "/images/guides/conf-vs-configstore-vs-electron-store-2026.webp"
title: "conf vs configstore vs electron-store 2026"
description: "Compare conf, configstore, and electron-store for persisting user configuration in Node.js CLI tools and Electron apps. JSON storage, schema validation."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["nodejs", "typescript", "developer-tools", "automation"]
---

## 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](https://github.com/sindresorhus/conf) — modern config store:

### Basic usage

```typescript
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

```typescript
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

```typescript
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

```typescript
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](https://github.com/yeoman/configstore) — simple config store:

### Basic usage

```typescript
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](https://github.com/sindresorhus/electron-store) — conf for Electron:

### Setup

```typescript
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

```typescript
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 →](https://www.pkgpulse.com)*

## 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 `conf` may 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 `conf` but 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](/guides/cac-vs-meow-vs-arg-lightweight-cli-argument-parsers-2026) and [cosmiconfig vs lilconfig vs conf](/guides/cosmiconfig-vs-lilconfig-vs-conf-configuration-loading-2026), [archiver vs adm-zip vs JSZip (2026)](/guides/archiver-vs-adm-zip-vs-jszip-zip-archive-creation-2026).*
