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

Bootstrapping

Overview

The Bootstrap system provides secure key management for Eidetica databases by controlling how new devices gain access to synchronized databases. It supports two approval methods:

  1. Global Wildcard Permissions - Databases with global ‘*’ permissions automatically approve bootstrap requests without adding new keys
  2. Manual Approval - Bootstrap requests are queued for administrator review and explicit approval

Global Permission Bootstrap

Global ‘*’ permissions provide the simplest and most flexible approach for collaborative or public databases:

How It Works

When a database has global permissions configured (e.g., {"*": {"pubkey": "*", "permissions": "Write: 10"}}), bootstrap requests are automatically approved if the requested permission level is satisfied by the global permission. No new keys are added to the database.

Devices use the global permission for both bootstrap approval and subsequent operations (transactions, reads, writes). The system automatically resolves to the global “*” key when a device’s specific key is not present in the database’s auth settings.

Advantages

  • No key management: Devices don’t need individual keys added to database
  • Immediate access: Bootstrap approval happens instantly
  • Simple configuration: One permission setting controls all devices
  • Flexible permissions: Set exactly the permission level you want to allow

Configuration Example

Configure a database with global write permission:

use eidetica::crdt::Doc;
use eidetica::auth::{AuthSettings, AuthKey, Permission};

// Create database settings with global permission
let mut settings = Doc::new();
settings.set("name", "shared_database");

// Configure auth settings
let mut auth_settings = AuthSettings::new();

// Add global permission for automatic bootstrap approval
auth_settings.add_key(
    "*",  // Wildcard matches any key
    AuthKey::active("*", Permission::Write(10))?,  // Allows Read and Write(11+) requests
)?;

settings.set("auth", auth_settings.to_doc());

// Create database through AccountSession API (specified key gets Admin)
let key_id = user.get_default_key()?;
let database = user.create_database(settings, &key_id).await?;

Permission Levels

Eidetica uses lower numbers = higher permissions:

  • Global Write(10) allows: Read, Write(11), Write(15), etc.
  • Global Write(10) denies: Write(5), Admin(*)

Choose your global permission level carefully based on your security requirements.

Client Workflow

From the client’s perspective, the bootstrap process follows these steps:

1. Initial Bootstrap Attempt

The client initiates a bootstrap request when it needs access to a synchronized database:

// Request database access through AccountSession API
user.request_database_access(
    &sync,
    &ticket,
    &key_id,                 // Client's key ID (from user.add_private_key())
    Permission::Write(5),    // Requested permission level
    SyncSettings::on_commit(),
    None,
).await

AccountSession::join takes the same arguments without the Sync handle and returns the opened database. It uses the instance’s own sync engine when embedded and the daemon’s when connected to a service daemon, where the user’s private key stays in the client process:

let database = user
    .join(&ticket, &key_id, Permission::Write(5), SyncSettings::on_commit(), None)
    .await?;

join has the same interface in embedded and connected modes and waits for the peer’s answer. A ticket is a locator, not an access grant. Success returns a Database after recording the selected user key’s SigKey mapping and the caller’s SyncSettings, so user.open_database() can open it again. SyncError::BootstrapPending changes no Account mappings or preferences: explicitly retry join with the same arguments after approval. SyncError::BootstrapRejected is terminal for that request and records no new mapping or preferences.

Success means the finite supplied response and its ancestry have verified locally and the selected key currently satisfies the requested permission; it is not global catch-up. Identical completed Account settings and established routing are not written again. Newer preferences observed after the network wait are preserved, and legacy provisional mappings are not deleted on Pending or rejection. An error, dropped future or disconnected caller can leave a peer request, replicated entries, routing or a persisted local account record without its acknowledgement. Reconnect and retry explicitly; a failed call does not imply rollback. There is no cancellation API, automatic restart-safe completion, or device enrollment. The client holds and signs with the user’s private keys; the daemon owns networking, without a second client sync engine. Later daemon replication uses its device key, which needs its own authorization or a global grant. User-key access alone does not promise ongoing private background replication.

Current recovery limits

Acquisition of an already-held database that the requesting key cannot read, including an orphan replica, remains refused. Join advertises empty tips and never sends local entries back, but this does not resolve native competing grant/revocation semantics. Missing delegated signing history leaves the affected entries Unverified and the transfer incomplete, not successful or terminally Failed. Native verification supports supplying and verifying the missing history before manually retrying on the same replica; join does not acquire that history automatically or bypass the held-tree Read gate. Complete held/unreadable, orphan and second-user recovery remains unsupported. The basic API uses existing native authority semantics; it does not fix competing grant/revocation resolution or guarantee that stale grants cannot restore access after ingestion. See the verification model for those native limitations.

2. Response Handling

The client must handle different response scenarios:

  • Global Wildcard Permission Approved:

    • Request succeeds immediately
    • Client gains access via global permission
    • No individual key added to database
    • Can proceed with normal operations
  • Manual Approval Required (default):

    • Request fails with an error
    • Error indicates request is “pending”
    • Bootstrap request is queued for admin review

3. Waiting for Approval

While the request is pending, the client has several options:

  • Polling Strategy: Periodically retry sync operations
  • Event-Based: Wait for notification from server (future enhancement)
  • User-Triggered: Let user manually retry when they expect approval

4. After Admin Decision

If Approved:

  • Retry user.request_database_access() with the same SyncSettings
  • The retry records the selected settings while it synchronizes the database
  • Once it succeeds, client can load and use the database normally

If Rejected:

  • All sync attempts continue to fail
  • Client remains unable to access the database
  • May submit a new request with different parameters if appropriate

5. Retry Logic Example

use eidetica::{sync::DatabaseTicket, account::types::SyncSettings};

async fn bootstrap_with_retry(
    user: &mut AccountSession,
    sync: &Sync,
    ticket: &DatabaseTicket,
    key_id: &PublicKey,
) -> Result<()> {
    // Initial bootstrap request
    if let Err(_) = user.request_database_access(
        sync, ticket, key_id, Permission::Write(5), SyncSettings::on_commit(), None
    ).await {
        println!("Bootstrap request pending approval...");

        // Poll for approval (with backoff)
        for attempt in 0..10 {
            tokio::time::sleep(Duration::from_secs(30 * (attempt + 1))).await;

            // Retry with the same explicit preferences after potential approval.
            if user.request_database_access(
                sync, ticket, key_id, Permission::Write(5), SyncSettings::on_commit(), None
            ).await.is_ok() {
                println!("Access granted!");
                return Ok(());
            }
        }

        return Err("Bootstrap request timeout or rejected".into());
    }

    Ok(()) // Auto-approved
}

Usage Examples

Manual Approval Workflow

For administrators managing bootstrap requests:

// 1. List pending requests
let pending = sync.pending_bootstrap_requests()?;
for (request_id, request) in pending {
    println!("Request {}: {} wants {} access to tree {}",
        request_id,
        request.requesting_key_name,
        request.requested_permission,
        request.tree_id
    );
}

// 2. Approve a request (using AccountSession API)
user.approve_bootstrap_request(
    &sync,
    "bootstrap_a1b2c3d4...",
    &admin_key_id  // Your admin key ID (must have Admin permission)
).await?;

// 3. Or reject a request
user.reject_bootstrap_request(
    &sync,
    "bootstrap_e5f6g7h8...",
    &admin_key_id
).await?;

Complete Client Bootstrap Example

// Step 1: Initial bootstrap attempt with authentication
let bootstrap_result = user.request_database_access(
    &sync,
    &ticket,
    &key_id,  // Account's key ID from user.add_private_key()
    Permission::Write(5),
    SyncSettings::on_commit(),
    None,
).await;

// Step 2: Handle the response based on approval method
match bootstrap_result {
    Ok(_) => {
        // Global wildcard permission granted immediate access
        println!("Bootstrap approved via global permission! Access granted immediately.");
    },
    Err(e) => {
        // Manual approval required
        // The error indicates the request is pending
        println!("Bootstrap request submitted, awaiting admin approval...");

        // Step 3: Wait for admin to review and approve
        // Options:
        // a) Poll periodically
        // b) Wait for out-of-band notification
        // c) User-triggered retry

        // Step 4: After admin approval, retry with the same sync preferences.
        tokio::time::sleep(Duration::from_secs(30)).await;

        match user.request_database_access(
            &sync,
            &ticket,
            &key_id,
            Permission::Write(5),
            SyncSettings::on_commit(),
            None,
        ).await {
            Ok(_) => {
                println!("Access granted! Database synchronized.");
                // Client can now load and use the database
                let db = user.open_database(ticket.database_id()).await?;
            },
            Err(_) => {
                println!("Still pending or rejected. Check with admin.");
            }
        }
    }
}

// Handling rejection scenario
// If the request was rejected, all sync attempts will continue to fail
// The client will need to submit a new bootstrap request if appropriate

Security Considerations

Trust Model

  • Global Wildcard Permissions: Trusts any device that can reach the sync endpoint

    • Suitable for: Development, collaborative projects, public databases
    • Risk: Any device can gain the configured global permissions
    • Benefit: Simple, immediate access for authorized scenarios
  • Manual Approval: Requires explicit admin action for each device

    • Suitable for: Production, sensitive data, controlled access scenarios
    • Benefit: Complete control over which devices gain access
    • Risk: Administrative overhead for each new device

Troubleshooting

Common Issues

  1. “Authentication required but not configured”

    • Cause: Sync handler cannot authenticate with target database
    • Solution: Ensure proper key configuration for database operations
  2. “Invalid request state”

    • Cause: Attempting to approve/reject non-pending request
    • Solution: Check request status before operation

Performance Considerations

  • Sync database grows linearly with request count
  • Request queries are indexed by ID