Synchronization Guide
Eidetica’s sync system enables real-time data synchronization between distributed peers.
Quick Start
1. Enable Sync
extern crate eidetica;
extern crate tokio;
use eidetica::{Instance, backend::database::Sqlite};
#[tokio::main]
async fn main() -> eidetica::Result<()> {
let backend = Box::new(Sqlite::in_memory().await?);
let (instance, mut user) = Instance::create_backend(
backend,
eidetica::NewAccount::passwordless("alice"),
).await?;
instance.enable_sync().await?;
// Create and login a user via the bootstrapped admin
Ok(())
}
2. Start a Server
use eidetica::sync::transports::http::HttpTransport;
let sync = instance.sync().unwrap();
// Register transport with bind address and start accepting connections
sync.register_transport("http", HttpTransport::builder().bind("127.0.0.1:8080")).await?;
sync.accept_connections().await?;
3. Connect and Sync
use eidetica::sync::{Address, DatabaseTicket};
// Using a typed Address
sync.sync_with_peer(&Address::http("127.0.0.1:8080"), Some(&tree_id)).await?;
// Or using a ticket (contains both address and database ID)
let ticket: DatabaseTicket = "eidetica:?db=bafyr4i...&pr=http:127.0.0.1:8080".parse()?;
sync.sync_with_ticket(&ticket).await?;
The system automatically detects whether you need full bootstrap or incremental sync.
Tickets embed the database ID, so sync_with_ticket requires no separate tree ID.
4. Share a Database
Sharing has two caller-visible operations: save this user’s durable signed preference, and separately create a point-in-time locator. The daemon’s combined configuration remains internal:
database.share().await?;
assert!(database.is_shared().await?);
let ticket = database.ticket().await?;
println!("Send this locator to your peer: {ticket}");
A successful write acknowledges durable intent, not daemon reconciliation or network readiness. If the connection fails while committing, the write may have succeeded; reading the user-scoped settings or retrying is safe. A ticket is a point-in-time locator and may contain no addresses when the owner is not currently advertising one. See Database Sharing for settings, ticket, and migration details.
A peer who receives the ticket calls sync.sync_with_ticket(&ticket) as
shown in step 3. The ticket identifies the database and address hints; it does
not grant access, so protected databases still require authorization or the
bootstrap approval flow.
Connection Architecture
The sync system separates outbound and inbound connection handling:
- Outbound (
sync_with_peer()): Works immediately afterregister_transport(). No server needed. - Inbound (
accept_connections()): Must be called to accept incoming connections.
register_transport()
│ - Transport ready for outbound requests
▼
[OUTBOUND READY]
│ - sync_with_peer() works
│ - Push hooks work
│ - NO incoming connections
▼
accept_connections()
│ - Starts server on registered transports
▼
[FULL P2P]
│ - Outbound works
│ - Inbound works
This separation provides:
- Security by default: Nodes don’t accept incoming connections unless explicitly enabled
- Zero-config outbound: Applications can sync data immediately without server setup
- Flexible deployment: Support client-only, server-only, or full peer-to-peer modes
Transport Options
Eidetica supports multiple transports simultaneously, allowing peers to be reachable via different networks.
HTTP
Simple REST-based sync. Good for development and fixed-IP deployments.
use eidetica::sync::transports::http::HttpTransport;
sync.register_transport("http", HttpTransport::builder().bind("127.0.0.1:8080")).await?;
sync.accept_connections().await?;
Iroh P2P (Recommended)
QUIC-based with NAT traversal. Works through firewalls.
use eidetica::sync::transports::iroh::IrohTransport;
sync.register_transport("iroh", IrohTransport::builder()).await?;
sync.accept_connections().await?;
let my_address = sync.get_server_address_async().await?; // Share this with peers
Eidetica uses iroh’s native default features and N0 networking settings, including relay selection and router port mapping.
Unset relay settings also honor iroh’s IROH_FORCE_STAGING_RELAYS environment switch; explicit relay choices take precedence.
By default, Iroh advertises and resolves addresses on the local network using mDNS, and publishes to and resolves from n0’s DNS service (via DNS and HTTPS).
Known peers can be reached by endpoint ID even when their ticket’s direct addresses are missing or stale.
This address lookup does not discover databases or grant access to them.
For same-host or same-LAN sync without public lookup or relay services:
use iroh::{RelayMode, endpoint::PortmapperConfig};
use eidetica::sync::transports::iroh::IrohTransport;
sync.register_transport(
"iroh",
IrohTransport::builder()
.relay_mode(RelayMode::Disabled)
.portmapper_config(PortmapperConfig::Disabled)
.n0_dns(false),
).await?;
sync.accept_connections().await?;
Use .mdns(false) to disable local address advertisement and lookup where multicast is unavailable or unwanted.
If mDNS cannot initialize, the transport logs a warning and still permits direct connections and any enabled n0 services.
These lookup and portmapper selections are runtime builder options; the transport’s persisted node identity is unchanged.
Supply the overrides again when constructing or registering the transport after a restart.
Multiple Transports
Enable multiple transports for maximum connectivity:
use eidetica::sync::transports::http::HttpTransport;
use eidetica::sync::transports::iroh::IrohTransport;
// Register both HTTP (for local network) and Iroh (for P2P)
sync.register_transport("http", HttpTransport::builder().bind("127.0.0.1:0")).await?;
sync.register_transport("iroh", IrohTransport::builder()).await?;
// Start servers on all transports
sync.accept_connections().await?;
// Get all server addresses
let addresses = sync.get_all_server_addresses_async().await?;
for (transport_name, addr) in addresses {
println!("{}: {}", transport_name, addr);
}
Requests are automatically routed to the appropriate transport based on address type.
Declarative Sync API
For persistent sync relationships:
extern crate eidetica;
extern crate tokio;
use eidetica::sync::{SyncPeerInfo, Address};
use eidetica::auth::PublicKey;
use eidetica::{Instance, backend::database::Sqlite, crdt::Doc};
#[tokio::main]
async fn main() -> eidetica::Result<()> {
let backend = Box::new(Sqlite::in_memory().await?);
let (instance, _) = Instance::connect_or_create_backend(
backend,
eidetica::NewAccount::passwordless("admin"),
).await?;
instance.enable_sync().await?;
let admin = instance.login_account("admin", "").await?;
admin.admin().await?.create_account(eidetica::NewAccount::passwordless("alice")).await?;
let mut user = instance.login_account("alice", "").await?;
let default_key = user.get_default_key()?;
let db = user.create_database(Doc::new(), &default_key).await?;
let tree_id = db.root_id().clone();
let sync = instance.sync().expect("Sync enabled");
let peer_pubkey = PublicKey::random();
// Register a peer for automatic background sync
let handle = sync.register_sync_peer(SyncPeerInfo {
peer_pubkey,
tree_id,
addresses: vec![Address {
transport_type: "http".to_string(),
address: "http://peer.example.com:8080".to_string(),
}],
auth: None,
display_name: Some("Peer Device".to_string()),
}).await?;
Ok(())
}
Background sync happens automatically. Check status with handle.status()?.
Sync Settings
Configure per-database sync behavior:
extern crate eidetica;
extern crate tokio;
use eidetica::{Instance, backend::database::Sqlite, crdt::Doc};
use eidetica::account::types::SyncSettings;
#[tokio::main]
async fn main() -> eidetica::Result<()> {
let backend = Box::new(Sqlite::in_memory().await?);
let (instance, _) = Instance::connect_or_create_backend(
backend,
eidetica::NewAccount::passwordless("admin"),
).await?;
instance.enable_sync().await?;
let admin = instance.login_account("admin", "").await?;
admin.admin().await?.create_account(eidetica::NewAccount::passwordless("alice")).await?;
let mut user = instance.login_account("alice", "").await?;
let key = user.get_default_key()?;
let db = user.create_database(Doc::new(), &key).await?;
let db_id = db.root_id().clone();
// Track this database with sync on every commit + periodic sync every 60s
user.track_database(
db_id,
&user.get_default_key()?,
SyncSettings::on_commit().with_interval(60),
).await?;
Ok(())
}
Authenticated Bootstrap
For joining databases that require authentication:
use eidetica::{sync::DatabaseTicket, account::types::SyncSettings};
// Request database access through AccountSession API
user.request_database_access(
&sync,
&ticket,
&key_id, // Account's key ID from user.add_private_key()
eidetica::auth::Permission::Write,
SyncSettings::on_commit(),
None,
).await?;
See Bootstrap Guide for approval workflows.
Automatic Behavior
Once configured, the sync system handles:
- Immediate sync on commit (if
sync_on_commit: true) - Periodic sync at configured intervals
- Retry with exponential backoff for failed sends
- Bidirectional transfer in each sync operation
Troubleshooting
| Issue | Solution |
|---|---|
| “No transport enabled” | Call register_transport() with HTTP or Iroh transport builder |
| Can’t receive connections | Call accept_connections() to start servers |
| Sync not happening | Check peer status, network connectivity |
| Auth failures | Verify keys are configured, protocol versions match |
Example
See the Chat Example for a complete working application demonstrating multi-transport sync, bootstrap, and real-time updates.