Sentinel Logo

Quick Start

Create your first Sentinel store, collection, and documents in minutes.

This guide walks you through creating your first Sentinel database in just a few minutes. By the end, you’ll understand how to create stores, manage collections, work with documents, and use advanced features like querying and verification.

Your First Store

A Store is the top-level container for all your data. It maps to a directory on your filesystem:

use sentinel_dbms::Store;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a store at ./my-database
    // The directory will be created if it doesn't exist
    let store = Store::new("./my-database", None).await?;

    println!("Store created at ./my-database");
    Ok(())
}

The second parameter (None) indicates we’re not using a passphrase for signing. We’ll cover signed documents later. After running this code, you’ll see a new directory:

my-database/

The store is now ready to use. Let’s add some collections and documents.

Creating a Collection

Collections are namespaces for related documents. They’re represented as subdirectories within your store:

use sentinel_dbms::Store;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;

    // Get or create a "users" collection
    let users = store.collection("users").await?;

    println!("Collection 'users' ready!");
    Ok(())
}

Your filesystem now looks like:

my-database/
└── data/
    └── users/

The collection is ready for storing documents.

Inserting Documents

Documents are JSON objects stored as individual files. Use insert to create a new document:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Insert a document with ID "alice"
    users.insert("alice", json!({
        "name": "Alice Johnson",
        "email": "[email protected]",
        "role": "admin",
        "department": "Engineering"
    })).await?;

    println!("Document 'alice' created!");
    Ok(())
}

This creates a file at my-database/data/users/alice.json containing:

{
  "id": "alice",
  "version": 1,
  "created_at": "2026-01-15T12:00:00.000000Z",
  "updated_at": "2026-01-15T12:00:00.000000Z",
  "hash": "a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789a",
  "signature": "",
  "data": {
    "name": "Alice Johnson",
    "email": "[email protected]",
    "role": "admin",
    "department": "Engineering"
  }
}

Notice how Sentinel automatically adds metadata: document ID, version number, timestamps, and a BLAKE3 hash for integrity verification.

Retrieving Documents

Use get to retrieve a document by its ID:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Retrieve the document
    if let Some(doc) = users.get("alice").await? {
        println!("Found user: {}", doc.data()["name"]);
        println!("Email: {}", doc.data()["email"]);
        println!("Role: {}", doc.data()["role"]);
        println!("Created at: {}", doc.created_at());
        println!("Hash: {}", doc.hash());
    } else {
        println!("User not found");
    }

    Ok(())
}

The get method returns Option<Document>. If the document doesn’t exist, you get None instead of an error.

Updating Documents

The update method replaces a document’s contents while preserving the file location:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Update Alice's information
    users.update("alice", json!({
        "name": "Alice Johnson",
        "email": "[email protected]",  // New email
        "role": "senior_admin",                // Promoted!
        "department": "Engineering"
    })).await?;

    println!("Document updated!");
    Ok(())
}

The updated document gets a new timestamp and hash, allowing you to track when changes occurred.

Deleting Documents

Use delete to remove a document:

use sentinel_dbms::Store;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Delete the document
    users.delete("alice").await?;

    println!("Document deleted!");
    Ok(())
}

The delete operation is idempotent, deleting a non-existent document succeeds without error. Documents are moved to a .deleted/ subdirectory for recovery purposes.

Bulk Inserting Documents

For inserting multiple documents efficiently, use the bulk_insert method:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Prepare multiple documents
    let documents = vec![
        ("bob", json!({"name": "Bob", "role": "user"})),
        ("charlie", json!({"name": "Charlie", "role": "user"})),
        ("david", json!({"name": "David", "role": "admin"})),
    ];

    // Insert all at once
    users.bulk_insert(documents).await?;

    println!("{} documents inserted", documents.len());
    Ok(())
}

Listing Documents

List all documents in a collection using the list or count methods:

use sentinel_dbms::Store;
use futures::TryStreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

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

    // List all document IDs
    let ids: Vec<_> = users.list().try_collect().await?;
    println!("Document IDs:");
    for id in ids {
        println!("  - {}", id);
    }

    Ok(())
}

Adding Signatures

