Skip to main content

Documentation Index

Fetch the complete documentation index at: https://zenbulabs.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Zenbu.js apps are Electron apps. The two processes you work with most are:
  • Main process - a Node.js process that has access to the file system and operating system.
  • Renderer process - a Chromium browser window that runs your React UI.
The two processes communicate through Zenbu’s RPC and event system instead of raw Electron IPC.

Supported runtimes

RuntimeStatus
Electron
Tauri
Web

Plugins

Every Zenbu.js application is itself a plugin. Your app and any third-party plugin that extends it share the same set of capabilities. Everything described below applies equally to both.

Services

Services are shared objects that run in the main process. They hold state, manage resources, and can depend on other services (dependency injection). Every public method on a service is automatically available to the renderer process via type-safe RPC.
src/main/services/counter.ts
import { Service } from "@zenbujs/core/runtime"

export class CounterService extends Service.create({
  key: "counter",
}) {
  private count = 0

  evaluate() {
    // Called when the service starts
  }

  increment() {
    this.count++
    return this.count
  }

  getCount() {
    return this.count
  }
}
When you edit a service file and save, evaluate() re-runs immediately without restarting the app.

Database

Zenbu.js includes a JSON database that syncs across every process. Components that read from the database automatically re-render when the data they depend on changes.
import { useDb, useDbClient } from "@zenbujs/core/react"

function Counter() {
  const count = useDb(root => root.app.count)
  const client = useDbClient()

  return (
    <button onClick={() => {
      client.update(root => { root.app.count++ })
    }}>
      Count: {count}
    </button>
  )
}

RPC

Every public method on a service becomes callable from the renderer process with full TypeScript inference. You define a service class in the main process and call it from React through useRpc().
import { useRpc } from "@zenbujs/core/react"

function App() {
  const rpc = useRpc()

  const handleClick = async () => {
    const newCount = await rpc.counter.increment()
    console.log(newCount)
  }

  return <button onClick={handleClick}>Increment</button>
}

Events

Services in the main process can emit events that the renderer process subscribes to. Events cross process boundaries automatically. A service emits an event through this.ctx.rpc.emit:
src/main/services/notifications.ts
export class NotificationService extends Service.create({
  key: "notifications",
}) {
  send(message: string) {
    this.ctx.rpc.emit.app.notification({ message })
  }
}
The renderer process listens with useEvents():
import { useEvents } from "@zenbujs/core/react"
import { useEffect, useState } from "react"

function Notifications() {
  const events = useEvents()
  const [message, setMessage] = useState("")

  useEffect(() => {
    const unsubscribe = events.app.notification.subscribe((data) => {
      setMessage(data.message)
    })
    return unsubscribe
  }, [])

  return message ? <div className="toast">{message}</div> : null
}