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:
- Global Wildcard Permissions - Databases with global ‘*’ permissions automatically approve bootstrap requests without adding new keys
- 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 sameSyncSettings - 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
-
“Authentication required but not configured”
- Cause: Sync handler cannot authenticate with target database
- Solution: Ensure proper key configuration for database operations
-
“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