Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What is Ankurah?

Ankurah is a state-management framework that enables real-time data synchronization across multiple nodes with built-in observability.

It supports multiple storage and data type backends to enable no-compromise representation of your data.

Note: This project is in the early stages of development, and is not yet ready for production use.

Key Features

  • Schema-First Design: Define data models using Rust structs with an ActiveRecord-style interface - View/Mutable
  • Content-filtered pub/sub: Subscribe to changes on a collection using a SQL-like query
  • Real-Time Observability: Signal-based pattern for tracking entity changes
  • Distributed Architecture: Multi-node synchronization with event sourcing
  • Flexible Storage: Support for multiple storage backends (Sled, Postgres, TiKV)
  • Isomorphic code: Server applications and Web applications use the same code, including first-class support for React and Leptos out of the box

Core Concepts

  • Model: A struct describing fields and types for entities in a collection (data binding)
  • Collection: A group of entities of the same type (similar to a database table, and backed by a table in the postgres backend)
  • Entity: A discrete identity in a collection - Dynamic schema (similar to a schema-less database row)
  • View: A read-only representation of an entity - Typed by the model
  • Mutable: A mutable state representation of an entity - Typed by the model
  • Event: An atomic change that can be applied to an entity - used for synchronization and audit trail

Design Philosophy

Ankurah follows an event-sourced architecture where:

  • All operations have unique IDs and precursor operations
  • Entity state is maintained per node with operation tree tracking
  • Operations use ULID for distributed ID generation
  • Entity IDs are derived from their creation operation

Quick Example

#![allow(unused)]
fn main() {
// Subscribe to changes on the client
let subscription = client.subscribe::<_,_,AlbumView>(
    "name = 'Origin of Symmetry'",
    |changes| {
        println!("Received changes: {}", changes);
    }
).await?;

// Create a new album on the server
let trx = server.begin();
let album = trx.create(&Album {
    name: "Origin of Symmetry".into(),
    year: "2001".into(),
}).await?;
trx.commit().await?;
}

Community

Join the conversation and contribute:

License

Ankurah is dual-licensed under MIT or Apache-2.0.

Getting Started

There are two ways to get started with Ankurah: using a template or setting up manually.

Starting from a Template

The quickest way to get started is to use our React + Sled template with cargo-generate:

cargo generate https://github.com/ankurah/react-sled-template

This will create a new project with:

  • A Rust server using the Sled storage backend
  • A React frontend with TypeScript
  • WASM bindings pre-configured
  • WebSocket communication between client and server
  • Example models and UI components

After generating your project:

cd your-project-name
./dev.sh

This starts watchers for the Rust server, wasm-bindings, and React app. Open your browser to http://localhost:5173. Press Ctrl+C to stop and all watchers will exit cleanly.

Tip: More templates will be added soon for different use cases!

Need help? Join the Ankurah Discord!


Manual Setup

If you want to set up Ankurah from scratch, follow these steps:

Prerequisites

Server Setup

Start the example server (keep this running):

cargo run -p ankurah-example-server

Or in development mode with auto-reload:

cargo watch -x 'run -p ankurah-example-server'

React Example App

  1. Compile the Wasm Bindings (keep this running):

    Navigate to the wasm-bindings example directory:

    cd examples/wasm-bindings
    wasm-pack build --target web --debug
    

    Or in development mode with auto-rebuild:

    cargo watch -s 'wasm-pack build --target web --debug'
    
  2. Run the React Example App (keep this running):

    cd examples/react-app
    bun install
    bun dev
    
  3. Test the app:

    Load http://localhost:5173/ in one regular browser tab, and one incognito browser tab to see real-time synchronization in action!

    Note: You can also use two regular browser tabs, but they share one IndexedDB local storage backend, so incognito mode provides a better test of multi-node synchronization.

Leptos Example App

  1. Install Trunk (build tool used by Leptos):

    cargo install trunk
    
  2. Run the Leptos Example App (keep this running):

    cd examples/leptos-app
    trunk serve --open
    

Note: For the Leptos app, there is no need to build the Wasm bindings crate separately.

How It Works

In the example setup:

  • The "server" process is a native Rust process whose node is flagged as "durable", meaning that it attests it will not lose data.
  • The "client" process is a WASM process that is also durable in some sense, but not to be relied upon to have all data.
  • The demo server currently uses the Sled backend, but Postgres is also supported, and TiKV support is planned.
  • WebSocket connections enable real-time bi-directional communication between nodes.

