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

Getting Started

This guide will walk you through the basics of using Eidetica in your Rust applications. We'll cover the essential steps to set up and interact with the database.

For contributing to Eidetica itself, see the Contributing guide.

Installation

Add Eidetica to your project dependencies:

[dependencies]
eidetica = "0.1.0"  # Update version as appropriate
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# Or if using from a local workspace:
# eidetica = { path = "path/to/eidetica/crates/lib" }

Eidetica uses an async-first API built on Tokio. All database operations are async and must be called within a Tokio runtime.

Setting up the Database

To start using Eidetica, you need to:

  1. Choose and initialize a Backend (storage mechanism)
  2. Create an Instance (the infrastructure manager)
  3. Create and login a User (authentication and session)
  4. Create or access a Database through the User (logical container for data)

Here's a simple example:

extern crate eidetica;
extern crate tokio;
use eidetica::{backend::database::Sqlite, Instance, crdt::Doc};

#[tokio::main]
async fn main() -> eidetica::Result<()> {
    // Create a new in-memory backend
    let backend = Sqlite::in_memory().await?;

    // Create the Instance
    let instance = Instance::open(Box::new(backend)).await?;

    // Create a passwordless user (perfect for embedded/single-user apps)
    instance.create_user("alice", None).await?;

    // Login to get a User session
    let mut user = instance.login_user("alice", None).await?;

    // Create a database in the user's context
    let mut settings = Doc::new();
    settings.set("name", "my_database");

    // Get the default key (earliest created key)
    let default_key = user.get_default_key()?;
    let _database = user.create_database(settings, &default_key).await?;

    Ok(())
}

Note: This example uses a passwordless user (password is None) for simplicity, which is perfect for embedded applications and CLI tools. For multi-user scenarios, you can create password-protected users by passing Some("password") instead.

The backend determines how your data is stored. The example above uses Sqlite::in_memory(), which keeps everything in memory. For persistent storage, use a file-based SQLite database:

use eidetica::{Instance, backend::database::Sqlite, crdt::Doc};

#[tokio::main]
async fn main() -> eidetica::Result<()> {
    // Create a persistent SQLite backend (data is saved automatically)
    let backend = Sqlite::open("my_data.db").await?;
    let instance = Instance::open(Box::new(backend)).await?;

    // Create user and database as before
    instance.create_user("alice", None).await?;
    let mut user = instance.login_user("alice", None).await?;
    // ... all changes are automatically persisted to my_data.db

    Ok(())
}

You can reopen a previously created database:

use eidetica::{Instance, backend::database::Sqlite};

#[tokio::main]
async fn main() -> eidetica::Result<()> {
    // Reopen existing SQLite database
    let backend = Sqlite::open("my_data.db").await?;
    let instance = Instance::open(Box::new(backend)).await?;

    // Login to existing user
    let user = instance.login_user("alice", None).await?;
    // ... data persists across restarts

    Ok(())
}

User-Centric Architecture

Eidetica uses a user-centric architecture:

  • Instance: Manages infrastructure (user accounts, backend, system databases)
  • User: Handles all contextual operations (database creation, key management)

All database and key operations happen through a User session after login. This provides:

  • Clear separation: Infrastructure management vs. contextual operations
  • Strong isolation: Each user has separate keys and preferences
  • Flexible authentication: Users can have passwords or not (passwordless mode)

Passwordless Users (embedded/single-user apps):

instance.create_user("alice", None)?;
let user = instance.login_user("alice", None)?;

Password-Protected Users (multi-user apps):

instance.create_user("bob", Some("password123"))?;
let user = instance.login_user("bob", Some("password123"))?;

The downside of password protection is a slow login. instance.login_user needs to verify the password and decrypt keys, which by design is a relatively slow operation.

Working with Data

Eidetica uses Stores to organize data within a database. One common store type is Table, which maintains a collection of items with unique IDs.

Defining Your Data

Any data you store must be serializable with serde:

Basic Operations

All operations in Eidetica happen within an atomic Transaction:

Inserting Data:

extern crate eidetica;
extern crate tokio;
extern crate serde;
use eidetica::{backend::database::Sqlite, Instance, crdt::Doc, store::Table, Database};
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

