Getting started

From an empty file to a document that renders instantly, edits offline and syncs with a remote database. Migrations come later; this page uses a single schema version.

Install

terminal
bun add pulsify-core
# or: npm install pulsify-core

Pulsify runs in browsers, Bun, Node 22+, Deno 2 and Cloudflare Workers. Schemas come from @gin-quin/codec, a small binary serialization library that doubles as the type system. It ships with Pulsify and its builders are re-exported from pulsify-core.

Define a pulse

A definition has a kind, a schema and a materializer. Call .materialize() with no arguments to expose the schema as is.

note.ts
import { array, definePulse, object } from "pulsify-core";

export const Note = definePulse("Note", object({
  title: "string",
  tags: array("string"),
  author: object({ name: "string" }),
})).materialize();

Create and edit

A pulse is a proxy over the CRDT. Assign fields, push to arrays, mutate nested objects. Each write is validated against the schema, committed and announced to subscribers synchronously.

app.ts
import { createPulse, subscribePulse, snapshotPulse } from "pulsify-core";
import { Note } from "./note";

const note = createPulse(Note, {
  title: "Draft",
  tags: ["work"],
  author: { name: "Ada" },
});

// Runs now, then after every committed change.
subscribePulse(note, (n) => console.log(n.title, n.tags.length));

note.title = "Ready";
note.tags.push("review");
note.author.name = "Grace";

snapshotPulse(note); // plain, detached copy

Persist locally

Connectors are either local or remote. Local ones are tried first so the UI never waits for the network. The built-in local connector is an in-memory relay, which is also what you use in tests.

app.ts
import { createMemoryConnector, createPulse } from "pulsify-core";

// Built-in local connector: in-process relay, kind "local".
const local = createMemoryConnector();

const note = createPulse(Note, { title: "Draft", tags: [], author: { name: "Ada" } }, {
  connectors: [local],
  connect: true,
});

Bind it to your UI

Pulsify has no framework dependency. Pick your framework in the Frontend section: Svelte, Solid, React or Vue. Each page shows how to render, edit and load a pulse on the client: pulses are never created or loaded during server rendering.

Sync to a server

Add a second connector. The WebSocket connector talks to any Pulsify relay: your own server in front of PostgreSQL, or Pulsify Cloud. Writes are batched per microtask and uploaded as one frame.

app.ts
import {
  createMemoryConnector, createWebSocketConnector, loadPulse,
} from "pulsify-core";

const local = createMemoryConnector();
const remote = createWebSocketConnector("wss://sync.example.com/sync");

// Local connectors answer first: `ready` resolves before the network does.
const note = await loadPulse(Note, "note-42", {
  connectors: [local, remote],
});

note.title = "Edited on this device"; // uploaded in one batched frame

loadPulse resolves when usable data is available. Await connectPulse(note).synced instead when you need every connector to have acknowledged your history.

Load or create

Loading an unknown ID rejects with NOT_FOUND. The usual pattern creates the document with the same ID.

openNote.ts
import { connectPulse, createPulse, loadPulse, PulseError } from "pulsify-core";

async function openNote(id: string) {
  try {
    return await loadPulse(Note, id, { connectors: [remote] });
  } catch (error) {
    if (!(error instanceof PulseError) || error.code !== "NOT_FOUND") throw error;
    const note = createPulse(Note, { id, title: "Untitled", tags: [], author: { name: "" } }, {
      connectors: [remote],
    });
    await connectPulse(note).synced;
    return note;
  }
}