Local persistence

A pulse lives in memory. A local connector stores its history on the device, so documents open without a network, offline edits survive reloads and restarts, and everything uploads once a remote connector is reachable again.

pattern.ts
const note = await loadPulse(Note, id, {
  connectors: [local, remote], // local first: ready from disk, then synced with the server
});
  • Local first. Local connectors are opened before remote ones and ready resolves from disk.
  • Everything is stored. Local edits and rows received from the server are written, acknowledged only after the transaction commits.
  • Same rules as the server. Each store keeps immutable operation rows with the semantics of the PostgreSQL and SQLite stores: duplicates are idempotent, conflicts roll back, rows wait for their dependencies.
  • Self-healing. A local store that fails (blocked storage, closed database, restarted main process) is retried like any connector, with a full resynchronization.

Pick a store

PackageRuntimeStorageSeveral windows
pulsify-indexeddbBrowsers, PWAsIndexedDB, one database per nameTabs updated through a BroadcastChannel
pulsify-electronElectronSQLite (better-sqlite3) in the main processEvery window updated by the main process
pulsify-tauriTauri 2SQLite (rusqlite) in the Rust coreEvery webview updated by the Rust core

Browsers: IndexedDB

app.ts
// bun add pulsify-indexeddb
import { createWebSocketConnector, loadPulse } from "pulsify-core";
import { createIndexedDbConnector } from "pulsify-indexeddb";

const local = createIndexedDbConnector({ name: `my-app:${user.id}` }); // one database per account
const remote = createWebSocketConnector("wss://example.com/sync?doc={docId}");

const note = await loadPulse(Note, noteId, { connectors: [local, remote] });

// On logout: stop the pulses, then wipe and close.
destroyPulse(note);
await local.clear();
local.close(); // pulses using it stop instead of reconnecting

A blocked or full storage fails with CONNECTION_FAILED or LIMIT_EXCEEDED; the document keeps working from memory and the remote meanwhile.

Electron: SQLite in the main process

better-sqlite3 runs in the main process; windows talk to it over IPC with binary frames, through a bridge that exposes nothing else.

main.ts
// main.ts (bun add pulsify-electron better-sqlite3, then rebuild it for Electron)
import { app, ipcMain } from "electron";
import { join } from "node:path";
import { servePulsifyStore } from "pulsify-electron/main";

const store = servePulsifyStore({
  ipcMain,
  database: join(app.getPath("userData"), "pulsify.sqlite"), // WAL, one file for every window
});
app.on("will-quit", () => void store.close());

// preload.ts
import { contextBridge, ipcRenderer } from "electron";
import { exposePulsifyBridge } from "pulsify-electron";
exposePulsifyBridge({ contextBridge, ipcRenderer }); // window.pulsify, frames only

The SQLite file uses the same schema as SqlitePatchStore, so any Pulsify SQLite tool can read it.

Tauri: SQLite in the Rust core

A Tauri 2 plugin stores documents with rusqlite and streams frames to each webview as raw bytes over a Tauri channel. The file format is the same as SqlitePatchStore: TypeScript and Rust read each other's databases.

src-tauri
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-pulsify = "6"

// src-tauri/src/lib.rs
tauri::Builder::default()
    .plugin(tauri_plugin_pulsify::init()) // pulsify.sqlite in the app data directory
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

// src-tauri/capabilities/default.json
{ "identifier": "default", "windows": ["main"], "permissions": ["core:default", "pulsify:default"] }

The database opens on first use: if it cannot be opened, the app still starts and connected pulses retry. Without the pulsify:default permission, loading fails with FORBIDDEN.

With Pulsify Cloud

Local connectors combine with any remote, including the cloud: the notebook opens offline, and the cloud socket catches up in the background.

notebook.ts
import { createCloudConnector } from "pulsify-cloud";
import { createIndexedDbConnector } from "pulsify-indexeddb";

const local = createIndexedDbConnector({ name: `notes:${user.id}` });
const cloud = createCloudConnector({ url: PULSIFY_URL, token: fetchToken });

const notebook = await loadPulse(Notebook, user.id, {
  connectors: [local, cloud],
  include: true,
});