Aiden McClelland 10413414dc fix: revert from ciborium to serde_cbor_2, make apply_patches iterative
- ciborium's deserialize_str bug (#32) caused DB deserialization to fail
  silently, nuking the database value to null on compress
- Switch to dr-bonez/cbor fork (serde_cbor_2) which has StreamDeserializer
  change_output_type support
- Propagate deserialization errors instead of falling back to null
- Convert apply_patches from recursion to iteration to prevent stack
  overflow on patches with many operations (e.g. 953 ops)
2026-03-09 23:42:14 -06:00
2026-03-06 16:32:33 -07:00

patch-db

A database that tracks state updates as RFC 6902 JSON Patches. Enables observable, event-driven state management with a Rust backend and TypeScript client.

Overview

patch-db stores your application state as a single JSON document. Instead of opaque writes, every mutation is recorded as a JSON Patch — a sequence of add/remove/replace operations. Subscribers receive only the patches relevant to the subtree they're watching, making it efficient for UIs that need to react to fine-grained state changes.

Key properties

  • Event-sourced — patches are the source of truth, not snapshots
  • Observable — subscribers watch arbitrary subtrees via JSON Pointers and receive scoped patches in real time
  • Persistent — the Rust backend writes to disk with CBOR serialization, automatic compaction, and crash-safe backup files
  • Type-safe — derive macros on the Rust side; generic type parameters and deep watch$() overloads on the TypeScript side
  • Immutable values — the Rust side uses imbl_value::Value for efficient structural sharing

Quick start

Rust

Add to your Cargo.toml:

[dependencies]
patch-db = { git = "https://github.com/Start9Labs/patch-db" }
use patch_db::PatchDb;
use json_ptr::ROOT;

#[tokio::main]
async fn main() -> Result<(), patch_db::Error> {
    let db = PatchDb::open("my.db").await?;

    // Write a value
    db.put(&ROOT, &serde_json::json!({ "count": 0 })).await?;

    // Read it back
    let dump = db.dump(&ROOT).await;
    println!("revision {}: {}", dump.id, dump.value);

    // Subscribe to changes
    let mut watch = db.watch(ROOT.to_owned()).await;
    // watch implements Stream — use it with tokio, futures, etc.

    Ok(())
}

TypeScript

import { PatchDB, Dump, Update } from 'patch-db'
import { Observable } from 'rxjs'

interface AppState {
  users: { [id: string]: { name: string; online: boolean } }
  settings: { theme: string }
}

// source$ delivers updates from the server (WebSocket, SSE, etc.)
const source$: Observable<Update<AppState>[]> = getUpdatesFromServer()

const db = new PatchDB<AppState>(source$)
db.start()

// Watch a deeply nested path — fully type-safe
db.watch$('settings', 'theme').subscribe(theme => {
  console.log('Theme changed:', theme)
})

Further reading

  • ARCHITECTURE.md — project structure, crate/package details, data flow, storage format
  • CONTRIBUTING.md — environment setup, build commands, testing, code style

License

MIT

Description
No description provided
Readme 1.1 MiB
Languages
Rust 89.6%
TypeScript 10.4%