Crust logoCrust

Store

Typed JSON persistence for CLI config, data, state, and cache files

@crustjs/store creates typed JSON stores from field definitions. It is standalone: it does not inject values into a Crust Context.

Install

npm install @crustjs/store

Quick example

const store = createStore({
	dirPath: configDir("my-cli"),
	name: "config",
	fields: {
		theme: { type: "string", default: "light" },
		verbose: { type: "boolean", default: false },
	},
});
const initial = await store.read(); // { theme: "light", verbose: false }
await store.write({ theme: "dark", verbose: false });
await store.patch({ verbose: true });
await store.reset();
console.log(initial);

Field types and defaults determine the state type. createStore() resolves <dirPath>/<name>.json once; dirPath must be absolute, and name cannot contain path separators or end in .json. Each name is an independent file, so use one store per file, such as config and auth.

Prop

Type

Operations

MethodBehavior
read()Parse, coerce, merge missing defaults, and validate in memory. Never writes.
write(state)Validate a complete state, create parent directories, replace the file atomically, return it.
update(fn)Read and validate current state, call fn with it, then validate and persist the result.
patch(partial)Shallow-merge the supplied keys, then validate and persist.
reset()Remove the file; the next read() returns defaults. Idempotent.

Prop

Type

  • update() rejects without calling fn when current state is invalid or a required value is missing. Use write() to initialize or patch() to repair.
  • Mutations are atomic replacements, not locks. Await operations on the same file in sequence, and coordinate across processes yourself.
  • read() coerces core number and boolean strings; only "true" and "1" become true.
  • Unknown keys are dropped by default (pruneUnknown: true); pass false to keep them. Persisted falsy values are kept.
  • Malformed JSON and non-object roots reject with PARSE. They never fall back to defaults.

Storage paths

HelperLinux and macOSWindowsOverride
configDir~/.config/<app>%APPDATA%\<app>XDG_CONFIG_HOME; APPDATA on Windows
dataDir~/.local/share/<app>%LOCALAPPDATA%\<app>\DataXDG_DATA_HOME; LOCALAPPDATA on Windows
stateDir~/.local/state/<app>%LOCALAPPDATA%\<app>\StateXDG_STATE_HOME; LOCALAPPDATA on Windows
cacheDir~/.cache/<app>%LOCALAPPDATA%\<app>\CacheXDG_CACHE_HOME; LOCALAPPDATA on Windows

Choose by intent: user configuration, durable app data, runtime state, or regenerable cache. macOS uses the XDG paths. Each helper accepts a PlatformEnv second argument for deterministic tests.

Secret files

const auth = createStore({
  dirPath: configDir("my-cli"),
  name: "auth",
  fields: { token: { type: "string" } },
  access: "private",
});

On Unix, access: "private" writes the file with mode 0600 and a parent directory the store creates with 0700; an existing directory keeps its permissions. Pass { file, directory } for custom bits. Windows uses inherited ACLs, so the bits are not enforced there.

Validation

const ports = createStore({
	dirPath: configDir("my-cli"),
	name: "ports",
	fields: {
		port: {
			type: "number",
			default: 3000,
			validate(value) {
				if (value < 1 || value > 65535) throw new Error("Port must be 1-65535");
			},
		},
	},
});
await ports.read();

Prop

Type

  • Core validate callbacks run on read and mutation and throw to reject. They may return { value } to transform, but transforms apply only on mutation.
  • A schema field gives validation, transformation, defaults, and optionality to its Standard Schema v1 schema, and cannot also set default or validate. read() returns the schema output without writing it.
  • Mutations revalidate transformed values and reject transforms whose result would read back differently.
  • Mutations reject explicit undefined for defaulted core fields, and values JSON cannot round-trip, such as NaN, Infinity, -0, sparse arrays, undefined properties, and cycles.

Errors

try {
  await store.read();
} catch (error) {
  if (error instanceof CrustStoreError && error.is("PARSE")) console.error(error.details.path);
}

CrustStoreError.code is PATH, PARSE, IO, VALIDATION, or DEFINITION; .is(code) narrows details.

Store has no locking, encryption, migrations, other formats, or synchronous methods. The package entry point lists every export.

On this page