Self-host: databases
A Pulsify server is a relay plus a store. Run both yourself in front of PostgreSQL or SQLite, or let Pulsify Cloud do it.
How storage works
Every committed change is an immutable row: (docId, actor, clock, span, dependencies, bytes). The store never decodes operations or instantiates a schema. Two queries do all the work:
vector(docId)returns the contiguous causal frontier per actor.missing(docId, remoteVector)returns only the rows a client lacks, by index, without loading the document.
Rows arriving out of order stay dangling until their predecessors land, so a partially uploaded history can never be served as complete. Duplicates are idempotent; forks of one actor's history roll back the whole batch.
Self-host: relay
The relay fans frames out to connected peers of the same document and answers initial downloads. The reference relay is in-memory; add the store at the boundary and append before acknowledging.
import { PulseRelay, type RelayPeer } from "pulsify-core";
const relay = new PulseRelay();
Bun.serve<{ peer?: RelayPeer }>({
port: 3000,
fetch(request, server) {
if (new URL(request.url).pathname !== "/sync") return new Response("Not found", { status: 404 });
const offered = request.headers.get("sec-websocket-protocol") ?? "";
const version = offered.includes("pulsify-v3") ? 3 : 1;
const headers = version > 1 ? { "Sec-WebSocket-Protocol": "pulsify-v3" } : {};
return server.upgrade(request, { data: {}, headers }) ? undefined : new Response(null, { status: 400 });
},
websocket: {
open(socket) {
socket.data.peer = relay.open((bytes) => socket.sendBinary(bytes), { protocolVersion: 3 });
},
message(socket, message) {
if (typeof message === "string") return socket.close(1003);
socket.data.peer!.receive(new Uint8Array(message));
},
close(socket) { socket.data.peer?.close(); },
},
});
Self-host: PostgreSQL
PostgreSQL is the primary target. An append is one call to pulsify_append, which takes a per-document advisory lock, checks only neighboring segments, inserts the batch and promotes rows whose dependencies are now met. Payloads are bytea[], never base64.
import { SQL } from "bun";
import { PostgresPatchStore } from "pulsify-core/storage";
const sql = new SQL(process.env.DATABASE_URL!);
const store = new PostgresPatchStore((query, params) => sql.unsafe(query, params as never[]));
await store.initialize(); // once, installs tables and pulsify_append()
// Persist an incoming update frame: one round trip, no document decoding.
await store.appendEncoded(frameBytes);
// Serve a client: rows it has not seen, straight from the index.
const rows = await store.missing(docId, clientVersionVector);
const vector = await store.vector(docId);
Any driver works as long as it binds the third parameter as a binary array. Bun's SQL does; a pg adapter must preserve Uint8Array values.
Self-host: SQLite
Same API, same indexes, immediate transactions. Ideal for single-node servers, desktop apps and tests.
import { Database } from "bun:sqlite";
import { SqlitePatchStore } from "pulsify-core/storage";
const store = new SqlitePatchStore(new Database("pulsify.db"));
await store.initialize();
await store.appendEncoded(frameBytes);
Measurements and the conformance suite for both stores are in the storage guide.
Or use Pulsify Cloud
The managed backend runs the same stores for you: one isolated root per namespace, one socket per user and root, built-in access control. From the client it is a connector.
import { createCloudConnector } from "pulsify-cloud";
const cloud = createCloudConnector({ url: PULSIFY_URL, token: fetchToken });
const note = await loadPulse(Note, "alice/notes/n1", { connectors: [cloud] });