JavaScript / TypeScript SDK
Build SapixDB-powered apps in Node.js, Bun, Deno, or the browser. Zero dependencies — uses the platform fetch API. Full TypeScript support with generics.
Installation
npm install sapixdb
bun add sapixdb
yarn add sapixdb
Quick Start
import { SapixClient } from "sapixdb";
const db = new SapixClient({
url: "http://localhost:7475", // your SapixDB agent
agent: "my-app", // matches SAPIX_AGENT_ID
});
// ✅ Check connection
const alive = await db.ping(); // true / false, never throws
// ✍️ Write a record
const product = await db.collection("products").write({
name: "Classic T-Shirt",
price: 29.99,
stock: 100,
});
console.log(product.id); // "nuc_abc123"
console.log(product.hash); // "sha3:e7f2a1..." (cryptographic proof)
// 📖 Read latest records
const all = await db.collection("products").latest();
// 🔍 Filter
const shirts = await db.collection("products").find({ category: "apparel" });
// ⏱️ Time travel
const yesterday = await db
.collection("orders")
.asOf("2026-05-11T00:00:00Z")
.latest();TypeScript Generics
Pass your own interface to collection<T>() for full autocomplete and type safety on every read and write.
interface Product {
name: string;
price: number;
stock: number;
category?: string;
}
const products = db.collection<Product>("products");
// write() only accepts Partial<Product> — wrong fields fail at compile time
await products.write({ name: "Sneakers", price: 89.99, stock: 50 });
// latest() returns NucleotideRecord<Product>[]
const items = await products.latest();
const price = items[0].data.price; // typed as number ✓Collection API
Every read and write goes through db.collection(name). Collections are schema-free — you never declare them; they emerge from your first write.
.write(data)→ Promise<WriteResult>WriteResult contains id, hash, prev_hash, and timestamp..writeBatch(records[])→ Promise<WriteResult[]>.write() for each item..get(id)→ Promise<NucleotideRecord>SapixNotFoundError if not found..latest(options?)→ Promise<NucleotideRecord[]>.history(options?)→ Promise<NucleotideRecord[]>.find(filter, options?)→ Promise<NucleotideRecord[]>.findOne(filter)→ Promise<NucleotideRecord | null>null. Never throws on empty result..asOf(timestamp)→ CollectionQueryCollectionQuerywith the same .latest(), .find(), .findOne(), and .all() methods.Time Travel Queries
// What was the order status 30 minutes ago?
const thirtyMinAgo = new Date(Date.now() - 30 * 60_000).toISOString();
const snapshot = await db
.collection("orders")
.asOf(thirtyMinAgo)
.find({ customer_id: "cust_001" });
// The result shows the order as it was 30 minutes ago —
// before any status updates that happened since.Graph Relationships
Connect records with typed directed edges and traverse the graph to any depth. Useful for org charts, order→customer links, product→category trees, and access control graphs.
db.graph.relate(src, dst, type, weight?)→ Promise<void>db.graph.traverse(fromId, options?)→ Promise<TraverseResult>{ nodes, edges }. Options: depth (default 1), direction — "outbound" | "inbound" | "both".db.graph.neighbors(id, direction?)→ Promise<NucleotideRecord[]>// Link records
await db.graph.relate(order.id, customer.id, "placed_by");
await db.graph.relate(product.id, category.id, "belongs_to");
// Traverse: find all orders placed by this customer (depth 2)
const { nodes, edges } = await db.graph.traverse(customer.id, {
depth: 2,
direction: "inbound",
});Creating and Managing Agents
Every collection in SapixDB is an agent — an independent strand with its own hash chain, Ed25519 keypair, and on-disk record store. Before you can write to a named agent like orders or customers, that agent must exist. You create it once; it persists across restarts.
The seedHexis a 64-character hex string (32 random bytes) that deterministically derives the agent's Ed25519 keypair. Store it securely — it is the agent's cryptographic identity.
import { SapixClient } from "sapixdb";
import { randomBytes } from "node:crypto"; // or use any CSPRNG
const db = new SapixClient({ url: "http://localhost:7475" });
// Generate a fresh seed for each agent — store these in your secrets manager
const ordersSeed = randomBytes(32).toString("hex"); // 64 hex chars
const customersSeed = randomBytes(32).toString("hex");
const shipmentsSeed = randomBytes(32).toString("hex");
// Create the agents (call this ONCE; re-running will return a 409 Conflict)
await db.createAgent("orders", ordersSeed, "governed");
await db.createAgent("customers", customersSeed, "governed");
await db.createAgent("shipments", shipmentsSeed, "governed");
console.log("Agents ready.");After creation, use db.collection(name) or db.agent(name) to write and query records on any of those agents.
Zone options
The zone parameter controls the mutation policy for the agent:
| Zone | Mutation policy | Use for |
|---|---|---|
immutable_core | No mutations ever — requires dual-admin proposal | Financial ledgers, compliance archives |
protected | Mutations require explicit approval | Customer PII, medical records |
governed | Mutations auto-approve after a timeout (default) | Orders, products, general business data |
free | Mutations apply immediately, no approval needed | Logs, metrics, scratch agents |
List existing agents
const { agents, total } = await db.listAgents();
console.log(agents); // ["customers", "orders", "shipments"]
console.log(total); // 3Write to a named agent after creation
// Your app's agent sends this event when an order is placed
await db.collection("orders").write({
order_number: "P-10042",
customer_id: "cust_alice",
product_id: "prod_headphones",
product_name: "Wireless Headphones",
category: "electronics",
shipping_carrier: "FedEx",
shipping_method: "express",
total_usd: 89.99,
event: "order_placed",
});
// SapixDB appends this as an immutable, signed, hash-chained record
// to the "orders" agent's strand. content_hash and parent_hash are
// computed and stored automatically.Agent Ingest
Use db.ingest() for automated pipelines — AI agents, webhooks, cron jobs. Identical to .write() but routes through the dedicated ingest endpoint, which supports optional dual-write to Supabase.
// Log every AI decision permanently and immutably
await db.ingest("ai_decisions", {
model: "gpt-4o",
action: "approve_loan",
confidence: 0.94,
applicant: "cust_001",
reasoning: "Credit score 780, DTI 28%",
});Error Handling
import { SapixError, SapixNetworkError, SapixNotFoundError } from "sapixdb";
try {
const record = await db.collection("orders").get("nuc_missing");
} catch (err) {
if (err instanceof SapixNotFoundError) {
// 404 — record does not exist
} else if (err instanceof SapixNetworkError) {
// Cannot reach SapixDB — check if it's running
} else if (err instanceof SapixError) {
console.log(err.status); // HTTP status code
console.log(err.message); // Error message from the server
}
}Full Example: Online Store
import { SapixClient } from "sapixdb";
const db = new SapixClient({ url: "http://localhost:7475", agent: "store" });
async function main() {
// 1. Add products
const shirt = await db.collection("products").write({
sku: "SHIRT-001", name: "Classic T-Shirt",
price: 29.99, stock: 200, category: "apparel",
});
// 2. Register customer
const customer = await db.collection("customers").write({
name: "Alice Johnson", email: "[email protected]",
});
// 3. Place order
const order = await db.collection("orders").write({
customer_id: customer.id,
items: [{ product_id: shirt.id, qty: 2, unit_price: 29.99 }],
total: 59.98,
status: "placed",
});
// 4. Link in graph
await db.graph.relate(order.id, customer.id, "placed_by");
await db.graph.relate(order.id, shirt.id, "contains");
// 5. Ship (append — the "placed" version is preserved forever)
await db.collection("orders").write({
customer_id: customer.id,
status: "shipped",
tracking: "UPS-1Z999AA10123456784",
shipped_at: new Date().toISOString(),
});
// 6. Audit: what did the order look like when it was placed?
const placedAt = order.timestamp;
const [original] = await db
.collection("orders")
.asOf(placedAt)
.find({ customer_id: customer.id });
console.log(original.data.status); // "placed" — not "shipped"
}
main();Realtime Subscriptions (SSE)
Subscribe to live writes on any agent using Server-Sent Events. db.subscribeAgent() and db.subscribeGlobal() return an EventSource. Call .close() to unsubscribe. Works natively in browsers and Node.js 18+.
.subscribeAgent(agentId, onEvent, opts?)→ EventSourceopts.since replays records from an HLC timestamp before going live. opts.filter limits events to those whose JSON payload contains the named field..subscribeGlobal(onEvent, opts?)→ EventSourceopts.agents restricts the stream to a subset of agent IDs.import { SapixClient, type StreamEvent } from "sapixdb";
const db = new SapixClient({ url: "http://localhost:7475", agent: "my-app" });
const es = db.subscribeAgent("orders", (event: StreamEvent) => {
console.log("new record:", event.record_id, event.payload);
if ((event.payload as any)?.status === "shipped") {
sendShippingNotification(event.payload);
}
});
// Later — clean up
es.close();const lastSeenHlc = 1748304000000; // HLC timestamp — replay from here first
const es = db.subscribeAgent(
"transactions",
(event) => {
const payload = event.payload as { risk_score: number };
if (payload.risk_score >= 0.8) flagForReview(event);
},
{ since: lastSeenHlc, filter: "risk_score" },
);import { useEffect, useRef } from "react";
import { SapixClient, type StreamEvent } from "sapixdb";
function useLiveOrders(onWrite: (event: StreamEvent) => void) {
const esRef = useRef<EventSource | null>(null);
useEffect(() => {
const db = new SapixClient({ url: process.env.NEXT_PUBLIC_SAPIX_URL! });
esRef.current = db.subscribeGlobal(onWrite, {
agents: ["orders", "payments"],
});
return () => esRef.current?.close();
}, [onWrite]);
}pip install sapixdb and go get github.com/sapixdb/sapixdb-go — same realtime API, every language.