Next Steps

  • Check out the Examples page for more code samples
  • Learn about the Architecture to understand how Ankurah works
  • Read the Glossary to understand key terminology
  • Join the Discord to ask questions and share your projects!

Architecture

Ankurah is built on a distributed, event-sourced architecture that enables real-time data synchronization across multiple nodes.

High-Level Overview

The following interactive diagram shows the key components and data flow in an Ankurah system:

Key Architectural Components

Node

A Node is the fundamental unit in Ankurah. Each node can:

  • Store data using a pluggable storage backend
  • Subscribe to changes from other nodes
  • Publish changes to subscribed nodes
  • Maintain its own view of entity state

Storage Backends

Ankurah supports multiple storage backends:

  • Sled: Embedded key-value store, great for development and embedded applications
  • Postgres: Production-grade relational database backend
  • IndexedDB (WASM): Browser-based storage for client applications
  • TiKV (planned): Distributed transactional key-value database

Event Sourcing

All changes in Ankurah are represented as immutable events:

  • Each event has a unique ID (ULID) for distributed generation
  • Events reference their precursor events, forming a directed acyclic graph (DAG)
  • Entity state is derived from applying events in order
  • The "present" state includes the "head" operations of the event tree

Subscriptions

Nodes can subscribe to changes using SQL-like queries:

#![allow(unused)]
fn main() {
client.subscribe::<_,_,AlbumView>(
    "name LIKE 'Origin%' AND year > '2000'",
    |changes| {
        // Handle matching changes
    }
).await?;
}

The subscription system uses:

  • Content filtering: Only matching entities trigger callbacks
  • Real-time updates: Changes propagate immediately
  • Efficient indexing: Queries are optimized using available indexes

Reactive Runtime

Ankurah includes a reactive runtime (Reactor) that:

  • Tracks dependencies between entities
  • Propagates changes through the dependency graph
  • Enables derived/computed values
  • Powers the signal-based observability pattern

Communication Patterns

Client-Server

  • WebSocket-based bidirectional communication
  • Automatic reconnection and synchronization
  • Delta-based updates for efficiency

Peer-to-Peer (Planned)

Future versions will support:

  • Direct peer-to-peer connections
  • Mesh networking
  • Cryptographic identities
  • End-to-end encryption

Data Flow

  1. Create/Update: A node creates or updates an entity
  2. Event Generation: An immutable event is generated and stored
  3. Local Application: The event is applied to the local node's state
  4. Subscription Matching: The reactor checks which subscriptions match
  5. Propagation: Matching events are sent to subscribed nodes
  6. Remote Application: Remote nodes receive and apply the event

Consistency Model

Ankurah uses eventual consistency with strong guarantees:

  • Operations are causally consistent: if event B depends on event A, all nodes see A before B
  • Conflicts are resolved deterministically using operation IDs
  • Nodes can operate while partitioned and sync when reconnected

Learn More

  • See the Design Goals for the philosophy behind these choices
  • Check out Examples for practical code demonstrating these concepts
  • Join the Discord to discuss architecture and implementation details

Glossary

This glossary defines key terms and concepts used throughout Ankurah.

Core Concepts

Model

A struct that describes the fields and their types for an entity in a collection. Models define the data binding schema and generate View and Mutable types.

#![allow(unused)]
fn main() {
#[derive(Model)]
struct Album {
    name: String,
    year: String,
}
// Generates: AlbumView, AlbumMutable
}

Collection

A collection of entities, with a name and a type that implements the Model trait. Similar to a table in a traditional database. In the Postgres backend, collections are backed by actual database tables.

Entity

A discrete identity in a collection similar to a row in a database. Each entity has a dynamic schema and can have properties bound to it via Models. An entity's ID is derived from the operation that created it.

View

A struct that represents the read-only view of an entity which is typed by the Model. Views provide type-safe access to entity properties without allowing mutations.

#![allow(unused)]
fn main() {
let album: AlbumView = entity.view()?;
println!("Album: {} ({})", album.name, album.year);
}

Mutable

A struct that represents the mutable state of an entity which is typed by the Model. Mutables allow type-safe modifications to entity properties.

#![allow(unused)]
fn main() {
let mut album: AlbumMutable = entity.mutable()?;
album.name.set("New Album Name");
}

Event

A single event that may or may not be applied to an entity. Events are immutable operations that form the basis of Ankurah's event sourcing. Each event has:

  • A unique ID (ULID)
  • References to precursor events
  • A payload describing the change
  • Metadata (timestamp, node ID, etc.)

Infrastructure

Node

A participant in the Ankurah network. Nodes can be servers, clients, or peers. Each node has:

  • A storage backend
  • A policy agent (for permissions)
  • Connection handlers
  • A reactor for subscriptions

Storage Engine

A means of storing and retrieving data which is generally durable (but not necessarily). Available engines:

  • Sled: Embedded KV store
  • Postgres: Relational database
  • IndexedDB: Browser storage (WASM)
  • TiKV (planned): Distributed KV store

Storage Collection

A collection of entities in a storage engine. The physical representation of a Collection in the storage layer.

Operations

Transaction

A unit of work that groups multiple operations. Transactions provide:

  • Atomicity: All operations succeed or fail together
  • Isolation: Operations are isolated from other transactions
  • Consistency: Database constraints are maintained
#![allow(unused)]
fn main() {
let trx = node.begin();
let entity = trx.create(&Album { /* ... */ }).await?;
trx.commit().await?;
}

Subscription

A live query that receives updates when matching entities change. Subscriptions use SQL-like predicates for filtering.

#![allow(unused)]
fn main() {
node.subscribe::<_,_,AlbumView>("year > '2000'", |changes| {
    // Handle changes
}).await?;
}

Event Sourcing Terms

ULID

Universally Unique Lexicographically Sortable Identifier. Used for operation IDs to enable:

  • Distributed ID generation without coordination
  • Temporal ordering via lexicographic sorting
  • Compact representation (128-bit)

DAG (Directed Acyclic Graph)

The structure formed by events and their precursor relationships. The DAG enables:

  • Causal consistency
  • Conflict detection
  • Efficient synchronization

Lineage

The chain of events that led to an entity's current state. Used for:

  • Audit trails
  • Conflict resolution
  • Replication

The most recent operation(s) in an entity's event DAG. Nodes track heads to determine if they have the latest version.

Reactivity

Signal

An observable value that notifies subscribers when it changes. Ankurah's signal system is inspired by SolidJS and enables reactive UIs.

Reactor

The runtime component that manages subscriptions, tracks dependencies, and propagates changes. The reactor ensures that all live queries and derived values stay up-to-date.

Live Query

A query that automatically updates when the underlying data changes. Implemented using subscriptions and the reactor.

Policy & Security

Policy Agent

A component that controls access to operations. Agents decide:

  • Can a node read an entity?
  • Can a node modify an entity?
  • Can a node subscribe to a collection?

Context

A wrapper around a Node that includes user/session information (ContextData). Operations performed through a Context are subject to policy checks.

#![allow(unused)]
fn main() {
let context = node.context(user_data)?;
let album = context.create(&Album { /* ... */ }).await?;
}

Additional Resources

Design Goals

Ankurah is designed with specific goals in mind to create a powerful, flexible, and developer-friendly state management framework.

Schema / UX

Model-Based Schema Definition

  • Define schema using "Model" structs, which define the data types for a collection of entities
  • An ActiveRecord style interface with type-specific methods for each value
  • TypeScript/JavaScript bindings allow these Model definitions to be used client or serverside
  • Macros to create and query entities in the collection

Example:

#![allow(unused)]
fn main() {
#[derive(Model)]
struct Album {
    name: String,
    year: String,
    artist: String,
}

// Use it
let album = context.create(&Album {
    name: "Origin of Symmetry".into(),
    year: "2001".into(),
    artist: "Muse".into(),
}).await?;
}

Observability

Signal-Style Reactive Pattern

  • Utilize a "signal" style pattern to allow for observability of changes to entities, collections, and values
  • Derivative signals can be created which filter, combine, and transform those changes
  • React bindings are a key consideration
  • Leptos and other Rust web frameworks should also work, but are lower priority initially

Benefits:

  • Automatic UI updates when data changes
  • Declarative data dependencies
  • Efficient change propagation

Storage and State Management

Multiple Backing Stores

Support for various storage backends:

  • Sled KV Store (initial implementation)
  • Postgres (production-ready relational database)
  • TiKV (planned - distributed transactional KV)
  • IndexedDB (browser/WASM support)
  • Others as needed

Event Sourcing / Operation-Based

All changes are tracked as immutable operations:

  • Audit Trail: All operations have a unique ID and a list of precursor operations
  • Immutable History: Operations are immutable (with considerations for CRDT compaction and GDPR)
  • Current State: The "present" state of an entity is maintained per node, including the "head" of the operation tree
  • Version Tracking: Nodes can determine if they have the latest version of an entity

Operation IDs

  • Use ULID (Universally Unique Lexicographically Sortable Identifiers) for distributed ID generation
  • Enables lexicographical ordering without coordination
  • Entity IDs are derived from the initial operation that created them (genesis operation)

Future Considerations:

  • How can this be modified to provide non-adversarial cryptographic collision resistance?
  • How can we add adversarial attack resistance?

Development Milestones

Major Milestone 1 - Getting the foot in the door

Core functionality for early adopters:

  • ✅ Production-usable event-sourced ORM with off-the-shelf database storage
  • ✅ Rust structs for data modeling
  • ✅ Signals pattern for notifications
  • ✅ WASM Bindings for client-side use
  • ✅ WebSocket server and client
  • ✅ React Bindings
  • ✅ Basic included data-types: CRDT text (yrs crate) and primitive types
  • ✅ Embedded KV backing store (Sled DB)
  • ✅ Basic, single field queries (auto-indexed)
  • ✅ Postgres backend
  • ✅ Multi-field queries
  • ✅ Robust recursive query AST for declarative queries

Major Milestone 2 - Stuff we need, but can live without for a bit

Enhanced functionality:

  • TiKV Backend
  • Graph Functionality
  • User-definable data types
  • Advanced indexing strategies
  • Query optimization
  • Performance profiling tools

Major Milestone 3 - Maybe someday...

Future aspirations:

  • P2P functionality: Direct peer-to-peer connections without central servers
  • Portable cryptographic identities: User identities that work across nodes
  • E2EE (End-to-End Encryption): Privacy-preserving data synchronization
  • Hypergraph functionality: More complex relationship modeling
  • CRDT compaction: Efficient storage of long operation histories
  • Byzantine fault tolerance: Security against malicious nodes

Design Philosophy

Ankurah prioritizes:

  1. Developer Experience: Easy to learn, hard to misuse
  2. Type Safety: Compile-time guarantees where possible
  3. Flexibility: Support various storage backends and use cases
  4. Performance: Efficient synchronization and querying
  5. Scalability: From embedded devices to large distributed systems

Inspirations

Ankurah draws inspiration from:

  • Event Sourcing: CQRS, Event Store
  • Reactive Programming: SolidJS signals, MobX
  • ActiveRecord: Ruby on Rails, Ecto (Elixir)
  • Distributed Systems: CRDTs, operational transformation
  • Modern Databases: Postgres, TiKV, FaunaDB

Contributing

We welcome contributions! Join the discussion:

Help shape the future of Ankurah by:

  • Reporting bugs and suggesting features
  • Improving documentation
  • Contributing code
  • Building example applications
  • Sharing your use cases

Examples

This page contains practical code examples demonstrating key Ankurah features.

Inter-Node Subscription

This example shows how to set up a server and client node, connect them, and subscribe to changes:

#![allow(unused)]
fn main() {
use ankurah::prelude::*;
use ankurah_storage_sled::SledStorageEngine;
use ankurah_connector_local_process::LocalProcessConnection;

// Create server node with durable storage
let server = Node::new_durable(
    Arc::new(SledStorageEngine::new_test()?),
    PermissiveAgent::new()
);

// Initialize a new "system" (only done once on first startup)
server.system.create()?;

// Create a context for the server
let server = server.context(context_data)?;

// Create client node
let client = Node::new(
    Arc::new(SledStorageEngine::new_test()?),
    PermissiveAgent::new()
);

// Connect nodes using local process connection
let _conn = LocalProcessConnection::new(&server, &client).await?;

// Wait for the client to join the server "system"
client.system.wait_system_ready().await;
let client = client.context(context_data)?;

// Subscribe to changes on the client
let subscription = client.subscribe::<_,_,AlbumView>(
    "name = 'Origin of Symmetry'",
    |changes| {
        println!("Received changes: {}", changes);
    }
).await?;

// Create a new album on the server
let trx = server.begin();
let album = trx.create(&Album {
    name: "Origin of Symmetry".into(),
    year: "2001".into(),
}).await?;
trx.commit().await?;

// The subscription callback will fire automatically!
}

Defining a Model

Models define the schema for your entities:

#![allow(unused)]
fn main() {
use ankurah::prelude::*;

// Define your model
#[derive(Model, Clone, Debug)]
struct BlogPost {
    title: String,
    content: String,
    author: String,
    published: bool,
    tags: Vec<String>,
}

// This automatically generates:
// - BlogPostView (read-only)
// - BlogPostMutable (for updates)
}

Creating Entities

