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 1This 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 modifiedTimestamps 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 stringThe 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:
- Setting the ID to match the provided key
- Setting the version to the current metadata version
- Setting
created_atandupdated_atto the current time - Computing the BLAKE3 hash of your data
- Signing the hash with Ed25519 if a passphrase was provided
- 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:
- Cryptography: Deep dive into hashing, signing, and encryption
- Querying and Filtering: Advanced document retrieval and filtering
- Error Handling: Understanding Sentinel errors and verification failures
- Collection: Advanced collection operations and management