Sentinel Logo

Document

Learn about Document structure, metadata fields, and integrity verification in Sentinel.

Documents are the fundamental unit of storage in Sentinel. Each document is a JSON file containing your data along with metadata for versioning, timestamps, cryptographic integrity verification, and optional digital signatures. This guide explains document structure, how to work with document metadata, and how verification works.

Document Structure

Every document stored in Sentinel has this structure:

{
  "id": "user-123",
  "version": 1,
  "created_at": "2026-01-15T12:00:00.000000Z",
  "updated_at": "2026-01-15T12:00:00.000000Z",
  "hash": "a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789a",
  "signature": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
  "data": {
    "name": "Alice",
    "email": "[email protected]"
  }
}

The structure has two main parts: metadata fields managed by Sentinel and a data field containing your actual content.

Metadata Fields

Each document includes several metadata fields that Sentinel manages automatically.

ID

The document identifier, which becomes the filename:

let doc = users.get("alice").await?.unwrap();
println!("Document ID: {}", doc.id());  // "alice"

The ID must be a valid filename. See the Collection documentation for validation rules.

Version

The metadata format version used by Sentinel:

println!("Version: {}", doc.version());  // Currently 1

This allows future versions of Sentinel to handle documents created by older versions. The version number is defined by the DOCUMENT_SENTINEL_VERSION constant (currently 1).

Timestamps

Two timestamps track the document lifecycle:

println!("Created: {}", doc.created_at());   // When first inserted
println!("Updated: {}", doc.updated_at());   // When last modified

Timestamps are in UTC using RFC 3339 format with microsecond precision. The created_at timestamp is set when the document is first created and never changes. The updated_at timestamp is updated every time the document data is modified.

Hash

A BLAKE3 cryptographic hash of the document data:

println!("Hash: {}", doc.hash());  // 64-character hex string

The hash is computed from the data field only (not metadata). It enables integrity verification—if the data changes, the hash changes. This provides a reliable way to detect data corruption or tampering.

use sentinel_crypto::hash_data;

// Recompute hash from data
let computed_hash = hash_data(doc.data()).await?;

// Verify integrity
if doc.hash() == computed_hash {
    println!("✓ Data integrity verified");
} else {
    println!("✗ Data may be corrupted!");
}

Signature

An optional Ed25519 digital signature:

println!("Signature: {}", doc.signature());

If the store was created with a passphrase, documents are signed automatically. The signature covers the hash, providing tamper-evident storage. An empty string indicates an unsigned document.

Signed Document Example:

{
  "signature": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
}

Unsigned Document Example:

{
  "signature": ""
}

Data

Your actual document content as a JSON value:

let data = doc.data();
println!("Name: {}", data["name"]);
println!("Email: {}", data["email"]);

The data field can contain any valid JSON: objects, arrays, strings, numbers, booleans, or null.

Creating Documents

Documents are created through the Collection 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("./data", Some("passphrase")).await?;
    let users = store.collection("users").await?;

    // Insert creates a new document with all metadata
    users.insert("alice", json!({
        "name": "Alice",
        "email": "[email protected]",
        "role": "admin"
    })).await?;

    // The file now contains full metadata
    Ok(())
}

Sentinel handles:

  1. Setting the ID to match the provided key
  2. Setting the version to the current metadata version
  3. Setting created_at and updated_at to the current time
  4. Computing the BLAKE3 hash of your data
  5. Signing the hash with Ed25519 if a passphrase was provided
  6. Writing the document atomically to disk as {id}.json

Creating Unsigned Documents

If you don’t have a signing key or want to skip signing:

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

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

    users.insert("alice", json!({
        "name": "Alice",
        "email": "[email protected]"
    })).await?;

    // Document will have empty signature
    Ok(())
}

Reading Document Properties

Access document properties through dedicated methods:

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

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

    users.insert("alice", json!({"name": "Alice", "level": 5})).await?;

    if let Some(doc) = users.get("alice").await? {
        // Metadata
        println!("ID: {}", doc.id());
        println!("Version: {}", doc.version());
        println!("Created: {}", doc.created_at());
        println!("Updated: {}", doc.updated_at());
        println!("Hash: {}", doc.hash());
        println!("Signature: {}", doc.signature());

        // Data access
        let data = doc.data();
        println!("Name: {}", data["name"]);
        println!("Level: {}", data["level"]);

        // Check if data is an object
        if let Some(obj) = data.as_object() {
            for (key, value) in obj {
                println!("  {}: {}", key, value);
            }
        }
    }

    Ok(())
}

Document Integrity

Sentinel provides two layers of integrity verification: hash verification for data integrity and signature verification for tamper evidence.

Hash Verification

The BLAKE3 hash allows you to verify data hasn’t been corrupted:

use sentinel_dbms::Store;
use sentinel_crypto::hash_data;
use serde_json::json;

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

    users.insert("alice", json!({"name": "Alice"})).await?;

    if let Some(doc) = users.get("alice").await? {
        // Recompute hash from data
        let computed_hash = hash_data(doc.data()).await?;

        // Compare with stored hash
        if doc.hash() == computed_hash {
            println!("✓ Data integrity verified");
        } else {
            println!("✗ Data may be corrupted!");
            println!("  Expected: {}", doc.hash());
            println!("  Computed: {}", computed_hash);
        }
    }

    Ok(())
}