For tamper-evident storage, create a store with a passphrase. This generates a signing key that will be used to sign all documents:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create store with passphrase for signing
    let store = Store::new("./secure-database", Some("my-secret-passphrase")).await?;

    let users = store.collection("users").await?;

    // Insert a document (will be signed automatically)
    users.insert("alice", json!({
        "name": "Alice",
        "email": "[email protected]"
    })).await?;

    // Retrieve and check signature
    if let Some(doc) = users.get("alice").await? {
        println!("Document ID: {}", doc.id());
        println!("Signature: {}", doc.signature());
        // Will show: "ed25519:..." (not empty)
    }

    Ok(())
}

When you create a store with a passphrase:

  1. Sentinel generates a random 32-byte salt
  2. An Ed25519 signing key is derived using the configured key derivation function (Argon2id by default)
  3. The signing key is encrypted using the configured encryption algorithm (XChaCha20-Poly1305 by default)
  4. The encrypted key and salt are stored in .keys/signing_key.json

On subsequent opens, you must provide the same passphrase to recover the signing key.

Querying Documents

Find documents matching specific criteria using the QueryBuilder API:

use sentinel_dbms::{Store, QueryBuilder, Operator, SortOrder};
use serde_json::json;
use futures::TryStreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Insert some test data
    users.insert("user1", json!({"name": "Alice", "age": 30})).await?;
    users.insert("user2", json!({"name": "Bob", "age": 25})).await?;
    users.insert("user3", json!({"name": "Charlie", "age": 35})).await?;

    // Find users older than 25
    let query = QueryBuilder::new()
        .filter("age", Operator::GreaterThan, json!(25))
        .sort("age", SortOrder::Ascending)
        .build();

    let result = users.query(query).await?;
    let documents: Vec<_> = result.documents.try_collect().await?;

    println!("Found {} users older than 25:", documents.len());
    for doc in documents {
        println!("  - {}, age: {}", doc.data()["name"], doc.data()["age"]);
    }

    Ok(())
}

Streaming All Documents

Process all documents without loading them all into memory:

use sentinel_dbms::Store;
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Stream all documents
    let mut all_docs = users.all();

    let mut count = 0;
    while let Some(doc_result) = all_docs.next().await {
        match doc_result {
            Ok(doc) => {
                println!("Document: {}", doc.id());
                count += 1;
            }
            Err(e) => {
                eprintln!("Error reading document: {}", e);
            }
        }
    }

    println!("Total documents processed: {}", count);
    Ok(())
}

Using Filters with Closures

For simple filtering, use closures with the filter method:

use sentinel_dbms::Store;
use futures::StreamExt;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;
    let users = store.collection("users").await?;

    // Insert test data
    users.insert("user1", json!({"name": "Alice", "role": "admin"})).await?;
    users.insert("user2", json!({"name": "Bob", "role": "user"})).await?;
    users.insert("user3", json!({"name": "Charlie", "role": "admin"})).await?;

    // Find all admins
    let mut admins = users.filter(|doc| {
        doc.data().get("role")
            .and_then(|v| v.as_str())
            .map_or(false, |role| role == "admin")
    });

    while let Some(doc) = admins.next().await {
        let doc = doc?;
        println!("Admin: {}", doc.data()["name"]);
    }

    Ok(())
}

Managing Multiple Collections

A store can contain multiple collections:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;

    // Create multiple collections
    let users = store.collection("users").await?;
    let products = store.collection("products").await?;
    let orders = store.collection("orders").await?;

    // Work with each collection
    users.insert("alice", json!({"name": "Alice"})).await?;
    products.insert("widget", json!({"name": "Widget", "price": 9.99})).await?;
    orders.insert("order-123", json!({"user_id": "alice", "total": 29.97})).await?;

    // List all collections in the store
    let collections = store.list_collections().await?;
    println!("Collections: {:?}", collections);

    Ok(())
}

Deleting Collections

Remove an entire collection and all its documents:

use sentinel_dbms::Store;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = Store::new("./my-database", None).await?;

    // Create a temporary collection
    let temp = store.collection("temp").await?;
    temp.insert("doc1", json!({})).await?;

    // Delete the entire collection
    store.delete_collection("temp").await?;

    println!("Collection deleted!");

    Ok(())
}

What’s Next?

Congratulations! You’ve completed the quick start guide. Now that you understand the basics, explore:

  • Store: Advanced store features like listing and deleting collections
  • Collection: Bulk operations, streaming, and verification options
  • Document: Understanding document structure and metadata
  • Cryptography: Configuring algorithms and verification modes
  • Querying and Filtering: Advanced query building and operators
  • Error Handling: Understanding and handling errors properly