The spec

atcomm binds an atproto identity to end-to-end-encrypted DIDComm v2 messaging via two lexicons and one message type. The binding is the open interop contract: any client, in any language, can implement it.

Identifiers

keyPackage record

Published by the user at rkey: self in their own PDS. Senders resolve a handle to this record to learn the messaging key and mediator(s).

{
  "$type": "at.didcomm.keyPackage",
  "did": "did:peer:2...",                 // DIDComm messaging key (X25519 keyAgreement)
  "mediators": ["did:web:wa.mediators.didcomm.at"],   // failover order
  "label": "phone",
  "createdAt": "2026-06-14T00:00:00Z"
}

dm message body

The schema describes the decrypted body carried inside the encrypted envelope. It is never stored as a record; infrastructure sees ciphertext only. The shape mirrors chat.bsky.convo message input so existing atproto clients can render it with the code they already have.

{
  "$type": "at.didcomm.dm",
  "senderDid": "did:plc:...",   // sender's atproto account DID (attribution claim)
  "text": "hello",
  "facets": [ /* app.bsky.richtext.facet — links, mentions */ ],
  "embed":  { /* app.bsky.embed.record — quote a public post */ }
}

Note on senderDid: it is a claim. It is carried but not yet cryptographically verified in the current implementation. Verification (resolve the DID, fetch its keyPackage, confirm it advertises the envelope's sender key) is planned. Until then, treat the attribution as unverified.

Message envelope

DIDComm v2, ECDH-1PU authenticated encryption (authcrypt, A256CBC-HS512). The message is encrypted to the recipient, then wrapped in a routing forward addressed to the recipient's mediator. Cross-mediator delivery double-wraps so the message transits the sender's own mediator first. Only the participants can read the body.

Lexicons

Canonical machine-readable schemas: