Skip to main content
Zenbu.js includes a JSON database that syncs across every process. Every process (main and renderer) holds an in-memory copy (also known as a replica) of the database. Reads are always local and synchronous. When a write happens in any process, it syncs to all other replicas automatically. This means both the main process and the renderer read and write through the same interface. The only difference is how you access the client: through DbService in a service, or through React hooks in the renderer.

Schema

Each plugin defines a schema using zod that describes the shape of its data.
src/main/schema.ts
Fields without a default() will be undefined initially. Use .default() to set an initial value for a field when the database is first created.

Reading data

The database is a single JSON object called the root. Each plugin’s data lives under its name, so root.app holds everything defined by the app plugin. In the renderer, use the useDb hook to read from the database.
useDb is a subscription. When the selected value changes, the component re-renders automatically. Unrelated changes don’t trigger re-renders. In a service, read through DbService.client:
readRoot() returns a synchronous snapshot since the entire root is held in memory. You can also subscribe to a specific field to react when it changes:
The returned function unsubscribes. Wrap it in this.setup() so it cleans up on hot reload.

Writing data

In the renderer, use useDbClient to get a client that can write to the database:
In a service, write through DbService.client:
Inside update(), you mutate the root object directly, the same way you would with a regular JavaScript object. The database tracks these mutations and syncs them to other processes in the background.

Collections

Regular data fields are always held in memory across every process. Collections are for data that can grow large (like agent messages or logs) and should only be loaded into memory when needed. Define a collection in your schema with collection(...):
src/main/schema.ts
Collections are append-only. In a service, use concat to add items:
In the renderer, use useCollection to subscribe to a collection’s data:
Collection data is only loaded into memory when you subscribe or read from it. This keeps the in-memory footprint small for data that can grow without bound.

Blobs

Blobs store binary data (Uint8Array) like files or images. Like collections, they live on disk and are only loaded into memory when you read them. Define a blob in your schema with blob(...):
src/main/schema.ts
Write binary data with set:
Read it back with read:

Migrations

When you change your schema, existing databases need to be updated to match the new shape. Running pnpm run db:generate compares your current schema to the previous version and creates a migration file that describes what changed.
The generated file is placed in your plugin’s migrations/ directory. Here’s an example of what one looks like after adding a new activeTabId field to the schema:
migrations/0003.ts
Three operation types cover most schema changes:
  • add: introduces a new key, optionally with a default value.
  • remove: drops an existing key.
  • alter: updates the metadata of an existing key, like changing its default.
For more complex changes, add a migrate function to transform the data with custom logic. Use ctx.apply to run the declared operations first, then modify the result:
migrations/0004.ts
Migrations run automatically when the app starts. Each migration runs at most once, and during development, adding or editing a migration file triggers a reload without restarting the app.