#[tokio::main]
async fn main() -> eidetica::Result<()> {
let instance = Instance::open(Box::new(Sqlite::in_memory().await?)).await?;
instance.create_user("alice", None).await?;
let mut user = instance.login_user("alice", None).await?;
let mut settings = Doc::new();
settings.set("name", "test_db");
let default_key = user.get_default_key()?;
let database = user.create_database(settings, &default_key).await?;

// Start an authenticated transaction
let op = database.new_transaction().await?;

// Get or create a Table store
let people = op.get_store::<Table<Person>>("people").await?;

// Insert a person and get their ID
let person = Person { name: "Alice".to_string(), age: 30 };
let _id = people.insert(person).await?;

// Commit the changes (automatically signed with the user's key)
op.commit().await?;
Ok(())
}

Reading Data:

extern crate eidetica;
extern crate tokio;
extern crate serde;
use eidetica::{backend::database::Sqlite, Instance, crdt::Doc, store::Table, Database};
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

#[tokio::main]
async fn main() -> eidetica::Result<()> {
let instance = Instance::open(Box::new(Sqlite::in_memory().await?)).await?;
instance.create_user("alice", None).await?;
let mut user = instance.login_user("alice", None).await?;
let mut settings = Doc::new();
settings.set("name", "test_db");
let default_key = user.get_default_key()?;
let database = user.create_database(settings, &default_key).await?;
// Insert some test data
let op = database.new_transaction().await?;
let people = op.get_store::<Table<Person>>("people").await?;
let test_id = people.insert(Person { name: "Alice".to_string(), age: 30 }).await?;
op.commit().await?;
let id = &test_id;

let op = database.new_transaction().await?;
let people = op.get_store::<Table<Person>>("people").await?;

// Get a single person by ID
if let Ok(person) = people.get(id).await {
    println!("Found: {} ({})", person.name, person.age);
}

// Search for all people (using a predicate that always returns true)
let all_people = people.search(|_| true).await?;
for (id, person) in all_people {
    println!("ID: {}, Name: {}, Age: {}", id, person.name, person.age);
}
Ok(())
}

Updating Data:

extern crate eidetica;
extern crate tokio;
extern crate serde;
use eidetica::{backend::database::Sqlite, Instance, crdt::Doc, store::Table, Database};
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

#[tokio::main]
async fn main() -> eidetica::Result<()> {
let instance = Instance::open(Box::new(Sqlite::in_memory().await?)).await?;
instance.create_user("alice", None).await?;
let mut user = instance.login_user("alice", None).await?;
let mut settings = Doc::new();
settings.set("name", "test_db");
let default_key = user.get_default_key()?;
let database = user.create_database(settings, &default_key).await?;
// Insert some test data
let op_setup = database.new_transaction().await?;
let people_setup = op_setup.get_store::<Table<Person>>("people").await?;
let test_id = people_setup.insert(Person { name: "Alice".to_string(), age: 30 }).await?;
op_setup.commit().await?;
let id = &test_id;

let op = database.new_transaction().await?;
let people = op.get_store::<Table<Person>>("people").await?;

// Get, modify, and update
if let Ok(mut person) = people.get(id).await {
    person.age += 1;
    people.set(id, person).await?;
}

op.commit().await?;
Ok(())
}

Deleting Data:

extern crate eidetica;
extern crate tokio;
extern crate serde;
use eidetica::{backend::database::Sqlite, Instance, crdt::Doc, store::Table, Database};
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

#[tokio::main]
async fn main() -> eidetica::Result<()> {
let instance = Instance::open(Box::new(Sqlite::in_memory().await?)).await?;
instance.create_user("alice", None).await?;
let mut user = instance.login_user("alice", None).await?;
let mut settings = Doc::new();
settings.set("name", "test_db");
let default_key = user.get_default_key()?;
let database = user.create_database(settings, &default_key).await?;
let _id = "test_id";

let op = database.new_transaction().await?;
let people = op.get_store::<Table<Person>>("people").await?;

// FIXME: Table doesn't currently support deletion
// You can overwrite with a "deleted" marker or use other approaches

op.commit().await?;
Ok(())
}

Complete Examples

For complete working examples, see:

  • Chat Example - Multi-user chat application demonstrating:

    • User accounts and authentication
    • Real-time synchronization with HTTP and Iroh transports
    • Bootstrap protocol for joining rooms
    • TUI interface with Ratatui
  • Todo Example - Task management application

Next Steps

After getting familiar with the basics, you might want to explore: