Skip to main content

conf vs configstore vs electron-store: Persistent Config Storage in Node.js (2026)

·PkgPulse Team

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

Featureconfconfigstoreelectron-store
JSON Schema validation
Migrations
Encryption
TypeScript generics
onDidChange
Dot notation
Default values
Electron support✅ (native)
Config file path~/.config/~/.config/configstore/userData/
DependenciesFewFewFew
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.

Compare configuration and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.