spacedb-sdk 0.1.1

SpaceDB developer SDK — the one surface a developer picks up: an offline-first local replica with a per-field schema (CRDT type + consistency tier), mID-authorized + budgeted ops that each return their honest achieved consistency, reactive queries, and CRDT sync. Composes the whole SpaceDB stack. Phase 1 SpaceDB Mission, M8.
Documentation

spacedb-sdk

The developer's whole world — one surface over the entire SpaceDB stack.

This is the crate you add. It composes spacedb-crdt, spacedb-access, spacedb-consistency and spacedb-meter into a single Database: offline-first, schema-declared per field, mID-authorized, budget-bounded, and honest about what every operation actually achieved.

Part of SpaceDB. Dual-licensed MIT OR Apache-2.0.

[dependencies]
spacedb-sdk = "0.1"

The whole model in one page

use spacedb_sdk::{
    Capability, CrdtType, Database, Identity, Ops, Outcome, Schema,
    Scope, SignedCapability, StrongResult, Tier,
};

// 1. Open an offline-first local replica for this device.
let mut db = Database::open(Identity::generate("did:mata:home-1")?);

// 2. Declare a schema — each field picks its CRDT type AND its consistency tier.
db.define(
    Schema::new("profile")
        .field("bio",          CrdtType::Text,     Tier::Convergent)
        .field("display_name", CrdtType::Register, Tier::Convergent)
        .field("cursor",       CrdtType::Register, Tier::Causal)
        .field("visits",       CrdtType::Counter,  Tier::Convergent)
        .field("username",     CrdtType::Register, Tier::Strong),
);

// 3. The owner grants a capability — to a person or an AI agent.
let owner = Identity::generate("did:mata:owner")?;
db.register_identity(&owner)?;
db.set_clock(1_700_000_000);

let cap = Capability::grant(
        owner.did().clone(),
        "did:agent:assistant",
        Scope::Collection("profile".into()),
        Ops::READ | Ops::WRITE,
    )?
    .with_expiry(1_702_592_000)
    .with_budget(1_000_000);          // micro-$MATA it may spend
let mut session = db.session(SignedCapability::sign(cap, &owner)?);

// 4. Write offline. Every op returns the consistency it ACTUALLY achieved.
let outcome = db.put_register(&mut session, "profile", "display_name", "Ada")?;
assert_eq!(outcome, Outcome::Local);  // durable here, converging outward
db.increment(&mut session, "profile", "visits", 1)?;
db.append_text(&mut session, "profile", "bio", "building on SpaceDB")?;

// 5. Read it back — honest about freshness.
let (name, read) = db.read_register(&mut session, "profile", "display_name")?;

// 6. Strong tier when you mean it: globally unique, or it cleanly refuses.
match db.claim_unique(&mut session, "profile", "username", "ada")? {
    StrongResult::Committed      => println!("username is yours"),
    StrongResult::Rejected(_)    => println!("already taken"),
    StrongResult::Unavailable(_) => println!("no quorum right now — try later"),
}
# Ok::<(), Box<dyn std::error::Error>>(())

open → schema → grant → write/read with honest state → strong when you mean it. No connection string, no server, no network required.

Sync two replicas (still no server)

let bytes = laptop.export("profile");     // CRDT state, content-addressed
phone.import("profile", &bytes)?;         // merges; conflicts resolve by CRDT rules

Export the collection after it has been written — exporting one that has never taken a write produces an empty update that import rejects.

React to change

let watcher = db.watch("profile");
// ... after any local or merged write:
if watcher.drain_changed() { /* re-render */ }

What the SDK enforces for you

  • Schema. require_field rejects an op whose field wasn't declared, or was declared as a different CRDT type — claim_unique on a non-Strong field is an error, not a silent downgrade.
  • Authorization. Every op runs through the spacedb-access chokepoint with the session's capability. No capability, no write.
  • Budget. Each mutating op is charged at write_cost(); a session that exhausts its budget stops rather than overdrawing. session.budget_remaining() reports it.
  • Honesty. Outcome (Local / Committed{tier} / Stale{lag} / Unavailable{reason}) and StrongResult come back from every op. Nothing is reported as durable that isn't.
  • Revocation. db.revoke(capability_id) cuts off the bearer and everything delegated beneath it.

quorum_partition / quorum_heal let a test take strong-tier members offline and prove the group fails safe instead of splitting.

Testing

The workspace defaults to wasm32; this crate is native. Test on your host triple:

cargo test -p spacedb-sdk --target aarch64-apple-darwin   # or your host triple

Suite: sdk.rs — the end-to-end developer path above.

License

MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.