#![allow(unused)]
fn main() {
// Start a transaction
let trx = context.begin();

// Create a new entity
let post = trx.create(&BlogPost {
    title: "Getting Started with Ankurah".into(),
    content: "Ankurah makes distributed state management easy...".into(),
    author: "Alice".into(),
    published: true,
    tags: vec!["tutorial".into(), "rust".into()],
}).await?;

// Commit the transaction
trx.commit().await?;

println!("Created post with ID: {}", post.id());
}

Reading Entities

#![allow(unused)]
fn main() {
// Get a view of the entity (read-only)
let view: BlogPostView = post.view()?;

println!("Title: {}", view.title);
println!("Author: {}", view.author);
println!("Published: {}", view.published);
}

Updating Entities

#![allow(unused)]
fn main() {
// Start a transaction
let trx = context.begin();

// Get a mutable handle
let mut mutable: BlogPostMutable = post.mutable(&trx)?;

// Update fields
mutable.title.set("Updated: Getting Started with Ankurah");
mutable.published.set(true);

// Commit changes
trx.commit().await?;
}

Querying with Subscriptions

Subscriptions let you receive real-time updates for entities matching a query:

#![allow(unused)]
fn main() {
// Subscribe to all published posts
let sub = context.query::<BlogPostView>(
    "published = true",
    |changes| {
        for change in changes.created {
            println!("New published post: {}", change.view.title);
        }
        for change in changes.updated {
            println!("Updated post: {}", change.view.title);
        }
    }
).await?;

// Subscribe with complex queries
let sub = context.query::<BlogPostView>(
    "published = true AND author = 'Alice' AND tags CONTAINS 'rust'",
    |changes| {
        println!("Alice published a new Rust post!");
    }
).await?;
}

Using Signals in React

Ankurah provides React hooks for reactive UI updates:

import { useQuery, useEntity } from "ankurah-react";

function BlogPostList() {
  // Subscribe to all published posts
  const posts = useQuery<BlogPost>("BlogPost", "published = true");

  return (
    <div>
      {posts.map((post) => (
        <BlogPostCard key={post.id} postId={post.id} />
      ))}
    </div>
  );
}

function BlogPostCard({ postId }) {
  // Subscribe to a specific entity
  const post = useEntity<BlogPost>(postId);

  if (!post) return <div>Loading...</div>;

  return (
    <div>
      <h2>{post.title}</h2>
      <p>By {post.author}</p>
      <p>{post.content}</p>
    </div>
  );
}

WebSocket Client Setup (WASM)

Connect a browser client to a server:

#![allow(unused)]
fn main() {
use ankurah_wasm::*;
use ankurah_connector_websocket_client_wasm::*;

// Create a client node with IndexedDB storage
let storage = IndexedDBStorageEngine::new("my-app").await?;
let client = Node::new(Arc::new(storage), PermissiveAgent::new());

// Connect to server via WebSocket
let ws = WebSocketClientWasm::connect(
    "ws://localhost:8080",
    &client
).await?;

// Wait for system to be ready
client.system.wait_system_ready().await;

// Now you can use the client normally
let context = client.context(user_data)?;
}

Transaction Error Handling

#![allow(unused)]
fn main() {
use ankurah::error::Result;

async fn create_post_with_validation(
    context: &Context,
    title: &str,
    content: &str,
) -> Result<Entity> {
    // Validate input
    if title.is_empty() {
        return Err(AnkurahError::validation("Title cannot be empty"));
    }

    let trx = context.begin();

    // Create the post
    let post = trx.create(&BlogPost {
        title: title.into(),
        content: content.into(),
        author: "System".into(),
        published: false,
        tags: vec![],
    }).await?;

    // Commit (or automatically rollback on error)
    trx.commit().await?;

    Ok(post)
}
}

Working with Collections

#![allow(unused)]
fn main() {
// Get a collection reference
let posts = context.collection::<BlogPost>("BlogPost");

// Count entities
let count = posts.count().await?;
println!("Total posts: {}", count);

// Iterate all entities (careful with large collections!)
let all_posts = posts.all().await?;
for post in all_posts {
    let view: BlogPostView = post.view()?;
    println!("- {}", view.title);
}
}

Custom Storage Backend

#![allow(unused)]
fn main() {
use ankurah::storage::*;

// Create nodes with different backends

// Sled (embedded KV)
let node = Node::new(
    Arc::new(SledStorageEngine::new("./data")?),
    agent
);

// Postgres
let node = Node::new(
    Arc::new(PostgresStorageEngine::connect("postgresql://...").await?),
    agent
);

// IndexedDB (WASM only)
let node = Node::new(
    Arc::new(IndexedDBStorageEngine::new("my-app").await?),
    agent
);
}

Next Steps