Ankurah

(The root of all cosmic projections of state)

A distributed state-management framework that enables real-time data synchronization across multiple nodes with built-in observability.

Beta It works! But exercise caution in production
Explore

Key Features

Schema-First Design

#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Album {
    #[active_type(YrsString)]
    pub name: String,
    pub artist: String,
    pub year: i32,
}

(YrsString is default backend for String, LWW otherwise)

Live Queries

Rust
// Using selection! macro with ctx.query()
let q: LiveQuery<AlbumView> = ctx.query(selection!("year > 1985"))?;
TypeScript
let albumsQuery: AlbumLiveQuery | undefined;

function queryAlbums(): AlbumLiveQuery {
  albumsQuery ??= Album.query(ctx(), "year > 1985");
  return albumsQuery;
}

The Model macro generates Rust views and browser-facing query types.

React Support

interface Props {
  albums: AlbumLiveQuery;
}
/* Bind a React observer to the component. */
const AlbumList = signalObserver(({ albums }: Props) => {
  return (
    <ul>
      {/* Reading items registers this render as a live-query observer. */}
      {albums.items.map((album) => (
        <li key={album.id.to_base64()}>{album.name}</li>
      ))}
    </ul>
  );
});

Reading items inside signalObserver registers the component.

Flexible Storage

Postgres
let storage = Postgres::open(uri).await?;
SQLite
let storage = SqliteStorageEngine::open("ankurah.sqlite").await?;
Sled
let storage = SledStorageEngine::new()?;
IndexedDB
let storage = IndexedDBStorageEngine::open("myapp").await?;

Generated Interfaces

export class Album {
  static query(
    context: Context,
    selection: string,
    ...substitution_values: any
  ): AlbumLiveQuery;
  static create(
    transaction: Transaction,
    album: Album
  ): Promise<AlbumView>;
}

Batteries Included

Common building blocks, ready to compose.

Storage engines

  • Postgres
  • SQLite
  • Sled
  • IndexedDB

Connectors & policy

Explore storage engines →

Quick Example

Server
let storage = SledStorageEngine::with_path(storage_dir)?;
let node = Node::new_durable(Arc::new(storage), PermissiveAgent::new());
node.system.wait_loaded().await;
if node.system.root().is_none() {
    node.system.create().await?;
}

let mut server = WebsocketServer::new(node);
println!("Running server...");
server.run("127.0.0.1:9797").await?;
Rust Client
let storage = SledStorageEngine::new_test()?;
let node = Node::new(Arc::new(storage), PermissiveAgent::new());
let _client = WebsocketClient::new(node.clone(), "ws://localhost:9797").await?;
node.system.wait_system_ready().await;

// Create album
let ctx = node.context(ankurah::policy::DEFAULT_CONTEXT)?;
let trx = ctx.begin();
trx.create(&Album { name: "Parade".into(), artist: "Prince".into(), year: 1986 }).await?;
trx.commit().await?;
Wasm Client
let storage = IndexedDBStorageEngine::open("myapp").await?;
let node = Node::new(Arc::new(storage), PermissiveAgent::new());
let client = WebsocketClient::new(node.clone(), server_url)?;
node.system.wait_system_ready().await;

let context = node.context(DEFAULT_CONTEXT)?;

CONTEXT.with(|slot| slot.replace(Some(context)));
CLIENT.with(|slot| slot.replace(Some(client)));
React Component
interface Props {
  albums: AlbumLiveQuery;
}
/* Bind a React observer to the component. */
const AlbumList = signalObserver(({ albums }: Props) => {
  return (
    <ul>
      {/* Reading items registers this render as a live-query observer. */}
      {albums.items.map((album) => (
        <li key={album.id.to_base64()}>{album.name}</li>
      ))}
    </ul>
  );
});
More Examples →

Ready to Start?

Get up and running quickly with our React + Sled template

cargo generate https://github.com/ankurah/react-template
View Full Guide →