pub trait DidDocumentStorage: Send + Sync {
// Required methods
fn get_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Document>>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn store_document<'life0, 'async_trait>(
&'life0 self,
document: Document,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn delete_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
}Expand description
Trait for implementing DID document CRUD operations across different storage backends.
This trait provides an abstraction layer for storing and retrieving DID documents, allowing different implementations for various storage systems such as databases, file systems, in-memory stores, or cloud storage services.
All methods return anyhow::Result to allow implementations to use their own error types
while providing a consistent interface for callers. Implementations should handle their
specific error conditions and convert them to appropriate error messages.
§Thread Safety
This trait requires implementations to be thread-safe (Send + Sync), meaning:
Send: The storage implementation can be moved between threadsSync: The storage implementation can be safely accessed from multiple threads simultaneously
This is essential for async applications where the storage might be accessed from different
async tasks running on different threads. Implementations should use appropriate
synchronization primitives (like Arc<Mutex<>>, RwLock, or database connection pools)
to ensure thread safety.
§Usage
Implementors of this trait can provide storage for AT Protocol DID documents in any backend:
use atproto_identity::storage::DidDocumentStorage;
use atproto_identity::model::Document;
use anyhow::Result;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
// Thread-safe in-memory storage using Arc<RwLock<>>
#[derive(Clone)]
struct InMemoryStorage {
data: Arc<RwLock<HashMap<String, Document>>>, // DID -> Document mapping
}
#[async_trait::async_trait]
impl DidDocumentStorage for InMemoryStorage {
async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>> {
let data = self.data.read().await;
Ok(data.get(did).cloned())
}
async fn store_document(&self, document: Document) -> Result<()> {
let mut data = self.data.write().await;
data.insert(document.id.clone(), document);
Ok(())
}
async fn delete_document_by_did(&self, did: &str) -> Result<()> {
let mut data = self.data.write().await;
data.remove(did);
Ok(())
}
}
// Database storage with thread-safe connection pool
struct DatabaseStorage {
pool: sqlx::Pool<sqlx::Postgres>, // Thread-safe connection pool
}
#[async_trait::async_trait]
impl DidDocumentStorage for DatabaseStorage {
async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>> {
// Database connection pools are thread-safe
let row: Option<(serde_json::Value,)> = sqlx::query_as(
"SELECT document FROM did_documents WHERE did = $1"
)
.bind(did)
.fetch_optional(&self.pool)
.await?;
if let Some((doc_json,)) = row {
let document: Document = serde_json::from_value(doc_json)?;
Ok(Some(document))
} else {
Ok(None)
}
}
async fn store_document(&self, document: Document) -> Result<()> {
let doc_json = serde_json::to_value(&document)?;
sqlx::query("INSERT INTO did_documents (did, document) VALUES ($1, $2) ON CONFLICT (did) DO UPDATE SET document = $2")
.bind(&document.id)
.bind(doc_json)
.execute(&self.pool)
.await?;
Ok(())
}
async fn delete_document_by_did(&self, did: &str) -> Result<()> {
sqlx::query("DELETE FROM did_documents WHERE did = $1")
.bind(did)
.execute(&self.pool)
.await?;
Ok(())
}
}Required Methods§
Sourcefn get_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Document>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Document>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Retrieves a DID document associated with the given DID.
This method looks up the complete DID document that is currently stored for the provided DID (Decentralized Identifier). The document contains services, verification methods, and other identity information for the DID.
§Arguments
did- The DID (Decentralized Identifier) to look up. Should be in the formatdid:method:identifier(e.g., “did:plc:bv6ggog3tya2z3vxsub7hnal”)
§Returns
Ok(Some(document))- If a document is found for the given DIDOk(None)- If no document is currently stored for the DIDErr(error)- If an error occurs during retrieval (storage failure, invalid DID format, etc.)
§Examples
let storage = MyStorage::new();
let document = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
match document {
Some(doc) => {
println!("Found document for DID: {}", doc.id);
if let Some(handle) = doc.handles() {
println!("Primary handle: {}", handle);
}
},
None => println!("No document found for this DID"),
}Sourcefn store_document<'life0, 'async_trait>(
&'life0 self,
document: Document,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn store_document<'life0, 'async_trait>(
&'life0 self,
document: Document,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Stores or updates a DID document.
This method creates a new DID document entry or updates an existing one. In the AT Protocol ecosystem, this operation typically occurs when a DID document is resolved from the network, updated by the identity owner, or cached for performance.
Implementations should ensure that:
- The document’s DID (
document.id) is used as the key for storage - The operation is atomic (either fully succeeds or fully fails)
- Any existing document for the same DID is properly replaced
- The complete document structure is preserved
§Arguments
document- The complete DID document to store. The document’sidfield will be used as the storage key.
§Returns
Ok(())- If the document was successfully stored or updatedErr(error)- If an error occurs during the operation (storage failure, serialization failure, constraint violation, etc.)
§Examples
let storage = MyStorage::new();
let document = Document {
id: "did:plc:bv6ggog3tya2z3vxsub7hnal".to_string(),
also_known_as: vec!["at://alice.bsky.social".to_string()],
service: vec![/* services */],
verification_method: vec![/* verification methods */],
extra: HashMap::new(),
};
storage.store_document(document).await?;
println!("Document successfully stored");Sourcefn delete_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn delete_document_by_did<'life0, 'life1, 'async_trait>(
&'life0 self,
did: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Deletes a DID document by its DID.
This method removes a DID document from storage using the DID as the identifier. This operation is typically used when cleaning up expired cache entries, removing invalid documents, or when an identity is deactivated.
Implementations should:
- Handle the case where the DID doesn’t exist gracefully (return Ok(()))
- Ensure the deletion is atomic
- Clean up any related data or indexes
- Preserve referential integrity if applicable
§Arguments
did- The DID identifying the document to delete. Should be in the formatdid:method:identifier(e.g., “did:plc:bv6ggog3tya2z3vxsub7hnal”)
§Returns
Ok(())- If the document was successfully deleted or didn’t existErr(error)- If an error occurs during deletion (storage failure, etc.)
§Examples
let storage = MyStorage::new();
storage.delete_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
println!("Document deleted");