Hash verification ensures that the document data hasn’t been accidentally corrupted (e.g., disk errors, file corruption).

Signature Verification

For signed documents, verify the signature using the store’s public key:

use sentinel_dbms::Store;
use sentinel_crypto::{verify_signature, hash_data};
use serde_json::json;

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

    users.insert("alice", json!({"name": "Alice"})).await?;

    if let Some(doc) = users.get("alice").await? {
        if !doc.signature().is_empty() {
            // Recompute hash for verification
            let computed_hash = hash_data(doc.data()).await?;

            // Verify signature (requires access to public key from signing key)
            // This is a simplified example
            println!("Document is signed");
            println!("  Hash: {}", doc.hash());
            println!("  Signature: {}", doc.signature());
        } else {
            println!("Document is unsigned");
        }
    }

    Ok(())
}

Signature verification provides tamper evidence—if someone modifies the document data without the signing key, the signature verification will fail.

Verification Modes

Sentinel provides flexible verification modes through VerificationOptions:

use sentinel_dbms::{Store, VerificationOptions, VerificationMode};

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

    // Strict mode (default for production)
    let strict_options = VerificationOptions::strict();
    let doc = users.get_with_verification("alice", &strict_options).await?;

    // Warning mode (for auditing)
    let warn_options = VerificationOptions::warn();
    let doc = users.get_with_verification("alice", &warn_options).await?;

    // Disabled mode (for performance)
    let disabled_options = VerificationOptions::disabled();
    let doc = users.get_with_verification("alice", &disabled_options).await?;

    Ok(())
}
  • Strict: Fail immediately on verification errors
  • Warn: Log warnings but continue processing
  • Silent: Skip verification entirely

See the Error Handling guide for details on handling verification errors.

File Format

Documents are stored as pretty-printed JSON files:

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

The pretty-printed format makes files easy to read with any text editor, diff tool, or version control system.

Working with Complex Data

The data field can contain any JSON structure:

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

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

    // Nested objects
    records.insert("config", json!({
        "database": {
            "host": "localhost",
            "port": 5432,
            "credentials": {
                "username": "admin",
                "password_hash": "sha256:..."
            }
        },
        "features": {
            "caching": true,
            "compression": false
        }
    })).await?;

    // Arrays
    records.insert("tags", json!({
        "items": ["rust", "database", "filesystem"],
        "count": 3
    })).await?;

    // Mixed types
    records.insert("mixed", json!({
        "string": "hello",
        "number": 42,
        "float": 3.14,
        "boolean": true,
        "null_value": null,
        "array": [1, "two", false],
        "object": {"nested": "value"}
    })).await?;

    Ok(())
}

Accessing Nested Data

Navigate nested structures using serde_json’s indexing:

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

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

    config.insert("app", json!({
        "server": {
            "host": "0.0.0.0",
            "port": 8080,
            "tls": {
                "enabled": true,
                "cert_path": "/etc/certs/server.crt"
            }
        }
    })).await?;

    if let Some(doc) = config.get("app").await? {
        let data = doc.data();

        // Direct access
        let host = &data["server"]["host"];
        let port = &data["server"]["port"];
        let tls_enabled = &data["server"]["tls"]["enabled"];

        println!("Server: {}:{}", host, port);
        println!("TLS: {}", tls_enabled);

        // Safe access with type conversion
        if let Some(port_num) = data["server"]["port"].as_u64() {
            println!("Port as number: {}", port_num);
        }

        // Check for existence
        if data["server"]["tls"]["enabled"].as_bool().unwrap_or(false) {
            println!("TLS is enabled");
        }
    }

    Ok(())
}

Document Versioning

Sentinel’s version field allows for future compatibility:

use sentinel_dbms::DOCUMENT_SENTINEL_VERSION;

// Current version
println!("Current sentinel version: {}", DOCUMENT_SENTINEL_VERSION);  // Returns 1

// All documents have this version
let doc_version = doc.version();
assert_eq!(doc_version, DOCUMENT_SENTINEL_VERSION);

This design allows Sentinel to:

  • Add new metadata fields without breaking existing documents
  • Change serialization formats while maintaining backward compatibility
  • Introduce new features that older versions can ignore
  • Migrate documents automatically when needed

Best Practices

When working with documents, keep these recommendations in mind:

  • Design your data schema thoughtfully. Even though Sentinel is schema-less, consistent structures make querying and maintenance easier.

  • Keep documents reasonably sized. While there’s no hard limit, very large documents (megabytes) may impact performance.

  • Use meaningful IDs. Choose IDs that help identify documents: user-{uuid}, order-{date}-{sequence}, cert-{fingerprint}.

  • Validate before inserting. Sentinel stores whatever JSON you provide—validate your data in your application.

  • Consider document relationships. Store related IDs as references: {"user_id": "user-123"} rather than embedding entire documents. Currently, Sentinel does not support joins, so design accordingly.

  • Use appropriate verification modes. Use strict mode for production, warn mode for auditing, and silent mode for performance-critical scenarios.

  • Leverage hash verification. Regularly verify document hashes to detect data corruption early.

  • Back up regularly. Since documents are just files, use standard backup tools.

Next Steps

Now that you understand documents, explore: