# electron-store: JSON persistence for Electron apps, and where it stops being the right tool

> electron-store wraps a single config.json in app.getPath('userData') with get/set/delete, JSON Schema validation through ajv, and atomic writes. It is built for small preference data, and its own README says it is not a database.

**sindresorhus/electron-store** — Simple data persistence for your Electron app or module - Save and load user preferences, app state, cache, etc

- Repository: https://github.com/sindresorhus/electron-store
- Stars: 5,020 · Forks: 166
- Language: JavaScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/sindresorhus-electron-store

## Who needs a JSON file in userData, and who does not

Electron ships no persistence layer. The README states this plainly: Electron doesn't have a built-in way to persist user settings and other data, so the module writes a JSON file named config.json into app.getPath('userData') and exposes get, set and delete over it. That is the whole product. If your app needs to remember a theme choice, a last-opened folder, a window position or an onboarding flag, this is the shortest path from zero to working persistence, because the file location is already the per-user, per-app directory Electron expects.

The audience is narrow on purpose. You need an Electron app, and the state you persist has to be small. The README carries an explicit note: this is not a database, the entire JSON file is read and written on every change, and it is best suited for small data like user settings, with SQLite named as the alternative for large data. That single sentence rules out log buffers, offline caches of thousands of records, and anything you plan to query. If you find yourself wanting to filter or index the contents, you have already picked the wrong tool.

## How the store reads and writes: conf, ajv and atomic replacement

electron-store is a thin Electron-aware layer over conf, which appears in package.json as a dependency ("conf": "^15.1.0"). It resolves the config path from Electron's userData directory, so the same code works in a packaged app and in development without you computing a path.

Validation runs through ajv, the JSON Schema validator. The README states the module uses JSON Schema draft-2020-12 and supports all validation keywords and formats, and that ajvOptions are passed straight through to ajv with allErrors and useDefaults set to true by default. Ajv runs in strict mode by default, so a schema containing a keyword ajv does not recognize throws something like strict mode: unknown keyword: "isEven" rather than silently ignoring it. You can set ajvOptions: {strict: false} to allow such schemas, but the README notes ajv then ignores the unknown keyword, so it validates nothing on its own. That is a footgun worth knowing about before you copy a schema from somewhere else.

Writes are atomic. The README states that changes are written to disk atomically, so a crash during a write will not corrupt the existing config. Combined with full-file reads on every access, the cost model is clear: cheap for a handful of keys, progressively worse as the file grows, because every set serializes and rewrites the whole document.

## Installing electron-store and saving your first preference

The README gives one install command, and it requires Electron 30 or later. Note the packaging constraint before you start: version 11 is native ESM and no longer provides a CommonJS export. If your project is CommonJS, the README says you have to convert to ESM and explicitly asks people not to open issues about CommonJS and ESM.

```bash
npm install electron-store
```

Once installed, the README's usage example is the fastest way to confirm the wiring. Import the default export, construct a store, and set a value. The store is created lazily against config.json in userData, so the first set creates the file.

```js
import Store from 'electron-store';

const store = new Store();

store.set('unicorn', 'unicorn-value');
console.log(store.get('unicorn'));
//=> 'unicorn-value'

store.set('foo.bar', true);
console.log(store.get('foo'));
//=> {bar: true}

store.delete('unicorn');
console.log(store.get('unicorn'));
//=> undefined
```

Dot notation is supported for nested properties, which is why store.get('foo') returns the whole object after store.set('foo.bar', true). Deleting a missing key returns undefined rather than throwing.

The next step is a schema, because that is where the module earns its keep. This example from the README constrains a number to a range and gives it a default, then shows what happens when you violate it.

```js
import Store from 'electron-store';

const schema = {
	foo: {
		type: 'number',
		maximum: 100,
		minimum: 1,
		default: 50
	},
	bar: {
		type: 'string',
		format: 'url'
	}
};

const store = new Store({schema});

console.log(store.get('foo'));
//=> 50

store.set('foo', '1');
// [Error: Config schema violation: `foo` should be number]
```

Two behaviours here are easy to miss. The default is applied on read, so store.get('foo') returns 50 before anything was ever written. And the violation throws rather than coercing the string '1' to a number, so you need a try/catch or a validation step at your input boundary.

## The renderer-process trap and the IPC route around it

The README says you can use the module directly in both the main and renderer process, then immediately qualifies it with a warning that undercuts the convenience. Using it in the renderer requires access to Node.js built-ins like fs and path. Electron disables Node.js in the renderer by default through nodeIntegration: false, and in the preload script since Electron 20 through sandbox: true. Importing the module from a renderer or preload script therefore fails with errors such as Can't resolve 'fs' or module not found: fs.

The README is direct about the fix: disabling those defaults is not recommended. The recommended pattern is to keep the store in the main process and expose it over IPC with ipcMain.handle and ipcRenderer.invoke. That means every renderer-side read or write becomes an async round trip you have to wire up and name yourself. For an app with a handful of settings, that is a small amount of boilerplate. For an app where the UI reads persisted state constantly, it is a design constraint you should notice before you build on top of it.

There is a documented escape hatch for renderer-only use: call Store.initRenderer() in the main process, or create a new Store instance in the main process. The README does not spell out the failure modes of that path in the excerpt available, so treat it as something to verify against your Electron version rather than assume.

## Nested defaults and the additionalProperties conflict

Two documented details cause most of the surprise reports. The first is nested defaults. A default on a nested property is only applied when the parent object exists. The README's example gives bar a default of an empty object so that bar.a can receive its own default of 5; without default: {} on bar, store.get('bar.a') returns undefined. The README adds that this applies at each level, so a property nested two objects deep needs a default of {} on both objects in the chain. If your schema is more than one level deep, budget time for this.

The second is the interaction between rootSchema and migrations. Setting additionalProperties: false on rootSchema rejects any key not declared in schema, which is a reasonable hardening step. But the README states that additionalProperties: false does not work together with the migrations option, because the store keeps its migration bookkeeping in the config under a key that is not in schema. So you can have strict key rejection or migrations, not both. That is a real design tension, not a documentation gap, and it should shape how you plan versioned config changes.

Related: to require a property, you add it to required and give it a default in schema as well. Otherwise a new config has no value for the property and the store throws when it is created. The failure happens at construction time, which is at least early and loud.

## Migrations are documented as buggy, and that changes the upgrade calculus

The migrations option lets you run operations whenever a version is upgraded, keyed by version string or semver range. The README's example maps '0.0.1', '1.0.0', '1.0.2' and '>=2.0.0' to handler functions that set and delete keys. The mechanism is straightforward: the store tracks the project version and runs the matching handlers.

What matters more is the warning above it, in the author's own words: I cannot provide support for this feature. It has some known bugs. I have no plans to work on it, but pull requests are welcome. That is an unusually candid statement, and it should be read as a support boundary rather than a marketing caveat. If your app's config shape will change across releases, you are relying on a feature the maintainer has said he will not fix. The practical alternatives are to version your own keys, to write the migration outside the store, or to accept that a bad config means asking users to reset it.

Licence and cost are simple by comparison. The package is MIT, declared in package.json, and its runtime dependencies are conf and type-fest. There is a funding link in package.json pointing at GitHub Sponsors. MIT means you can use, modify and redistribute it, but it comes with no warranty; none of this is legal advice, and if your product has compliance obligations, read the licence text yourself.

## electron-store against SQLite, localStorage and electron-settings

The README draws the SQLite comparison itself: for large data, use SQLite or similar. The difference is not speed tuning, it is the storage model. electron-store reads and rewrites one JSON document per change, so a write costs the size of the whole config. SQLite writes pages and supports queries, indexes and transactions. If your data has rows rather than keys, or you need to ask questions of it, the JSON file becomes a liability the moment it stops being small.

Against localStorage, the split is process and durability. localStorage lives in the renderer and is tied to the browser storage layer, so it is not available to main-process code and does not share the same file the main process reads. electron-store lives in userData as a file the main process owns, which is what you want for settings that affect window creation, tray behaviour or anything decided before a window exists.

Against electron-settings, the meaningful difference visible here is validation and packaging. electron-store ships a JSON Schema layer through ajv, with defaults applied on read and violations thrown as errors, plus atomic writes. That combination is the reason to pick it over a thinner key-value wrapper. If you do not want schema validation and do not care about atomic writes, you are paying for features you will not use.

## TypeScript support, ESM and what to check before you commit

Types ship in the package: index.d.ts is listed in the files array alongside index.js, and the exports map points types at ./index.d.ts. The repository also carries index.test-d.ts and a tsd script, so the type definitions are tested as part of npm test, which runs xo, ava and tsd. The tsd configuration sets module and moduleResolution to node16 with moduleDetection forced, which is consistent with the ESM-only packaging.

The engines field requires Node 22 or later, and the README requires Electron 30 or later. Those two constraints, plus ESM-only, are the things most likely to block an existing project. A CommonJS Electron app on an older Electron cannot adopt version 11 without converting its module system first, and the README is explicit that issues about that conversion will not be answered.

Maintenance looks current: the last push to the repository was on 2026-09-20, and the most recent release listed is v11.0.2 from 2025-10-05. The repository is not archived. That is a fact about activity, not a promise about the migrations feature, which the README already excludes from support.

## Conclusion

Adopt electron-store if your persisted state is small, schema-shaped and lives in the main process: user preferences, window bounds, feature flags, cache keys. Do not adopt it if you need queries, indexes or documents larger than a few hundred kilobytes, because the README states the whole JSON file is read and written on every change, and it points at SQLite for large data. Do not adopt it in a renderer or preload script either, since Electron disables Node.js there by default and the README says disabling those defaults is not recommended. Before committing, verify three things: that your project is ESM, because version 11 ships no CommonJS export; that you are on Electron 30 or later; and that your migrations story is acceptable, given the README's own warning that the migrations feature has known bugs and no planned support.

## FAQ

### How do I use electron-store in an Electron app?

Install it with npm install electron-store, import the default export, and create a Store instance. The README's example calls store.set with a key and value, then store.get to read it back, with dot notation available for nested keys such as foo.bar.

### What is electron-store?

It is an MIT-licensed module that gives Electron apps a persistence layer for settings and app state, saved as config.json in app.getPath('userData'). It exposes get, set and delete, and validates data against a JSON Schema using ajv.

### electron-store vs sqlite: which should I use?

The README answers this directly: electron-store is not a database, the entire JSON file is read and written on every change, and it is best suited for small data like user settings, with SQLite or similar named for large data. Choose SQLite once you need queries, indexes or a dataset that is not small.

### electron store vs localstorage: what is the difference?

localStorage belongs to the renderer's browser storage, while electron-store writes a JSON file in userData that the main process owns. The README also warns that importing the module from a renderer or preload script fails because Electron disables Node.js built-ins there by default.

### electron-store vs electron-settings: how do they differ?

electron-store adds a JSON Schema validation layer through ajv, applies defaults on read, throws on schema violations, and writes changes to disk atomically. A thinner key-value wrapper gives you none of those, so the validation and atomic writes are the reason to prefer this one.

## Sources

- [Issues](https://github.com/sindresorhus/electron-store/issues)
- [License: MIT](https://github.com/sindresorhus/electron-store/blob/main/LICENSE)
- [README](https://github.com/sindresorhus/electron-store/blob/main/README.md)
- [Releases](https://github.com/sindresorhus/electron-store/releases)
- [sindresorhus/electron-store on GitHub](https://github.com/sindresorhus/electron-store)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sindresorhus-electron-store
