English | Tiếng Việt | 简体中文
Jsonlet is a lightweight, embedded local database for Node.js that provides safe, predictable, and typed persistence using standard JSON files.
Developers commonly store CLI state, desktop settings, or automation data by simply fs.readFileing and fs.writeFileing a JSON file. This approach is deceptively unsafe and frequently leads to partially written files, corrupted data from concurrent writes, and lost updates.
Jsonlet solves this by wrapping JSON persistence in a predictable API with safe atomic writes, deterministic concurrency queues, cross-process file locking, schema migrations, and crash recovery—all with zero runtime dependencies.
- Safe Atomic Writes: Writes to a temporary file, flushes, and atomically replaces the primary file. Your JSON is never left partially written.
- Concurrency & Locking: Serializes mutations in-process with a queue, and coordinates cross-process writes using a heartbeat file lock.
- Immutability by Default: Cached memory reads are deeply frozen. You cannot accidentally mutate your database snapshot without going through a transaction.
- Schema Validation & Migrations: Hook in Zod (or any validator), handle version upgrades safely, and automatically backup before migrating.
- Self-Healing: Detects stale locks, handles Windows antivirus file-locking (
EBUSY/EPERM), and automatically recovers corrupted files from backups. - TypeScript Native: First-class type inference with zero
anyusage.
npm install @homielab/jsonlet
# or
pnpm add @homielab/jsonlet
# or
bun add @homielab/jsonletRequires Node.js 18.0.0 or higher.
- API Reference — constructor options, methods, hooks, and errors.
- Examples — runnable, production-style usage examples.
- Changelog — release history and notable changes.
Jsonlet guarantees that your reads and writes always match your exact generic type.
import { Jsonlet } from "@homielab/jsonlet";
interface MyDatabase {
counter: number;
users: Array<{ id: string; name: string }>;
}
const db = new Jsonlet<MyDatabase>("./data/db.json", {
defaults: () => ({
counter: 0,
users: [],
}),
});
// 1. Open and load the database
await db.open();
// 2. Read the deeply-frozen cached snapshot synchronously
const state = db.read();
console.log(`Current users: ${state.users.length}`);
// 3. Update state safely within a transaction queue
await db.update((draft) => {
draft.counter += 1;
draft.users.push({ id: crypto.randomUUID(), name: "Alice" });
});
await db.close();Jsonlet provides full IntelliSense and JSDoc support for plain JavaScript users.
import { Jsonlet } from "@homielab/jsonlet";
const db = new Jsonlet("./data/db.json", {
defaults: {
theme: "dark",
notifications: true,
},
});
await db.open();
await db.update((draft) => {
draft.theme = "light";
});
console.log("Current theme:", db.read().theme);
await db.close();When you call db.update(), Jsonlet does not write directly to your target file. It:
- Writes the complete serialized document to a uniquely named temporary file (
.db.json.jsonlet-tmp-<pid>). - Flushes the temporary file to disk (in
balancedorstrictdurability modes). - Atomically replaces the primary file using
fs.rename(with an exponential backoff retry loop on Windows to defeat transient AntivirusEPERMlocks). - Optionally syncs the parent directory.
- In-Process: Every call to
update()orreload()is placed into a sequential Promise queue. You can fire 100 concurrentdb.update()calls and they will execute safely, one after the other. - Cross-Process: If two separate Node.js scripts target the same file, Jsonlet provides best-effort cross-process coordination using an atomic directory lock, heartbeat mechanism, and asynchronous token-fencing immediately before writes commit.
(Note: Because standard JSON files do not have OS-level mandatory locks, extreme process suspension, event-loop starvation, or storage-level race conditions can mathematically bypass userspace locks. However, Jsonlet safely mitigates standard overlapping processes, stale crashed locks, and slow disk I/O).
Jsonlet doesn't force a validation library on you, but it perfectly integrates with tools like Zod:
import { Jsonlet } from "@homielab/jsonlet";
import { z } from "zod";
const Schema = z.object({
version: z.literal(1),
settings: z.object({ theme: z.string() }),
});
const db = new Jsonlet("./data/config.json", {
defaults: () => ({ version: 1, settings: { theme: "dark" } }),
parse: (input) => Schema.parse(input), // Validates on load!
});Data shapes change over time. Jsonlet can automatically migrate your data on startup:
const db = new Jsonlet("./data/db.json", {
defaults: () => ({ _v: 2, users: [] }),
schema: {
version: 2,
readVersion: (data) => data._v,
updateVersion: (data, version) => {
data._v = version;
return data;
},
migrations: {
1: (data) => {
// Migrate from v1 to v2
data.users = data.users.map((u) => ({ ...u, status: "active" }));
return data;
},
},
},
});Files can get corrupted if a user opens them in a text editor and makes a mistake. You can configure Jsonlet to backup data automatically and recover from disasters.
const db = new Jsonlet("./data/db.json", {
defaults: () => ({ data: [] }),
backup: {
beforeMigration: true, // Dump a backup before migrating schemas
beforeEveryWrite: false, // Dump before every write
retention: { maxCount: 10 }, // Keep the last 10 backups
},
recovery: {
// If the primary file is broken JSON, automatically restore the latest valid backup!
corruptedPrimary: "restore-latest-valid-backup",
quarantineBeforeReplace: true, // Save the broken file as .corrupt-xxx.json for inspection
},
});Jsonlet is a file persistence utility, not a database server. Please understand the following limitations before using it in production:
- Memory Bound: The entire document is loaded into memory, and every commit serializes and rewrites the complete document. It is incredibly fast for 100KB files, acceptable for 10MB files, and heavily degrades beyond 50MB.
- Throughput: It is designed for low-to-moderate write frequencies. High-throughput event logging will bottleneck on disk I/O.
- Filesystem Edge Cases: File locks and atomic rename guarantees rely on the underlying OS and filesystem. Network drives (SMB/NFS), cloud-synchronized folders (Dropbox/OneDrive), and certain Docker volume configurations may weaken atomic guarantees.
- No Queries: There is no query language, no secondary indexing, and no relational joins. You are interacting with a standard JavaScript object.
If your application requires concurrent remote clients, multi-node clustering, relational constraints, or partial document updates, please use a standard database like SQLite, PostgreSQL, or Redis.
- Node.js:
v18.0.0+ - OS: Linux, macOS, Windows
Contributions are welcome. Read the contribution guide to set up the project and submit a pull request. Please report vulnerabilities through the process described in the security policy.
For bugs and feature requests, use GitHub Issues.
MIT © 2026
